小程序開發筆記

章節規劃

  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下添加須要新建的頁面,而後保存,開發者工具就會自動建立好頁面模版。css

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

Git 管理代碼版本

嚴格按照Git工做流管理代碼版本。html

深刻理解學習Git工做流(git-workflow-tutorial)webpack

工具類封裝

清單

.
├── 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. 回調方式,很差用,會無限嵌套;ios

  2. wx.request接口併發有限制,目前限制最大數爲10,這個在開發過程當中,會遇到瓶頸,須要處理;git

  3. 錯誤信息,多種多樣,不適合UI層面上提示;web

  4. 須要作錯誤的統一處理;ajax

  5. 須要埋點上報錯誤信息;json

  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函數;都與服務端數據請求加解密有關。segmentfault

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…

相關文章
相關標籤/搜索