小程序开发笔记

6,738 阅读19分钟

章节规划

  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.cn/post/684490…