ES6問世的時間也不短了,而且很多時候對ES6所謂的“熟練應用”基本還停留在下面的幾種api應用:
- const/let
- 箭頭函式
- Promise
- async await
- 解構、擴充套件運算子
- Object.assign
- class static
- 陣列遍歷api
(當然也可能是我用的比較簡單)
最近也是看了很多大神寫的程式碼,確實學到了很多東西,這也讓我下定決心要更深層次的應用ES6
本次我們介紹decorator(修飾器)在業務中的應用
decorator 基礎
首先我們先看下decorator的用法:
1.類修飾器(只有一個引數):
target -> 指向類,如果是型別是function,則指向MyFunction.prototype
// 類修飾器
const animalDecorator = (target) => {
target.isAnimal = true
target.prototype.nickname = 'nimo'
};
@animalDecorator
class Cat {
...
}
console.log(Cat.isAnimal); // true
console.log((new Cat()).nickname); // 'nimo'
複製程式碼
2.方法修飾器(有三個引數)
target -> 方法所在的類
key -> 方法名稱
descriptor -> 描述物件
// 方法修飾器
const log = (target, key, descriptor) => {
const oriFunc = descriptor.value
descriptor.value = (...args) => {
console.log(`${key}:`, args)
oriFunc.apply(this, args)
}
return descriptor
};
class Util {
@log
static setParam (param) {
...
}
}
Util.setParam({name: 'xxx'}) // 'setParam: {name: "xxx"}'
複製程式碼
上面的用法沒有傳引數,如果需要傳引數的話,內部需要return一個方法,以方法修飾器為例
// 方法修飾器
const log = (name) => {
return (target, key, descriptor) => {
const oriFunc = descriptor.value
descriptor.value = (...args) => {
console.log(`${key} ${name}:`, args)
oriFunc.apply(this, args)
}
return descriptor
}
};
class Util {
@log('forTest')
static setParam (param) {
...
}
}
Util.setParam({name: 'xxx'}) // 'setParam forTest: {name: "xxx"}'
複製程式碼
decorator 實際應用
上面說的大家從網路上各種文章基本都能看到。
應用的話打日誌也算是一種,但是感覺應用場景有限,一般對關鍵業務操作才會用到。常規的業務感覺應用並不多。
下面介紹幾個常見的場景:
- 某個場景下需要同時請求多個介面,但這些介面都需要做登入驗證
- 傳送行為埋點,傳送前需要獲取token(如果cookie中有就從本地獲取,否則從介面獲取。注:這個token和登入沒關係,是用來計算pv和uv的唯一標識)
我們以傳送行為統計前需要獲取token為例:
場景: 頁面載入完成後,需要同時傳送多個行為埋點統計(如:pv、某些模組曝光點)
特點: 每次傳送埋點都要檢查token是否存在,在本地cookie中沒有token的時候,就會從介面獲取,並種到本地。
看著邏輯好像沒問題。
實際: 這些行為埋點方法呼叫的時機,基本上是同時發生。如果cookie中沒用token,這幾次api呼叫都會觸發獲取token介面的呼叫,這就導致多次不必要的請求。
目標: 我們希望,就請求一次介面就可以了。
那麼,我們就需要處理髮送埋點的方法,一般有兩種方式:
- 傳統方式:修改統計方法,建立callback快取陣列,只有第一次呼叫介面,修改標誌位,把後面呼叫的callback通通快取在陣列裡,等請求結束,在統一呼叫陣列裡的callbakc
- 通過修飾器處理(但實現原理也是如此)
統計方法:
...
/**
* 上報埋點
* @param {string} actiontype
* @param {string, optional} pagetype
* @param {Object, optional} backup
*/
static report (actiontype, pagetype, backup = {}) {
try {
// 處理actiontype欄位
if (!actiontype) return
actiontype = actiontype.toUpperCase() // 轉為大寫
// 處理pagetype欄位
if (!pagetype) {
// 獲取當前頁面的頁面名稱
pagetype = Util.getPageName()
}
pagetype = pagetype.toUpperCase()
// 處理backup欄位
if (backup && typeof backup !== 'object') {
console.error('[埋點失敗] backup欄位應為物件型別, actionType:', actiontype, 'pageType:', pagetype, 'backup:', backup)
return
}
let commonParams = LeStatic._options.commonBackup.call(this)
for (let param in backup) {
if (param in commonParams) {
console.warn(`[埋點衝突] 引數名稱: ${param} 與統一埋點引數名稱衝突,請注意檢查`, `actionType:`, actiontype, 'pageType:', pagetype, 'backup:', backup)
}
}
backup = Object.assign(commonParams, backup)
backup = JSON.stringify(backup)
// 保證token的存在
ZZLogin.ensuringExistingToken().then(() => {
// 獲取cookieid欄位
let cookieid = Cookies.get('tk')
// 傳送埋點請求
wx.request({
url: LeStatic._options.LOG_URL,
data: {
cookieid,
actiontype,
pagetype,
appid: 'ZHUANZHUAN',
_t: Date.now(),
backup
},
success: (res) => {
if (res.data === false) {
console.warn('[埋點上報失敗] 介面返回false, actionType:', actiontype, 'pageType:', pagetype)
}
},
fail: (res) => {
console.warn('[埋點上報失敗] 網路異常, res:', res)
}
})
})
} catch (e) {
console.warn('[埋點上報失敗] 捕獲程式碼異常:', e)
}
}
複製程式碼
這塊看著好像沒做快取處理,彆著急
關鍵點在:ZZLogin.ensuringExistingToken()的呼叫,我們來看下ZZLogin中的ensuringExistingToken方法
lib/ZZLogin.js
import { mergeStep } from '@/lib/decorators'
class ZZLogin {
...
/**
* token機制,請求發起前,先確保本地有token,如果沒有,呼叫介面生成一個臨時token,登入後
* @return {Promise}
*/
@mergeStep
static ensuringExistingToken () {
return new Promise((resolve, reject) => {
const tk = cookie.get('tk') || ''
// token已存在
if (/^wt-/.test(tk)) {
resolve()
return
}
// 獲取使用者token
ZZLogin.getToken().then(res => {
resolve()
})
})
}
}
複製程式碼
我們在呼叫ensuringExistingToken 時加了修飾器,目的就是,即使同時刻多次呼叫,非同步請求也是被合併成了一次,其他次的呼叫也是在第一次非同步請求完成後,再進行統一呼叫。
來看看修飾器是怎麼寫的(mergeStep)
lib/decorators.js
...
// 快取物件
const mergeCache = {}
export function mergeStep (target, funcName, descriptor) {
const oriFunc = descriptor.value
descriptor.value = (...args) => {
// 如果第一次呼叫
if (!mergeCache[funcName]) {
mergeCache[funcName] = {
state: 'doing', // 表示處理中
fnList: []
}
return new Promise((resolve, reject) => {
// 進行第一次非同步處理
oriFunc.apply(null, args).then(rst => {
// 處理完成後,將狀態置為done
mergeCache[funcName].state = 'done'
resolve(rst)
// 將快取中的回撥逐一觸發
mergeCache[funcName].fnList.forEach(fnItem => {
fnItem()
})
// 觸發後將陣列置空
mergeCache[funcName].fnList.length = 0
})
})
// 同時刻多次呼叫
} else {
// 後面重複的呼叫的回撥直接快取到陣列
if (mergeCache[funcName].state === 'doing') {
return new Promise((resolve, reject) => {
mergeCache[funcName].fnList.push(() => {
resolve(oriFunc.apply(null, args))
})
})
// 如果之前非同步狀態已經完成,則直接呼叫
} else {
return oriFunc.apply(null, args)
}
}
}
return descriptor
}
複製程式碼
原理:
- 如果是第一次呼叫:建立快取,建立promise物件,直接進行非同步請求,並將狀態改為doing
- 後面重複呼叫時,發現是doing狀態,就將每個呼叫包裝成一個promise,將callback,放到快取陣列中
- 第一次非同步請求完成後,將狀態改為done,並將快取陣列中的callback統一呼叫
- 後面再重複呼叫,發現狀態已經是done了,就直接觸發回撥
其實修飾器大家知道麼,基本上都瞭解,可業務裡就是從來不用。包括es6中其它api也一樣,會用了才是自己的。
最近也是全組一起重新深入學習es6的應用,並且是結合實際業務。
後面也是打算對現有專案的公共庫進行演算法優化升級。如果有機會再進行分享。