JavaScript好用還未火的註解@Decorator(註解 | 裝飾器 | 裝潢器)

目錄: javascript

1、閱讀收穫

  1. What(是什麼)-Why(爲何)-How(怎麼用)-Where(哪裏用)闡述方法論;
  2. AOP編程思想;
  3. JavaScript Decorator的弊端;
  4. 如何定義Decorator
  5. 自定義Decorator如何傳參;
  6. 優雅的異常處理;
  7. 如何判斷一個函數爲異步函數;

2、What:是什麼

1. AOP思想

​ 先了解一下火於後端的一個編程思想:AOP( Aspect Oriented Programming :面向切面編程)。 也叫作面向方法編程,是經過預編譯方式和運行期動態代理的方式實現不修改源代碼的狀況下給程序動態統一添加功能的技術 。詳見:AOP 面向切面編程。歸納文章主要思想:前端

  1. AOP面對業務處理過程當中的某個步驟或階段,下降業務流程步驟間的耦合度;
  2. 與業務無關,被多個業務模塊共同調用的邏輯可定義爲一個Aspect切面
  3. AOPOOP(封裝、繼承,多態)的補充和完善,AOP實現分散關注點;
  4. AOP是典型的代理模式體現;
  5. 應用場景包括日誌記錄、性能統計、安全控制、事務處理、異常處理等。

2. JavaScript的AOP

JavaScript:同爲C系列語言,JavaAOP那麼好用,我也要(磨刀霍霍向豬羊,期待的小眼神)。java

Decorator無疑是對AOP最有力的設計,在ES5時代,能夠經過 Object.defineProperty 來對對象屬性/方法 進行訪問修飾,但用起來須要寫一堆東西;在ES6時代,能夠經過Proxy來對對象屬性 / 方法進行訪問修飾。Decorator已經在ES7的提案中,也就是叫你敬請期待;藉助Babel轉碼工具,咱們能夠先吃螃蟹。webpack

Decorator,能夠不侵入原有代碼內部的狀況下修改類代碼的行爲,處理一些與具體業務無關的公共功能,常見:日誌,異常,埋碼等。ES7 Decorator提案描述以下:ios

A decorator is:git

  1. an expression(一個表達式)
  2. that evaluates to a function(等價於一個函數)
  3. that takes the target, name, and decorator descriptor as arguments(參數有target,name,descriptor )
  4. and optionally returns a decorator descriptor to install on the target object(可選的返回一個裝飾器描述符以安裝在目標對象上)

思想卻是理解了,上面的翻譯可能有出入,由於老感受第4點翻譯的不夠貼切,歡迎斧正。github

3、Why:爲何

1. 爲何要用Decorator

​ 首先拋開「迎合」後端開發人員的Class寫法,對應會引入的相關概念和特性,固然隨着前端業務的發展,有時候也須要對應的特性。好比:private,static等見於Java的特性,現現在經過Class語法糖能在前端實現一樣的特性,換湯不換藥,語法糖底層仍是經過原生JavaScript來模擬實現相關特性的。前端Class編寫風格更加"後端",那麼就更容易吸引一大波後端人員學習和使用JavascriptJavascript一統編程界「指日可待」。後端都學Javascript了,這讓純前端壓力山大,咱們要加快學習的腳步才行,技多不壓身,觸類旁通學習,把後端的空氣都咬完,讓後端沒法呼吸。web

​ 其次舉個栗子闡述爲何要用Decorator:現實生活中咱們可能也遇到過,百度過一個商品後,打開淘寶京東後,淘寶京東便能精準的推薦該商品的相關廣告,大數據時代,咱們慢慢愈來愈透明。轉換爲專業術語:埋碼。express

​ 埋碼具體需求以下:大數據時代,數據就是金錢,業務方須要統計用戶對某些功能的使用狀況,好比使用時間,頻率,用戶習慣等。對應後端會提供一個埋碼接口,用戶調用功能的時候,前端須要 侵入式 的在全部須要統計的功能前調用後端埋碼接口。編程

原始寫法:

// 埋碼 監聽用戶使用狀況
function monitor(name) {
  console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}

