内容简介:即,在app.json下的pages下添加需要新建的页面,然后保存,开发者工具就会自动创建好页面模版。严格按照
- 目录结构与开发约定
- 工具类封装
- 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分钟,这样可以避免频繁的触发微信开发者工具刷新工程。 复制代码
微信开发者工具
用于新建页面,调试,提交微信平台。 复制代码
: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二次封装
二次封装的理由
-
回调方式,不好用,会无限嵌套;
-
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…
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。