章節規劃
- 目錄結構與開發約定
- 工具類封裝
- 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…