decorator(修飾器)的業務應用

decrator(修飾器)的業務應用

ES6問世的時間也不短了,並且不少時候對ES6所謂的「熟練應用」基本還停留在下面的幾種api應用:es6

  • const/let
  • 箭頭函數
  • Promise
  • async await
  • 解構、擴展運算符
  • Object.assign
  • class static
  • 數組遍歷api

(固然也多是我用的比較簡單)算法

最近也是看了不少大神寫的代碼,確實學到了不少東西,這也讓我下定決心要更深層次的應用ES6api

本次咱們介紹decrator(修飾器)在業務中的應用數組

decrator 基礎

首先咱們先看下decrator的用法:promise

1.類修飾器(只有一個參數):緩存

target -> 指向類,若是是類型是function,則指向MyFunction.prototypecookie

// 類修飾器
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 -> 方法所在的類app

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"}'

decrator 實際應用

上面說的你們從網絡上各類文章基本都能看到。

應用的話打日誌也算是一種,可是感受應用場景有限,通常對關鍵業務操做纔會用到。常規的業務感受應用並很少。

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

  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的應用,而且是結合實際業務。

後面也是打算對現有項目的公共庫進行算法優化升級。若是有機會再進行分享。

相關文章
相關標籤/搜索