章节规划
- 目录结构与开发约定
- 工具类封装
- App.js中工具方法的封装
- 组件封装
- 一点说明
框架构建与约定
目录规划
.
├── 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
工作流管理代码版本。
工具类封装
清单
.
├── 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二次封装
二次封装的理由
-
回调方式,不好用,会无限嵌套;
-
wx.request接口并发有限制,目前限制最大数为10,这个在开发过程中,会遇到瓶颈,需要处理;
-
错误信息,多种多样,不适合UI层面上提示;
-
需要做错误的统一处理;
-
需要埋点上报错误信息;
-
需要统一监听网络连接情况,并统一处理网络变化;
代码封装
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();
},
})
复制代码
组件封装
组件封装有两种方式
-
按照小程序开发文档的组件开发方式封装,这里就不介绍,唯一要说的是,组件使用到的资源,最好单独放入组件文件夹中,这样便于管理;
-
更具实际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上交互,做一些变通,尽量减少调用次数。
这一点上,可能会来自产品与设计师的压力,但是为了性能考虑,尽可能的沟通好,做到一个平衡。
复制代码
图片资源的使用
-
图标资源,若是使用雪碧图,那没话说;
-
若不是使用雪碧图,图标能使用background-image最好,用image进行图标布局,在细节上会很难控制,而且能减少布局层级,也对页面优化有好处;
-
图标,使用background-image方式,引入bage64字符串,这样,对于本地静态图标显示上也有优势,能够第一时间显示出来。
总结先到这里,后续会加上InnerAduioContext,BackgroundAudioManager, RecordMananger, API的封装。
转载请注明出处:juejin.im/post/5bc70e…
作者:傾襁
链接:https://juejin.im/post/5bc70e275188255c5f541da1
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。