class PageAPI {
  onWatch() {
    monitor('侵入式:帥哥靚妹X訪問了xxx')
    console.log('原訪問頁面邏輯')
  }
  onLike() {
    monitor('侵入式:帥哥靚妹X點讚了xxx')
    console.log('原點贊正常邏輯')
  }
  onAttention() {
    monitor('侵入式:帥哥靚妹X關注了xxx')
    console.log('原關注正常邏輯')
  }
  onBack(){
    console.log('退出相關邏輯,不須要監聽')
  }
}
const page = new PageAPI()
// 各類暗示點贊,關注,各位看官你懂的,哈哈
page.onWatch()
page.onLike()
page.onAttention()
page.onBack()
複製代碼

打印結果:

使用Decorator寫法以下

// 埋碼 監聽用戶使用狀況
function monitor(name) {
  // 注意:
  // 實際中埋碼數據是從一個單例store裏面取
  // 好比,用戶名,訪問時間等
  // 操做類型可做爲`Decorator`參數
  console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}

/** Decorator 定義: 1. an expression(一個表達式) 2. that evaluates to a function(等價於一個函數) 3. that takes the target, name, and decorator descriptor as arguments(參數有target,name,descriptor ) 4. and optionally returns a decorator descriptor to install on the target object(可選的返回一個裝飾器描述符以安裝在目標對象上) * @param {*} name */
const monitorDecorator = (name) => { // Decorator 定義2
  return (target, propertyKey, descriptor) => {// Decorator 定義3
    const func = descriptor.value
    return { // Decorator 定義4
      get() {
        return (...args) => {
          monitor(name) // 埋碼
          func.apply(this, args) // 原來邏輯
        }
      },
      set(newValue) {
        return newValue
      }
    }
  }
}

class PageAPI {
  @monitorDecorator('Decorator:帥哥靚妹X訪問了xxx') // Decorator 定義1
  onWatch() {
    console.log('原訪問頁面邏輯')
  }
  @monitorDecorator('Decorator:帥哥靚妹X點讚了xxx')
  onLike() {
    console.log('原點贊正常邏輯')
  }
  @monitorDecorator('Decorator:帥哥靚妹X關注了xxx')
  onAttention() {
    console.log('原關注正常邏輯')
  }
  onBack() {
    console.log('退出相關邏輯,不須要監聽')
  }
}
const page = new PageAPI()
// 各類暗示點贊,關注,各位看官你懂的,哈哈
page.onWatch()
page.onLike()
page.onAttention()
page.onBack()
複製代碼

打印結果:

通過上面的栗子應該能直觀的感覺到面向切面編程核心:非侵入式,解耦。

2. 爲何Decorator還未火

不火的緣由主要爲:

  1. 還在ES7提案中,還未獲得官方支持;
  2. Function寫法支持不友善,不少用戶和框架依然都用Function寫法,好比:Vue 3.0React Hook等都推崇Function寫法,畢竟Javascript從骨子裏就是用Function編程。
  3. Decorator暫時不能串聯,存在覆蓋問題;

4、How:怎麼用

​ 目前標準還未支持Decorator,可是Babel已經支持相關寫法,咱們能夠經過getset來模擬實現。根據Decorator不一樣的切入點能夠分爲:Class,MethodProperty三種Decorator。順帶介紹一下原生Function如何實現面向切面編程。

1. Babel支持

​ 在自我搭建的Webpack項目中使用Decorator,運行項目編譯失敗,終端報錯,並提供了對應的解決方法。按照提示操做,便能在自我搭建的webpack項目使用Decorator了。

​ 另外,親測,在新版Vue-cli項目中已經默認支持Decorator寫法了

2. Class Decorator

​ 切入點爲Class,修飾整個Class,能夠讀取和修改類的方法和屬性。須要傳遞參數,能夠經過高階的函數來實現傳遞參數,以下面的classDecoratorBuilder

// 埋碼 監聽用戶使用狀況
function monitor(name) {
  console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}

