decorator(修飾器)的業務應用

轉轉_陳龍發表於2018-12-24

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 實際應用

上面說的大家從網路上各種文章基本都能看到。

應用的話打日誌也算是一種,但是感覺應用場景有限,一般對關鍵業務操作才會用到。常規的業務感覺應用並不多。

下面介紹幾個常見的場景:

  1. 某個場景下需要同時請求多個介面,但這些介面都需要做登入驗證
  2. 傳送行為埋點,傳送前需要獲取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的應用,並且是結合實際業務。

後面也是打算對現有專案的公共庫進行演算法優化升級。如果有機會再進行分享。

相關文章