小程式開發筆記

磨劍十年發表於2018-10-17

章節規劃

  1. 目錄結構與開發約定
  2. 工具類封裝
  3. App.js中工具方法的封裝
  4. 元件封裝
  5. 一點說明

框架構建與約定

目錄規劃

.
├── assets
│   ├── imgs                    // 存放大圖,GIF
│   ├── audios                  // 存放靜態MP3,非常小的,否則應該放再雲上
├── components                  // 元件
│   ├── player                  // 音訊播放元件:底欄播放器、播放頁面播放器
│   │   ├── icons               // 元件專用的圖片資源
│   │   ├── player.*            // 播放頁面播放器元件
│   │   ├── miniplayer.*        // 底欄播放器元件
│   ├── wedialog                // 對話方塊元件
│   │   ├── wedialog.*          // 對話方塊元件:包含喚起授權按鈕
│   ├── footer.wxml             // 統一引入元件的wxml
├── config                      // 配置檔案
│   ├── config.js
├── http                        // 所有與請求相關的部分
│   ├── libs                    // 與請求相關的libs
│   │   ├── tdweapp.js          // 與talkingdata
│   │   ├── tdweapp-conf.js     // 與請求相關的libs
│   ├── ajax.js                 // 結合業務需要,對wx.request的封裝
│   ├── analysisService.js      // 依賴ajax.js,對事件統計系統的介面封裝
│   ├── api.js                  // 結合config.js,對所有介面API地址,與開發環境配合,封裝的介面地址
│   ├── businessService.js      // 依賴ajax.js,對業務介面封裝
│   ├── config.js               // 介面請求相關引數,與服務端系統配套,同時還有開發環境切換
│   ├── eventReporter.js        // 依賴analysisService.js,封裝所有事件上報介面,統一管理
│   ├── md5.min.js
├── libs                        // 通用的libs
│   ├── base64.js
│   ├── crypto-js.js            // 加密庫
│   ├── wx.promisify.js         // wx介面Promise化封裝
├── media-manager               // 媒體管理庫
│   ├── bgAudio.js              // wx.backgroundAudioManager操作封裝
│   ├── recorder.js             // wx.getRecorderManager操作封裝
│   ├── innerAudio.js           // wx.createInnerAudioContext操作封裝
├── pages                       // 小程式頁面
├── utils                       // 工具庫
│   ├── utils.js
├── app.js
├── app.json
├── app.wxss
├── project.config.json
複製程式碼

開發工具選擇與設定

Visual Studio Code      -- 程式碼編寫
微信開發者工具            -- 除錯/預覽/上傳等
Git                     -- 程式碼版本控制
複製程式碼

Visual Studio Code 設定

安裝外掛:minapp,小程式助手

設定自動儲存檔案,延遲事件改為1分鐘,這樣可以避免頻繁的觸發微信開發者工具重新整理工程。
複製程式碼

微信開發者工具

用於新建頁面,除錯,提交微信平臺。
複製程式碼
⚠️ 新建頁面,一定通過微信開發者工具上的app.json檔案新增

即,在app.json下的pages下新增需要新建的頁面,然後儲存,開發者工具就會自動建立好頁面模版。

{
    "pages": [
        "pages/index",
        "pages/mine",
        "pages/rankings",
        "pages/audio",
        "pages/recording",
        "pages/recordingOk",
        "pages/shareBack",
        "pages/test/test"
    ],
}
複製程式碼

Git 管理程式碼版本

嚴格按照Git工作流管理程式碼版本。

深入理解學習Git工作流(git-workflow-tutorial)

工具類封裝

清單

.
├── http                        // 所有與請求相關的部分
│   ├── libs                    // 與請求相關的libs
│   ├── ajax.js                 // 結合業務需要,對wx.request的封裝
│   ├── analysisService.js      // 依賴ajax.js,對事件統計系統的介面封裝
│   ├── api.js                  // 結合config.js,對所有介面API地址,與開發環境配合,封裝的介面地址
│   ├── businessService.js      // 依賴ajax.js,對業務介面封裝
│   ├── config.js               // 介面請求相關引數,與服務端系統配套,同時還有開發環境切換
│   ├── eventReporter.js        // 依賴analysisService.js,封裝所有事件上報介面,統一管理
├── libs                        // 通用的libs
│   ├── wx.promisify.js         // wx介面Promise化封裝
├── utils                       // 工具庫
│   ├── utils.js
複製程式碼

工具詳細開發過程

wx介面Promise化

wx介面還是基於ES5規範開發,對於ES6都橫行霸道好幾年的js開發社群來說,是在沒有心情在寫無限回撥,所以使用Proxy方式,將wx下的所有函式屬性都代理成Promise方式。