const classDecoratorBuilder = (dataMap) => {
  return target => {
    // ! 此處不能用 Object.entries(target.prototype) --> []
    Object
      .getOwnPropertyNames(target.prototype)
      .forEach(key => {
        console.log(target)
        if (!['onBack'].includes(key)) { // 屏蔽某些操做
          const func = target.prototype[key]
          target.prototype[key] = (...args) => {
            monitor(dataMap[key] || '埋碼數據') // 埋碼
            func.apply(this, args) // 原來邏輯
          }
        }
      })
    return target
  }
}
const dataMap = {
  onWatch: 'class Decorator:帥哥靚妹X訪問了xxx',
  onLike: 'class Decorator:帥哥靚妹X點讚了xxx',
  onAttention: 'class Decorator:帥哥靚妹X關注了xxx',
}
const classDecorator = classDecoratorBuilder(dataMap)

@classDecorator
class PageAPI {
  onWatch() {
    console.log('原訪問頁面邏輯')
  }
  onLike() {
    console.log('原點贊正常邏輯')
  }
  onAttention() {
    console.log('原關注正常邏輯')
  }
  onBack() {
    console.log('退出相關邏輯,不須要監聽')
  }
}
const page = new PageAPI()
// 各類暗示點贊,關注,各位看官你懂的,哈哈
page.onWatch()
page.onLike()
page.onAttention()
page.onBack()
複製代碼

運行結果以下:

3. Methods Decorator

​ 切入點爲Method,修飾方法,和Class Decorator功能類似,能額外的獲取修飾的方法名。詳見 Why 中的栗子。這裏就不贅述了。

4. Property Decorator

​ 切入點爲屬性,修飾屬性,和Class註解功能功能相同,能額外的獲取修飾的屬性名。

const propertyDecorator = (target, propertyKey) => {
  Object.defineProperty(target, propertyKey, {
    get() {
      return 'property-decorator-value'
    },
    set(val) {
      return val
    }
  })
}
class Person {
  @propertyDecorator
  private name = 'default name'
  sayName(){
    console.log(`class Person name = ${this.name}`)
  }
}
new Person().sayName()
複製代碼

運行結果以下:

5. Decorator優先級,串聯

​ Java的Decorator功能強大,不只有豐富的Decorator,並且Decorator還能夠串聯。壞消息:親測JavaScript Decorator不能串聯,存在覆蓋問題,也就是優先級關係:Method Decorator > Class Decorator。當一個Method上定義了Decorator,則Class Decorator則不起做用。但願ES7標準能解決這個痛點。

const classDecoratorBuilder = (name) => {
  return target => {
    Object
      .getOwnPropertyNames(target.prototype)
      .forEach(key => {
        const func = target.prototype[key]
        target.prototype[key] = (...args) => {
          console.log(`>>>>> class-decorator ${name}`)
          func.apply(this, args)
        }
      })
    return target
  }
}
const methodDecoratorBuilder = (name) => {
  return (target, propertyKey, descriptor) => {
    const func = descriptor.value
    return {
      get() {
        return (...args) => {
          console.log(`>>>>> method-decorator ${name}`)
          func.apply(this, args) 
        }
      },
      set(newValue) {
        return newValue
      }
    }
  }
}
const classDecorator1 = classDecoratorBuilder(1)
const classDecorator2 = classDecoratorBuilder(2)
const methodDecorator1 = methodDecoratorBuilder(1)
const methodDecorator2 = methodDecoratorBuilder(2)
const propertyDecorator = (target, propertyKey) => {
  Object.defineProperty(target, propertyKey, {
    get() {
      return 'property-decorator-value'
    },
    set(val) {
      return val
    }
  })
}

// Decorator不能串聯
// @classDecorator1
@classDecorator2
class Person {
  @propertyDecorator
  private name = 'default name'

  // @methodDecorator1 // 不能串聯,會報錯
  @methodDecorator2 // class Decorator會被覆蓋
  sayName() {
    console.log('sayName : ', this.name)
  }

  eat(food) {
    console.log('eat : ', food)
  }
}
const person = new Person()
person.sayName()
person.eat('rice')
複製代碼

​ 運行結果以下:

