小程式開發筆記
- 目錄結構與開發約定
- 工具類封裝
- 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
工作流管理程式碼版本。
ofollow,noindex">深入理解學習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物件: // progressnumber下載進度百分比 // totalBytesWrittennumber已經下載的資料長度,單位 Bytes // totalBytesExpectedToWritenumber預期需要下載的資料總長度,單位 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…