編寫方式參考:[深度揭祕ES6代理Proxy](https://blog.csdn.net/qq_28506819/article/details/71077788)
複製程式碼
// wx.promisify.js

/**
 * 定義一個空方法,用於統一處理,不需要處理的wx方法回撥,避免重複定義,節省資源
 */
let nullFn = () => { };


/**
 * 自定義錯誤型別
 */

class IllegalAPIException {
    constructor(name) {
        this.message = "No Such API [" + name + "]";
        this.name = 'IllegalAPIException';
    }
}

/**
 * 擴充套件的工具方法
 */
let services = {
    /**
     * 延遲方法
     */
    sleep: (time) => new Promise((resolve) => setTimeout(resolve, time)),

    /**
     * 用於中斷呼叫鏈
     */
    stop: () => new Promise(() => { }),

    /**
     * 空方法,只是為了使整個呼叫鏈排版美觀
     */
    taskSequence: () => new Promise((resolve) => resolve()),
    
};

const WxPromisify = new Proxy(services, {
    get(target, property) {
        if (property in target) {
            return target[property];
        } else if (property in wx) {
            return (obj) => {
                return new Promise((resolve, reject) => {
                    obj = obj || {};
                    obj.success = (...args) => {
                        resolve(...args)
                    };
                    obj.fail = (...args) => {
                        reject(...args);
                    };
                    obj.complete = nullFn;
                    wx[property](obj);
                });
            }
        } else {
            throw new IllegalAPIException(property);
        }
    }
});


/**
 * 對外暴露代理例項,處理所有屬性呼叫,包含:自定義擴充套件方法,wx物件
 */
export { WxPromisify };
複製程式碼
使用樣例
wxPromisify.taskSequence()
    .then(() => wxPromisify.showLoading({title: "儲存中"}))
    .then(() => wxPromisify.sleep(1000))
    .then(() => wxPromisify.hideLoading())
    .then(() => wxPromisify.sleep(500))
    .then(() => wxPromisify.showLoading({title: "載入中"}))
    .then(() => wxPromisify.sleep(1000))
    .then(() => wxPromisify.hideLoading())
    .then(() => console.debug("done"));
 
wxPromisify.taskSequence()
    .then(() => wxPromisify.showModal({title: "儲存", content: "確定儲存?"}))
    .then(res => {
        if (!res.confirm) {
            return wxPromisify.stop();
        }
    })
    .then(() => console.debug("to save"))
    .then(() => wxPromisify.showLoading({title: "儲存中"}))
    .then(() => wxPromisify.sleep(1000))
    .then(() => wxPromisify.hideLoading())
    .then(() => console.debug("done"));
複製程式碼

wx.request二次封裝

二次封裝的理由
  1. 回撥方式,不好用,會無限巢狀;

  2. wx.request介面併發有限制,目前限制最大數為10,這個在開發過程中,會遇到瓶頸,需要處理;

  3. 錯誤資訊,多種多樣,不適合UI層面上提示;

  4. 需要做錯誤的統一處理;

  5. 需要埋點上報錯誤資訊;

  6. 需要統一監聽網路連線情況,並統一處理網路變化;

程式碼封裝
const RequestTimeMap = {};

// 網路請求,錯誤編碼
const NetErrorCode = {
  WeakerNet: 100,
  BrokenNet: 110,
  ServerErr: 120,
  Unexcepted: 190,
};


let isConnected = true;
let isWeakerNetwork = false;
let networkType = 'wifi';


/**
 * 自定義網路錯誤類,
 * 增加code,用於標識錯誤型別
 * 
 * @author chenqq
 * @version v1.0.0
 * 
 * 2018-09-18 11:00
 */
class NetError extends Error {
  constructor(code, message) {
    super(message);
    this.name = 'NetError';
    this.code = code;
  }
}


/**
 * wx.request介面請求,併發控制工具類,使用快取方式,將超限的介面併發請求快取,等待介面完成後,繼續傳送多餘的請求。
 * 
 * @author chenqq
 * @version v1.0.0
 * 
 * 2018-09-17 11:50
 */
const ConcurrentRequest = {
  // request、uploadFile、downloadFile 的最大併發限制是10個,
  // 所以,考慮uploadFile與downloadFile,應該將request最大定為8
  MAX_REQUEST: 8,
  // 所有請求快取
  reqMap: {},
  // 當前所有請求key值快取表
  mapKeys: [],
  // 正在請求的key值表
  runningKeys: [],

  /**
   * 內部方法
   * 增加一個請求
   * 
   * @param {Object} param wx.request介面的引數物件
   */
  _add(param) {
    // 給param增加一個時間戳,作為存入map中的key
    param.key = +new Date();

    while ((this.mapKeys.indexOf(param.key) > -1) || (this.runningKeys.indexOf(param.key) > -1)) {
      // 若key值,存在,說明介面併發被併發呼叫,這裡做一次修復,加上一個隨機整數,避免併發請求被覆蓋
      param.key += Math.random() * 10 >> 0;
    }

    param.key += '';

    this.mapKeys.push(param.key);
    this.reqMap[param.key] = param;
  },

  /**
   * 內部方法
   * 傳送請求的具體控制邏輯
   */
  _next() {
    let that = this;

    if (this.mapKeys.length === 0) {
      return;
    }

    // 若正在傳送的請求數,小於最大併發數,則傳送下一個請求
    if (this.runningKeys.length <= this.MAX_REQUEST) {
      let key = this.mapKeys.shift();
      let req = this.reqMap[key];
      let completeTemp = req.complete;

      // 請求完成後,將該請求的快取清除,然後繼續新的請求
      req.complete = (...args) => {
        that.runningKeys.splice(that.runningKeys.indexOf(req.key), 1);
        delete that.reqMap[req.key];
        completeTemp && completeTemp.apply(req, args);
        console.debug('~~~complete to next request~~~', this.mapKeys.length);
        that._next();
      }

      this.runningKeys.push(req.key);
      return wx.request(req);
    }
  },

  /**
   * 對外方法
   * 
   * @param {Object} param 與wx.request引數一致
   */
  request(param) {
    param = param || {};

    if (typeof (param) === 'string') {
      param = { url: param };
    }

    this._add(param);

    return this._next();
  },

}


/**
 * 封裝wx.request介面用於傳送Ajax請求,
 * 同時還可以包含:wx.uploadFile, wx.downloadFile等相關介面。
 * 
 * @author chenqq
 * @version v1.0.0
 */
class Ajax {
  /**
   * 建構函式,需要兩個例項引數
   * 
   * @param {Signature} signature Signature例項
   * @param {UserAgent} userAgent UserAgent例項
   */
  constructor(signature, userAgent) {
    this.signature = signature;
    this.userAgent = userAgent;
  }

  /**
   * Ajax Get方法
   * 
   * @param {String} url 請求介面地址
   * @param {Object} data 請求資料,會自動處理成get的param資料
   * 
   * @returns Promise
   */
  get(url, data = {}) {
    let that = this;
    return new Promise((resolve, reject) => {
      if (!isConnected) {
        reject(new NetError(NetErrorCode.BrokenNet, '當前網路已斷開,請檢查網路設定!'));
        return;
      }

      if (isWeakerNetwork) {
        reject(new NetError(NetErrorCode.WeakerNet, '當前網路較差,請檢查網路設定!'));
        return;
      }

      request(that.signature, that.userAgent, url, data,
        'GET', 'json', resolve, reject);
    });
  }

  /**
   * Ajax Post方法
   * 
   * @param {String} url 請求介面地址
   * @param {Object} data 請求資料
   * 
   * @returns Promise
   */
  post(url, data = {}) {
    let that = this;
    return new Promise((resolve, reject) => {
      if (!isConnected) {
        reject(new NetError(NetErrorCode.BrokenNet, '當前網路已斷開,請檢查網路設定!'));
        return;
      }

      if (isWeakerNetwork) {
        reject(new NetError(NetErrorCode.WeakerNet, '當前網路較差,請檢查網路設定!'));
        return;
      }

      request(that.signature, that.userAgent, url, data,
        'POST', 'json', resolve, reject);
    });
  }

  /**
   * 
   * @param {String} url 下載檔案地址
   * @param {Function} progressCallback 下載進度更新回撥
   */
  downloadFile(url, progressCallback) {
    return new Promise((resolve, reject) => {
      if (!isConnected) {
        reject(new NetError(NetErrorCode.BrokenNet, '當前網路已斷開,請檢查網路設定!'));
        return;
      }

      const downloadTask = wx.downloadFile({
        url,
        success(res) {
          // 注意:只要伺服器有響應資料,就會把響應內容寫入檔案並進入 success 回撥,
          // 業務需要自行判斷是否下載到了想要的內容
          if (res.statusCode === 200) {
            resolve(res.tempFilePath);
          }
        },
        fail(err) {
          reject(err);
        }
      });

      if (progressCallback) {
        // 回撥引數res物件:
        // progress                 	number	下載進度百分比	
        // totalBytesWritten	        number	已經下載的資料長度,單位 Bytes	
        // totalBytesExpectedToWrite	number	預期需要下載的資料總長度,單位 Bytes
        downloadTask.onProgressUpdate = progressCallback;
      }
    });
  }

  /**
   * 設定介面請求資訊上報處理器
   * 
   * succeed, isConnected, networkType, url, time, errorType, error
   */
  static setOnRequestReportHandler(handler) {
    _requestReportHandler = handler;
  }

  /**
   * 設定網路狀態監聽,啟用時,會將網路連線狀態,同步用於控制介面請求。
   * 
   * 若網路斷開連線,介面直接返回。
   */
  static setupNetworkStatusChangeListener() {
    if (wx.onNetworkStatusChange) {
      wx.onNetworkStatusChange(res => {
        isConnected = !!res.isConnected;
        networkType = res.networkType;
        if (!res.isConnected) {
          toast('當前網路已斷開');
        } else {
          if ('2g, 3g, 4g'.indexOf(res.networkType) > -1) {
            toast(`已切到資料網路`);
          }
        }
      });
    }
  }

  static getNetworkConnection() {
    return !!isConnected;
  }

  /**
   * 設定小程式版本更新事件監聽,根據小程式版本更新機制說明,
   * https://developers.weixin.qq.com/miniprogram/dev/framework/operating-mechanism.html
   * 
   * 需要立即使用新版本,需要監聽UpdateManager事件,有開發者主動實現。
   * 
   * 這裡,若是檢測到有更新,並且微信將新版本程式碼下載完成後,會使用對話方塊進行版本更新提示,
   * 引導使用者重啟小程式,立即應用小程式。
   */
  static setupAppUpdateListener() {
    let updateManager = null
    if (wx.getUpdateManager) {
      updateManager = wx.getUpdateManager()
    } else {
      return
    }

    updateManager.onCheckForUpdate(function (res) {
      // 請求完新版本資訊的回撥
      //console.debug('是否有新版本:', res.hasUpdate);
    });

    updateManager.onUpdateReady(function () {
      wx.showModal({
        title: '更新提示',
        content: '新版本已經準備好,是否重啟應用?',
        confirmText: '重 啟',
        showCancel: false,
        success: function (res) {
          if (res.confirm) {
            // 新的版本已經下載好,呼叫 applyUpdate 應用新版本並重啟
            updateManager.applyUpdate()
          }
        }
      });
    });

    updateManager.onUpdateFailed(function () {
      // 新的版本下載失敗
      //console.error("新的版本下載失敗!");
    });
  }

  static setupNetSpeedListener(url, fileSize, minSpeed = 10) {
    let start = +new Date();
    this.downloadFile(url, res => {
      // totalBytesWritten number 已經下載的資料長度,單位 Bytes
      let { totalBytesWritten } = res;
      // 轉kb
      totalBytesWritten /= 1024;
      // 下載耗時,單位毫秒
      let div = (+new Date()) - start;
      // 轉秒
      div /= 1000;
      // 單位為: kb/s
      let speed = div > 0 ? totalBytesWritten / div : totalBytesWritten;

      if (speed < minSpeed) {
        isWeakerNetwork = true;
        toast('~~當前網路較差,請檢查網路設定~~');
      } else {
        isWeakerNetwork = false;
      }
    }).then(res => {
      if (fileSize > 0) {
        // 下載耗時,單位毫秒
        let div = (+new Date()) - start;
        // 轉秒
        div /= 1000;
        // 單位為: kb/s
        let speed = div > 0 ? fileSize / div : fileSize;

        if (speed < minSpeed) {
          isWeakerNetwork = true;
          toast('~~當前網路較差,請檢查網路設定~~');
        } else {
          isWeakerNetwork = false;
        }
      }
    });
  }

}

function toast(title, duration = 2000) {
  wx.showToast({
    icon: 'none',
    title,
    duration
  });
}


/**
 * 基於wx.request封裝的request
 * 
 * @param {Signature} signature Signature例項
 * @param {UserAgent} userAgent UserAgent例項
 * @param {String} url 請求介面地址
 * @param {Object} data 請求資料
 * @param {String} method 請求方式
 * @param {String} dataType 請求資料格式
 * @param {Function} successCbk 成功回撥
 * @param {Function} errorCbk 失敗回撥
 *
 * @returns wx.request例項返回的控制物件requestTask
 */
function request(signature, userAgent, url, data, method, dataType = 'json', successCbk, errorCbk) {
  console.debug(`#### ${url} 開始請求...`, userAgent, data);

  let start = +new Date();

  // 記錄該url請求的開始時間
  RequestTimeMap[url] = start;

  // 加密方法處理請求資料,返回結構化的結果資料
  let req = encryptRequest(signature, userAgent, url, data, errorCbk);

  return ConcurrentRequest.request({
    url: req.url,
    data: req.data,
    header: req.header,
    method,
    dataType,
    success: res => decrypyResponse(url, signature, res, successCbk, errorCbk),
    fail: error => {
      console.error(`#### ${url} 請求失敗:`, error);
      reportRequestAnalytics(false, url, 'wx發起請求失敗', error);
      wx.showToast({
        title: '網路不給力,請檢查網路設定!',
        icon: 'none',
        duration: 1500
      });
      errorCbk && errorCbk(new NetError(NetErrorCode.BrokenNet, '網路不給力,請檢查網路設定!'));
    },
    complete: () => {
      console.debug(`#### ${url} 請求完成!`);
      console.debug(`#### ${url} 本次請求耗時:`, (+new Date()) - start, 'ms');
    }
  });
}
複製程式碼

程式碼註釋都比較全,就不多說明;這裡解釋下:Signature,UserAgent例項,以及encryptRequest,decrypyResponse函式;都與服務端資料請求加解密有關。

Ajax 類還包含了App更新監聽,以及網路狀態變化監聽,弱網監測等實用性監聽器,屬於靜態方法,在App中直接設定即可,簡單,方便。

介面地址結合開發環境封裝處理

這裡,為什麼不用webpack等工具,開發CLI,這個目前在規劃中。。。現在直接上程式碼
複製程式碼
// http/config.js
const VersionName = '1.2.1';
const VersionCode = 121;
// const Environment = 'development';
// const Environment = 'testing';
const Environment = 'production';


export default {
  environment: Environment,
  minWxSDKVersion: '2.0.0',
  versionName: VersionName,
  versionCode: VersionCode,
  enableTalkingData: false,
  // 使用者中心繫統與業務資料系統使用同一個配置
  business: {
    // 使用者中心介面Host
    userCenterHost: {
      development: 'https://xxx',
      testing: 'https://xxx',
      production: 'https://xxx',
    },
    // 業務資料介面Host
    businessHost: {
      development: 'http://xxx',
      testing: 'https://xxx',
      production: 'https://xxx',
    },
    // 簽名金鑰
    sign: {},
    // 預設的 UserAgent
    defaultUserAgent: {
      "ProductID": 3281,
      "CHID": 1,
      "VerID": VersionCode,
      "VerCode": VersionName,
      "CHCode": "WechatApp",
      "ProjectID": 17,
      "PlatForm": 21
    },
  },
  // 分析系統使用的配置
  analysis: {
    host: {
      development: 'https://xxx',
      testing: 'https://xxx',
      production: 'https://xxx',
    },
    // 簽名金鑰
    sign: {},
    // UserAgent 需要的引數
    defaultUserAgent: {
      "ProductID": 491,
      "CHID": 1,
      "VerID": VersionCode,
      "VerCode": VersionName,
      "CHCode": "WechatApp",
      "ProjectID": 17,
      "PlatForm": 21,
      "DeviceType": 1
    }
  },
  // 網路型別編碼
  networkType: {
    none: 0,
    wifi: 1,
    net2G: 2,
    net3G: 3,
    net4G: 4,
    net5G: 5,
  },
  /**
   * 統一配置本地儲存中需要用到的Key
   */
  dataKey: {
    userInfo: 'UserInfo', // 值為:微信使用者資訊或者是伺服器介面返回的userInfo
    session: 'SessionKey', // 值為:伺服器返回的session
    code: 'UserCode', // 值為:伺服器返回的userCode
    StorageEventKey: 'StorageEvent', // 用於快取上報分析系統事件池資料
  }
}
複製程式碼
// http/api.js
import Configs from './config';

const Environment = Configs.environment;
const UCenterHost = Configs.business.userCenterHost[Environment];
const BusinessHost = Configs.business.businessHost[Environment];
const AnalysisHost = Configs.analysis.host[Environment];

export default {
  Production: Environment === 'production',

  /** 業務相關介面 */
  // 獲取首頁資料
  HomePage: BusinessHost + '/sinology/home',



  /** 分析系統相關介面 */

  // 裝置報導 -- 即裝置開啟App與退出App整個週期時長資訊上報
  StatRegister: AnalysisHost + '/Stat/Register',

  // 統計事件,上報介面
  StatUserPath: AnalysisHost + '/Stat/UserPath',
}
複製程式碼

這樣,版本號,介面環境,就在config.js檔案中直接修改,簡單方便。

其他幾個檔案的說明

http資料夾
analysisService.js, businessService.js這兩個檔案,就是基於Ajax類與api介面進行實際的介面請求封裝;businessService.js是業務相關介面封裝,analysisService.js是與後臺對應的資料分析系統介面封裝。

eventReporter.js這個檔案,是微信事件上報,後臺分析系統事件上報,TalkingData資料上報的統一封裝。封裝這個類是由於三個事件系統,對於事件的ID,名稱,事件資料屬性規範都不同,為了保證對外呼叫時,引數都保持一致,將三個平臺的同一個埋點事件,封裝成一個函式方法,使用統一的引數,降低編碼複雜度,降低維護成本。
複製程式碼
utils資料夾
至於,utils資料夾下的工具檔案,就基本上封裝當前小程式工程,需要使用到的工具方法即可,這個資料夾儘量避免拷貝,減少冗餘。
複製程式碼

App.js中工具方法的封裝

為什麼把這些函式,封裝到App中,主要是考慮這些函式都使用頻繁,放入App中,呼叫方便,全域性都能使用,不需要而外import。

工具方法包含了:
    預存/預取資料操作,
    獲取當前前臺頁面例項,
    頁面導航統一封裝,
    提示對話方塊,
    無圖示Toast,
    快速操作攔截,
    延遲處理器,
    Storage緩衝二次封裝,
    頁面間通訊實現(emitEvent),
    獲取裝置資訊,
    rpx-px相互轉化,
    計算scrollview能夠使用的剩餘高度,
    函式防抖/函式節流
複製程式碼
const GoToType = {
    '1': '/pages/index',
    '2': '/pages/audio',
    '20': '/pages/rankings',
    '22': '/pages/mine',
    '25': '/pages/recording',
    '28': '/pages/shareBack',
};

App({

    onLaunch() {
        this.pagePreLoad = new Map();
    },

    /**
     * 用於儲存頁面跳轉時,預請求的Promise例項
     * 該介面應該用於在頁面切換時呼叫,充分利用頁面載入過程
     * 這裡,只做成單條資料快取
     * 
     * @param {String} key
     * @param {Promise} promise 
     */
    putPreloadData(key, promise) {
        this.pagePreLoad.set(key, promise);
    },

    /**
     * 獲取頁面預請求的Promise例項,用於後續的介面資料處理,
     * 取出後,立即清空
     * 
     * @param {String} key
     */
    getPreloadData(key) {
        let temp = this.pagePreLoad.get(key);
        this.pagePreLoad.delete(key);
        return temp;
    },

    getActivePage() {
        let pages = getCurrentPages();
        return pages[pages.length - 1];
    },

    /**
     * 全域性控制頁面跳轉
     * 
     * @param {String} key 快取預請求的資料key
     * @param {Object} item 跳轉點選的節點對應的資料資訊
     * @param {Object} from 頁面來源描述資訊
     */
    navigateToPage(key, item, from, route = true, method = 'navigate') {
        if (item.go.type === 'undefined') {
            return;
        }

        key && this.putPreloadData(key, BusinessService.commonRequest(item.go.url));

        if (route) {
            let url = GoToType[item.go.type + ''];
            EventReporter.visitPage(from);
            if (method === 'redirect') {
                wx.redirectTo({
                    url,
                    success(res) {
                        console.debug('wx.redirectTo', url, res);
                    },
                    fail(err) {
                        console.error('wx.redirectTo', url, err);
                    }
                });
            } else {
                wx.navigateTo({
                    url,
                    success(res) {
                        console.debug('wx.navigateTo', url, res);
                    },
                    fail(err) {
                        console.error('wx.navigateTo', url, err);
                    }
                });
            }
        }
    },

    showDlg({
        title = '提示',
        content = '',
        confirmText = '確定',
        confirmCbk,
        cancelText = '取消',
        cancelCbk }) {

        wx.showModal({
            title,
            content,
            confirmText,
            cancelText,
            success: (res) => {
                if (res.confirm) {
                    confirmCbk && confirmCbk();
                } else if (res.cancel) {
                    cancelCbk && cancelCbk();
                }
            }
        });
    },

    toast(title) {
        wx.showToast({
            icon: 'none',
            title
        });
    },

    isFastClick() {
        let time = (new Date()).getTime();
        let div = time - this.lastClickTime;

        let isFastClick = div < 800;

        if (!isFastClick) {
            this.lastClickTime = time;
        }

        isFastClick && console.debug("===== FastClick =====");

        return isFastClick;
    },

    asyncHandler(schedule, time = 100) {
        setTimeout(schedule, time);
    },

    setStorage(key, data, callback, retry = true) {
        let that = this;
        if (callback) {
            wx.setStorage({
                key,
                data,
                success: callback,
                fail: err => {
                    console.error(`setStorage error for key: ${key}`, err);
                    if (typeof (retry) === 'function') {
                        retry(err);
                    } else {
                        retry && that.setStorage(key, data, callback, false);
                    }
                },
                complete: () => console.debug('setStorage complete'),
            });
        } else {
            try {
                wx.setStorageSync(key, data);
            } catch (err) {
                console.error(`setStorageSync error for key: ${key}`, err);
                retry && this.setStorage(key, data, callback, false);
            }
        }
    },

    getStorage(key, callback, retry = true) {
        let that = this;
        if (callback) {
            wx.getStorage({
                key,
                success: callback,
                fail: err => {
                    console.error(`getStorage error for key: ${key}`, err);
                    if (typeof (retry) === 'function') {
                        retry(err);
                    } else {
                        retry && that.getStorage(key, callback, false);
                    }
                },
                complete: () => console.debug('getStorage complete'),
            });
        } else {
            try {
                return wx.getStorageSync(key);
            } catch (err) {
                console.error(`getStorageSync error for key: ${key}`, err);
                retry && this.getStorage(key, callback, false);
            }
        }
    },

    /**
     * 事件分發方法,可以在元件中使用,也可以在頁面中使用,方便頁面間資料通訊,特別是頁面資料的狀態同步。
     * 
     * 預設只分發給當前頁面,若是全部頁面分發,會根據事件消費者返回的值,進行判斷是否繼續分發,
     * 即頁面事件消費者,可以決定該事件是否繼續下發。
     * 
     * @param {String} name 事件名稱,即頁面中註冊的用於呼叫的方法名
     * @param {Object} props 事件資料,事件傳送時傳遞的資料,可以是String,Number,Boolean,Object等,視具體事件處理邏輯而定,沒有固定格式
     * @param {Boolean} isAll 事件傳遞方式,是否全部頁面分發,預設分發給所有頁面
     */
    emitEvent(name, props, isAll = true) {
        let pages = getCurrentPages();
        if (isAll) {
            for (let i = 0, len = pages.length; i < len; i++) {
                let page = pages[i];
                if (page.hasOwnProperty(name) && typeof (page[name]) === 'function') {
                    // 若是在事件消費方法中,返回了true,則中斷事件繼續傳遞
                    if (page[name](props)) {
                        break;
                    }
                }
            }
        } else {
            if (pages.length > 1) {
                let lastPage = pages[pages.length - 2];
                if (lastPage.hasOwnProperty(name) && typeof (lastPage[name]) === 'function') {
                    lastPage[name](props);
                }
            }
        }
    },

    getSystemInfo() {
        return WxPromisify.taskSequence()
            .then(() => {
                if (this.systemInfo) {
                    return this.systemInfo;
                } else {
                    return WxPromisify.getSystemInfo();
                }
            });
    },

    getPxToRpx(px) {
        return WxPromisify.taskSequence()
            .then(() => this.getSystemInfo())
            .then(systemInfo => 750 / systemInfo.windowWidth * px);
    },

    getRpxToPx(rpx) {
        return WxPromisify.taskSequence()
            .then(() => this.getSystemInfo())
            .then(systemInfo => systemInfo.windowWidth / 750 * rpx);
    },

    getScrollViewSize(deductedSize) {
        return this.getSystemInfo()
            .then(res => this.getPxToRpx(res.windowHeight))
            .then(res => res - deductedSize);
    },

    /**
     * 函式防抖動:短時間內,執行最後一次呼叫,而忽略其他呼叫
     * 
     * 即防止短時間內,多次呼叫,因為短時間,多次呼叫,對於最終結果是多餘的,而且浪費資源。
     * 只要將短時間內呼叫的最後一次進行執行,就能滿足操作要求。
     * 
     * @param {Function} handler 處理函式
     * @param {Number} time 間隔時間,單位:ms
     */
    debounce(handler, time = 500) {
        clearTimeout(this.debounceTimer);
        this.debounceTimer = setTimeout(() => {
            handler && handler();
        }, time);
    },

    /**
     * 函式節流:短時間內,執行第一次呼叫,而忽略其他呼叫
     * 
     * 即短時間內不允許多次呼叫,比如快速點選,頁面滾動事件監聽,不能所有觸發都執行,需要忽略部分觸發。
     * 
     * @param {Function} handler 處理函式
     * @param {Number} time 間隔時間,單位:ms
     */
    throttle(handler, time = 500) {
        if (this.throttling) {
            return;
        }

        this.throttling = true;
        setTimeout(() => {
            this.throttling = false;
            handler && handler();
        }, time);
    },

    /**
     * 獲取當前網路連線情況
     */
    getNetworkConnection() {
        return Ajax.getNetworkConnection();
    },
})
複製程式碼

元件封裝

元件封裝有兩種方式

  1. 按照小程式開發文件的元件開發方式封裝,這裡就不介紹,唯一要說的是,元件使用到的資源,最好單獨放入元件資料夾中,這樣便於管理;

  2. 更具實際Page宣告,注入到相應的Page中,這裡給出詳細程式碼;

擴充套件的對話方塊元件

由於小程式官方使用者授權互動調整,獲取使用者資訊,開啟設定都需要使用按鈕方式,才能觸發,但是在開發中可能又不想設計多餘的獨立頁面,這時,就需要使用對話方塊了,微信提供的對話方塊又沒有辦法實現,所以需要封裝一個通用對話方塊。


元件統一放在components資料夾下。
複製程式碼
具體實現
<!-- wedialog.wxml -->
<template name="wedialog">
  <view class="wedialog-wrapper {{reveal ? 'wedialog-show' : 'wedialog-hide'}}" catchtouchmove="onPreventTouchMove">
    <view class="wedialog">
      <view class="wedialog-title">{{title}}</view>
      <text class="wedialog-message">{{message}}</text>
      <view class="wedialog-footer">
        <button class="wedialog-cancel" catchtap="onTapLeftBtn">{{leftBtnText}}</button>
        <button 
          class="wedialog-ok"
          open-type="{{btnOpenType}}"
          bindgetuserinfo="onGotUserInfo"
          bindgetphonenumber="onGotPhoneNumber"
          bindopensetting="onOpenSetting"
          catchtap="onTapRightBtn">{{rightBtnText}}</button>
      </view>
    </view>
  </view>
</template>
複製程式碼
/* wewedialog.wxss */

.wedialog-show {
  display: block;
}

.wedialog-hide {
  display: none;
}

.wedialog-wrapper {
  z-index: 999;
  position: fixed;
  top: 0;
  left: 0;
  width: 750rpx;
  height: 100%;
  background-color: rgba(80, 80, 80, 0.5);
}

.wedialog {
  z-index: 1000;
  position: absolute;
  top: 300rpx;
  left: 50%;
  width: 540rpx;
  margin-left: -270rpx;
  background: #fff;
  border-radius: 12rpx;
}

.wedialog-title {
  width: 540rpx;
  height: 34rpx;
  padding-top: 40rpx;
  text-align: center;
  font-size: 34rpx;
  font-weight: bold;
  color: #323236;
}

.wedialog-message {
  padding-top: 29rpx;
  padding-bottom: 42rpx;
  margin-left: 88rpx;
  display: block;
  width: 362rpx;
  font-size: 28rpx;
  color: #323236;
  text-align: center;
}

.wedialog-footer {
  position: relative;
  width: 540rpx;
  height: 112rpx;
  border-top: 1px solid #d9d9d9;
  border-bottom-right-radius: 12rpx;
  border-bottom-left-radius: 12rpx;
}

.wedialog-footer button {
  position: absolute;
  top: 0;
  display: block;
  margin: 0;
  padding: 0;
  width: 270rpx;
  height: 112rpx;
  line-height: 112rpx;
  background-color: #fff;
  border-bottom: 0.5rpx solid #eee;
  font-size: 34rpx;
  text-align: center;
}

.wedialog button::after {
  border: none;
}

.wedialog-cancel {
  left: 0;
  border-right: 1px solid #d9d9d9;
  color: #323236;
  border-radius: 0 0 0 12rpx;
}

.wedialog-ok {
  right: 0;
  border-radius: 0 0 12rpx 0;
  color: #79da8e;
}
複製程式碼
重點一:js如何封裝
/**
 * WeDialog by chenqq
 * 微信小程式Dialog增強外掛,按鈕只是設定button中的open-type,以及事件繫結
 */
function WeDialogClass() {

  // 建構函式
  function WeDialog() {
    let pages = getCurrentPages();
    let curPage = pages[pages.length - 1];
    this.__page = curPage;
    this.__timeout = null;

    // 附加到page上,方便訪問
    curPage.wedialog = this;

    return this;
  }

  /**
   * 更新資料,採用合併的方式,使用新資料對就資料進行更行。
   * 
   * @param {Object} data 
   */
  WeDialog.prototype.setData = function (data) {
    let temp = {};
    for (let k in data) {
      temp[`__wedialog__.${k}`] = data[k];
    }
    this.__page.setData(temp);
  };

  // 顯示
  WeDialog.prototype.show = function (data) {
    let page = this.__page;

    clearTimeout(this.__timeout);

    // display需要先設定為block之後,才能執行動畫
    this.setData({
      reveal: true,
    });

    setTimeout(() => {
      let animation = wx.createAnimation();
      animation.opacity(1).step();
      data.animationData = animation.export();
      data.reveal = true;
      this.setData(data);

      page.onTapLeftBtn = (e) => {
        data.onTapLeftBtn && data.onTapLeftBtn(e);
        this.hide();
      };

      page.onTapRightBtn = (e) => {
        data.onTapRightBtn && data.onTapRightBtn(e);
        this.hide();
      };

      page.onGotUserInfo = (e) => {
        data.onGotUserInfo && data.onGotUserInfo(e);
        this.hide();
      };

      page.onGotPhoneNumber = (e) => {
        data.onGotPhoneNumber && data.onGotPhoneNumber(e);
        this.hide();
      };

      page.onOpenSetting = (e) => {
        data.onOpenSetting && data.onOpenSetting(e);
        this.hide();
      };

      page.onPreventTouchMove = (e) => {};
      
    }, 30);

  }

  // 隱藏
  WeDialog.prototype.hide = function () {
    let page = this.__page;

    clearTimeout(this.__timeout);

    if (!page.data.__wedialog__.reveal) {
      return;
    }

    let animation = wx.createAnimation();
    animation.opacity(0).step();
    this.setData({
      animationData: animation.export(),
    });

    setTimeout(() => {
      this.setData({
        reveal: false,
      });
    }, 200)
  }

  return new WeDialog()
}

module.exports = {
  WeDialog: WeDialogClass
}
複製程式碼
重點二:如何使用
不知道在看檔案目錄結構時,有沒有注意到components資料夾下,有一個footer.wxml檔案,這個檔案就用用來統一管理該類元件的佈局引入的。
複製程式碼
<!-- footer.wxml -->
<import src="./player/miniplayer.wxml" />
<template is="miniplayer" data="{{...__miniplayer__}}" />


<import src="./wedialog/wedialog.wxml" />
<template is="wedialog" data="{{...__wedialog__}}" />
複製程式碼

樣式全域性引入

/* app.wxss */
@import "./components/player/miniplayer.wxss";
@import "./components/wedialog/wedialog.wxss";

複製程式碼

物件全域性引入

// app.js

import { WeDialog } from './components/wedialog/wedialog';

App {{
    // 全域性引入,方便使用
    WeDialog,

    onLaunch() {},
}}
複製程式碼

在需要元件的頁面,引入佈局

<!-- index.wxml -->

<include src="../components/footer.wxml"/>
複製程式碼

實際Page頁面中呼叫

// index.js

const App = getApp();

Page({
    onLoad(options) {
        App.WeDialog();

        this.wedialog.show({
            title: '授權設定',
            message: '是否允許授權獲取使用者資訊',
            btnOpenType: 'getUserInfo',
            leftBtnText: '取消',
            rightBtnText: '允許',
            onGotUserInfo: this.onGetUserInfo,
        });
    },

    onGetUserInfo(res) {
        // TODO 這裡接收使用者授權返回資料
    },
});
複製程式碼

一點說明

分頁列表資料對setData的優化

正常分頁資料格式


let list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// 分頁資料追加
list.concat([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);

// 再全量更新一次
this.setData({
    list,
});

複製程式碼

優化方案

let list = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]];