6. Function的「Decorator

Decorator目前只能應用於Class,不能用於修飾Function,由於Function的執行上下文是不肯定的,太靈活了。可是AOP編程思想是先進的,合理的。咱們能夠採用不一樣的形式來實現FunctionAOP,雖然沒Decorator那麼優雅。經過這種方式還能夠解決Decorator串聯的痛點。

function monitor(name) {
  console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}
const functionAOP = (name, fn) => {
  return (...args) => {
    monitor(name)
    fn.apply(this, args)
  }
}

let onWatch = (pageName) => {
  console.log('原訪問頁面邏輯,訪問頁面:', pageName)
}
let onLike = (pageName) => {
  console.log('原點贊正常邏輯,求點贊:', pageName)
}
let onAttention = (author) => {
  console.log('原關注正常邏輯,求關注:', author)
}
// 相似`Decorator`
onWatch = functionAOP(
  '****我串聯啦****',
  functionAOP('functionAOP:帥哥靚妹X訪問了xxx', onWatch)
)
onLike = functionAOP('functionAOP:帥哥靚妹X點讚了xxx', onLike)
onAttention = functionAOP('functionAOP:帥哥靚妹X關注了xxx', onAttention)

onWatch('JavaScript好用還未火的`Decorator`@Decorator')
onLike('JavaScript好用還未火的`Decorator`@Decorator')
onAttention('JS強迫症患者')
複製代碼

​ 運行結果以下:

5、Where:哪裏用

​ AOP在前端的應用場景包括日誌記錄、統計、安全控制、事務處理、異常處理、埋碼等與業務關聯性不強的功能。上面栗子已經詳細介紹了AOP在埋碼上的應用,下面再詳細介紹一個經常使用場景:異常處理。

1. 異常處理背景

​ 一個好的應用,用戶體驗要良好,當用戶使用核心功能,不管功能是否成功,都但願獲得一個信息反饋,而不是感受不到功能是否有運行,是否成功。核心功能運行成功的時候彈出消息:xxx功能成功;失敗的時候彈出錯誤:xxx功能失敗,請xxx之類。

​ 廢話很少說,直接擼代碼。因爲是模擬代碼,一是爲了節省時間,二是爲了各位看官能夠一覽無遺,博主就不拆解文件了。合理的結構應該將APIDecorator,頁面邏輯拆解到對應文件中,以供複用。

​ 生成模擬接口的公共代碼:

const promiseAPIBuilder = (code) => { // 模擬生成各類接口
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (code === 0) {
        resolve({
          code,
          message: 'success',
          data: []
        })
      } else if (code === 404) {
        reject({
          code,
          message: '接口不存在'
        })
      } else {
        reject({
          code,
          message: '服務端異常'
        })
      }
    }, 1000)
  })
}
複製代碼

2. 實現方式1:多catch

​ 咱們能夠修改axios攔截器,當狀態code非0的時候一概認爲功能失敗,統一reject錯誤信息,最後在API調用處catch內統一作錯誤信息彈出。相應弊端:多處接口調用處都須要增長與業務無關的catch方法或者用try catch處理。

const api = {
  successAPI() {
    return promiseAPIBuilder(0)
  },
  error404API() {
    return promiseAPIBuilder(404)
  },
  errorWithoutCatchAPI() { // 沒有catch error
    return promiseAPIBuilder(500)
  }
}

const successAPI = async () => {
  const res = await api
    .successAPI()
    .catch(error => console.log(`多個catch的error : ${error.message}`))
  if (!res) return
  console.log('接口調用成功後的邏輯1')
}
successAPI()

const error404API = async () => {
  const res = await api
    .error404API()
    .catch(error => console.log(`消息提示:多個catch的error : ${error.message}`))
  if (!res) return
  console.log('接口調用成功後的邏輯2')
}
error404API()

const errorWithoutCatchAPI = async () => {
  const res = await api.errorWithoutCatchAPI() // error 沒有 catch
  if (!res) return
  console.log('接口調用成功後的邏輯3')
}
errorWithoutCatchAPI()
複製代碼

運行結果:

