小程序开发笔记

栏目: IOS · Android · 发布时间: 5年前

内容简介:即,在app.json下的pages下添加需要新建的页面,然后保存,开发者工具就会自动创建好页面模版。严格按照
  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分钟,这样可以避免频繁的触发微信开发者工具刷新工程。
复制代码

微信开发者工具

用于新建页面,调试,提交微信平台。
复制代码

:warning: 新建页面,一定通过微信开发者工具上的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(() => wsAPI.showLoading({title: "保存中"}))
    .then(() => wsAPI.sleep(1000))
    .then(() => wsAPI.hideLoading())
    .then(() => wsAPI.sleep(500))
    .then(() => wsAPI.showLoading({title: "载入中"}))
    .then(() => wsAPI.sleep(1000))
    .then(() => wsAPI.hideLoading())
    .then(() => console.debug("done"));
 
wxPromisify.taskSequence()
    .then(() => wsAPI.showModal({title: "保存", content: "确定保存?"}))
    .then(res => {
        if (!res.confirm) {
            return wsAPI.stop();
        }
    })
    .then(() => console.debug("to save"))
    .then(() => wsAPI.showLoading({title: "保存中"}))
    .then(() => wsAPI.sleep(1000))
    .then(() => wsAPI.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…


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Building Web Reputation Systems

Building Web Reputation Systems

Randy Farmer、Bryce Glass / Yahoo Press / 2010 / GBP 31.99

What do Amazon's product reviews, eBay's feedback score system, Slashdot's Karma System, and Xbox Live's Achievements have in common? They're all examples of successful reputation systems that enable ......一起来看看 《Building Web Reputation Systems》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具