// 分頁資料追加
// page 為分頁數
let page = 1;
this.setData({
    [`list[${page}]`]: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
});

複製程式碼

這樣優化,就能將每次更新資料降到最低,加快setData更新效率,同時還能避免1024k的大小限制。這裡將分頁資料按照二維陣列拆分後,還能將原來的列表單項元件,重新優化,封裝成列表分頁元件分頁。

setData的其他使用優化

場景1:更新物件中的某個節點值

let user = {
    age: 20,
    nickName: 'CQQ',
    address: {
        name: '福建省福州市',
        code: '350000',
    },
};

this.setData({
    user,
});

// 修改address下的name 和 code
this.setData({
    [`user.address.name`]: '福建省廈門市',
    [`user.address.code`]: '361000',
});

複製程式碼

場景2:更新列表中指定索引上的值

let list = [1, 2, 3, 4];
let users = [{
    user: {
        age: 20,
        name: 'CQQ',
    },
},{
    user: {
        age: 50,
        name: 'YAA',
    },
},{
    user: {
        age: 60,
        name: 'NDK',
    },
}];

this.setData({
    list,
    users,
});


// 修改list index= 3的值
let index = 3;
this.setData({
    [`list[${index}]`]: 40,
});

// 修改users index = 1 的age值
index = 1;
this.setData({
    [`users[${index}].age`]: 40,
});

// 修改users index = 2 的age和name
index = 2;
this.setData({
    [`users[${index}]`]: {
        age: 10,
        name: 'KPP',
    },
});

// 或者

this.setData({
    [`users[${index}].age`]: 10,
    [`users[${index}].name`]: 'KPP',
});

複製程式碼

場景3:有時會需要在一個位置上,多次的使用setData,這時,應該結合UI上互動,做一些變通,儘量減少呼叫次數。

這一點上,可能會來自產品與設計師的壓力,但是為了效能考慮,儘可能的溝通好,做到一個平衡。
複製程式碼

圖片資源的使用

  1. 圖示資源,若是使用雪碧圖,那沒話說;

  2. 若不是使用雪碧圖,圖示能使用background-image最好,用image進行圖示佈局,在細節上會很難控制,而且能減少佈局層級,也對頁面優化有好處;

  3. 圖示,使用background-image方式,引入bage64字串,這樣,對於本地靜態圖示顯示上也有優勢,能夠第一時間顯示出來。


總結先到這裡,後續會加上InnerAduioContext,BackgroundAudioManager, RecordMananger, API的封裝。

轉載請註明出處:juejin.im/post/5bc70e…

相關文章