3. 實現方式2:全局catch

​ 定義全局異常處理函數。相應弊端:狀況多的話須要作不少case判斷,由於引用不少沒攔截的異常都會跑到全局異常處理函數。

const api = {
  successAPI() {
    return promiseAPIBuilder(0)
  },
  error404API() {
    return promiseAPIBuilder(404)
  },
  errorWithoutCatchAPI() { // 沒有catch error
    return promiseAPIBuilder(500)
  }
}

// 統一處理
window.addEventListener('unhandledrejection', (event) => {
  if (event.reason.code === 404) {
    console.log(` 消息提示:統一catch的error, 須要經過if或者switch判斷處理流程 : ${event.reason.message} `)
  }
})

const successAPI = async () => {
  const res = await api.successAPI() // error 沒有 catch
  if (!res) return
  console.log('接口調用成功後的邏輯1')
}
successAPI()

const error404API = async () => {
  const res = await api.error404API() // error 沒有 catch
  if (!res) return
  console.log('接口調用成功後的邏輯2')
}
error404API()

const errorWithoutCatchAPI = async () => {
  const res = await api.errorWithoutCatchAPI() // error 沒有 catch
  if (!res) return
  console.log('接口調用成功後的邏輯3')
}
errorWithoutCatchAPI()
複製代碼

運行結果:

3. 優雅的實現方式3:Decorator

Decorator修飾API接口管理文件。雖然說也有Class寫法的限制,可是咱們能夠經過其餘方式避開這個限制。注意帶*號的代碼

// ****** catch error Decorator 構造器
const showTipDecoratorBulder = (errorHandler) => (target, propertyKey, descriptor) => {
  const func = descriptor.value
  return {
    get() {
      return (...args) => {
        return Promise
          .resolve(func.apply(this, args))
          .catch(error => {
            errorHandler && errorHandler(error)
          })
      }
    },
    set(newValue) {
      return newValue
    }
  }
}
// ****** 構造一個提示錯誤的`Decorator`
const showTipDecorator = showTipDecoratorBulder((error) => {
  console.log(`Decorator error 消息提示 : ${error.message}`)
})

// ****** class 寫法避開限制
class PageAPI {
  @showTipDecorator
  successAPI() {
    return promiseAPIBuilder(0)
  }
  @showTipDecorator
  error404API() {
    return promiseAPIBuilder(404)
  }
  errorWithoutCatchAPI() {
    return promiseAPIBuilder(500)
  }
}
const api = new PageAPI()

const successAPI = async () => {
  const res = await api.successAPI() // error 沒有 catch
  if (!res) return
  console.log('接口調用成功後的邏輯1')
}
successAPI()

const error404API = async () => {
  const res = await api.error404API() // error 沒有 catch
  if (!res) return
  console.log('接口調用成功後的邏輯2')
}
error404API()

const errorWithoutCatchAPI = async () => {
  const res = await api.errorWithoutCatchAPI() // error 沒有 catch
  if (!res) return
  console.log('接口調用成功後的邏輯3')
}
errorWithoutCatchAPI()
複製代碼

運行結果:

附送:如何判斷一個函數爲AsyncFucntion

/** * 附送:如何判斷一個函數爲AsyncFucntion */
const asyncFn = async _ => _
const fn = _ => _
// AsyncFucntion非JS內置對象,不能直接經過以下方式判斷
// console.log('<<<< asyncFn instanceof AsyncFucntion <<<', asyncFn instanceof AsyncFucntion)
console.log('<<<< asyncFn instanceof Function <<<', asyncFn instanceof Function) // true
console.log('<<<< fn instanceof Function <<<', fn instanceof Function) // true

const AsyncFucntion = Object.getPrototypeOf(async _ => _).constructor
console.log('<<<< asyncFn instanceof AsyncFucntion <<<', asyncFn instanceof AsyncFucntion) // true
console.log('<<<< fn instanceof AsyncFucntion <<<', fn instanceof AsyncFucntion) // false
複製代碼

運行結果:

​ 都看到這裏了,點個贊,關注再走唄。

相關文章
相關標籤/搜索