成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Javascript裝飾器的妙用

phodal / 2325人閱讀

摘要:最近新開(kāi)了一個(gè)項(xiàng)目,采用來(lái)開(kāi)發(fā),在數(shù)據(jù)庫(kù)及路由管理方面用了不少的裝飾器,發(fā)覺(jué)這的確是一個(gè)好東西。在中的使用該裝飾器會(huì)在定義前調(diào)用,如果函數(shù)有返回值,則會(huì)認(rèn)為是一個(gè)新的構(gòu)造函數(shù)來(lái)替代之前的構(gòu)造函數(shù)。函數(shù)參數(shù)裝飾器最后,還有一個(gè)用于函數(shù)參

最近新開(kāi)了一個(gè)Node項(xiàng)目,采用TypeScript來(lái)開(kāi)發(fā),在數(shù)據(jù)庫(kù)及路由管理方面用了不少的裝飾器,發(fā)覺(jué)這的確是一個(gè)好東西。
裝飾器是一個(gè)還處于草案中的特性,目前木有直接支持該語(yǔ)法的環(huán)境,但是可以通過(guò) babel 之類的進(jìn)行轉(zhuǎn)換為舊語(yǔ)法來(lái)實(shí)現(xiàn)效果,所以在TypeScript中,可以放心的使用@Decorator。

什么是裝飾器

裝飾器是對(duì)類、函數(shù)、屬性之類的一種裝飾,可以針對(duì)其添加一些額外的行為。
通俗的理解可以認(rèn)為就是在原有代碼外層包裝了一層處理邏輯。
個(gè)人認(rèn)為裝飾器是一種解決方案,而并非是狹義的@Decorator,后者僅僅是一個(gè)語(yǔ)法糖罷了。

裝飾器在身邊的例子隨處可見(jiàn),一個(gè)簡(jiǎn)單的例子,水龍頭上邊的起泡器就是一個(gè)裝飾器,在裝上以后就會(huì)把空氣混入水流中,摻雜很多泡泡在水里。
但是起泡器安裝與否對(duì)水龍頭本身并沒(méi)有什么影響,即使拆掉起泡器,也會(huì)照樣工作,水龍頭的作用在于閥門的控制,至于水中摻不摻雜氣泡則不是水龍頭需要關(guān)心的。

所以,對(duì)于裝飾器,可以簡(jiǎn)單地理解為是非侵入式的行為修改。

為什么要用裝飾器

可能有些時(shí)候,我們會(huì)對(duì)傳入?yún)?shù)的類型判斷、對(duì)返回值的排序、過(guò)濾,對(duì)函數(shù)添加節(jié)流、防抖或其他的功能性代碼,基于多個(gè)類的繼承,各種各樣的與函數(shù)邏輯本身無(wú)關(guān)的、重復(fù)性的代碼。

函數(shù)中的作用

可以想像一下,我們有一個(gè)工具類,提供了一個(gè)獲取數(shù)據(jù)的函數(shù):

class Model1 {
  getData() {
    // 此處省略獲取數(shù)據(jù)的邏輯
    return [{
      id: 1,
      name: "Niko"
    }, {
      id: 2,
      name: "Bellic"
    }]
  }
}

console.log(new Model1().getData())     // [ { id: 1, name: "Niko"}, { id: 2, name: "Bellic" } ]
console.log(Model1.prototype.getData()) // [ { id: 1, name: "Niko"}, { id: 2, name: "Bellic" } ]

現(xiàn)在我們想要添加一個(gè)功能,記錄該函數(shù)執(zhí)行的耗時(shí)。
因?yàn)檫@個(gè)函數(shù)被很多人使用,在調(diào)用方添加耗時(shí)統(tǒng)計(jì)邏輯是不可取的,所以我們要在Model1中進(jìn)行修改:

class Model1 {
  getData() {
+   let start = new Date().valueOf()
+   try {
      // 此處省略獲取數(shù)據(jù)的邏輯
      return [{
        id: 1,
        name: "Niko"
      }, {
        id: 2,
        name: "Bellic"
      }]
+   } finally {
+     let end = new Date().valueOf()
+     console.log(`start: ${start} end: ${end} consume: ${end - start}`)
+   }
  }
}

// start: XXX end: XXX consume: XXX
console.log(new Model1().getData())     // [ { id: 1, name: "Niko"}, { id: 2, name: "Bellic" } ]
// start: XXX end: XXX consume: XXX
console.log(Model1.prototype.getData()) // [ { id: 1, name: "Niko"}, { id: 2, name: "Bellic" } ]

這樣在調(diào)用方法后我們就可以在控制臺(tái)看到耗時(shí)的輸出了。
但是這樣直接修改原函數(shù)代碼有以下幾個(gè)問(wèn)題:

統(tǒng)計(jì)耗時(shí)的相關(guān)代碼與函數(shù)本身邏輯并無(wú)一點(diǎn)關(guān)系,影響到了對(duì)原函數(shù)本身的理解,對(duì)函數(shù)結(jié)構(gòu)造成了破壞性的修改

如果后期還有更多類似的函數(shù)需要添加統(tǒng)計(jì)耗時(shí)的代碼,在每個(gè)函數(shù)中都添加這樣的代碼顯然是低效的,維護(hù)成本太高

所以,為了讓統(tǒng)計(jì)耗時(shí)的邏輯變得更加靈活,我們將創(chuàng)建一個(gè)新的工具函數(shù),用來(lái)包裝需要設(shè)置統(tǒng)計(jì)耗時(shí)的函數(shù)。
通過(guò)將Class與目標(biāo)函數(shù)的name傳遞到函數(shù)中,實(shí)現(xiàn)了通用的耗時(shí)統(tǒng)計(jì):

function wrap(Model, key) {
  // 獲取Class對(duì)應(yīng)的原型
  let target = Model.prototype

  // 獲取函數(shù)對(duì)應(yīng)的描述符
  let descriptor = Object.getOwnPropertyDescriptor(target, key)

  // 生成新的函數(shù),添加耗時(shí)統(tǒng)計(jì)邏輯
  let log = function (...arg) {
    let start = new Date().valueOf()
    try {
      return descriptor.value.apply(this, arg) // 調(diào)用之前的函數(shù)
    } finally {
      let end = new Date().valueOf()
      console.log(`start: ${start} end: ${end} consume: ${end - start}`)
    }
  }

  // 將修改后的函數(shù)重新定義到原型鏈上
  Object.defineProperty(target, key, {
    ...descriptor,
    value: log      // 覆蓋描述符重的value
  })
}

wrap(Model1, "getData")
wrap(Model2, "getData")

// start: XXX end: XXX consume: XXX
console.log(new Model1().getData())     // [ { id: 1, name: "Niko"}, { id: 2, name: "Bellic" } ]
// start: XXX end: XXX consume: XXX
console.log(Model2.prototype.getData()) // [ { id: 1, name: "Niko"}, { id: 2, name: "Bellic" } ]

接下來(lái),我們想控制其中一個(gè)Model的函數(shù)不可被其他人修改覆蓋,所以要添加一些新的邏輯:

function wrap(Model, key) {
  // 獲取Class對(duì)應(yīng)的原型
  let target = Model.prototype

  // 獲取函數(shù)對(duì)應(yīng)的描述符
  let descriptor = Object.getOwnPropertyDescriptor(target, key)

  Object.defineProperty(target, key, {
    ...descriptor,
    writable: false      // 設(shè)置屬性不可被修改
  })
}

wrap(Model1, "getData")

Model1.prototype.getData = 1 // 無(wú)效

可以看出,兩個(gè)wrap函數(shù)中有不少重復(fù)的地方,而修改程序行為的邏輯,實(shí)際上依賴的是Object.defineProperty中傳遞的三個(gè)參數(shù)。
所以,我們針對(duì)wrap在進(jìn)行一次修改,將其變?yōu)橐粋€(gè)通用類的轉(zhuǎn)換:

function wrap(decorator) {
  return function (Model, key) {
    let target = Model.prototype
    let dscriptor = Object.getOwnPropertyDescriptor(target, key)

    decorator(target, key, descriptor)
  }
}

let log = function (target, key, descriptor) {
  // 將修改后的函數(shù)重新定義到原型鏈上
  Object.defineProperty(target, key, {
    ...descriptor,
    value: function (...arg) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, arg) // 調(diào)用之前的函數(shù)
      } finally {
        let end = new Date().valueOf()
        console.log(`start: ${start} end: ${end} consume: ${end - start}`)
      }
    }
  })
}

let seal = function (target, key, descriptor) {
  Object.defineProperty(target, key, {
    ...descriptor,
    writable: false
  })
}

// 參數(shù)的轉(zhuǎn)換處理
log = wrap(log)
seal = warp(seal)

// 添加耗時(shí)統(tǒng)計(jì)
log(Model1, "getData")
log(Model2, "getData")

// 設(shè)置屬性不可被修改
seal(Model1, "getData")

到了這一步以后,我們就可以稱logseal為裝飾器了,可以很方便的讓我們對(duì)一些函數(shù)添加行為。
而拆分出來(lái)的這些功能可以用于未來(lái)可能會(huì)有需要的地方,而不用重新開(kāi)發(fā)一遍相同的邏輯。

Class 中的作用

就像上邊提到了,現(xiàn)階段在JS中繼承多個(gè)Class是一件頭疼的事情,沒(méi)有直接的語(yǔ)法能夠繼承多個(gè) Class。

class A { say () { return 1 } }
class B { hi () { return 2 } }
class C extends A, B {}        // Error
class C extends A extends B {} // Error

// 這樣才是可以的
class C {}
for (let key of Object.getOwnPropertyNames(A.prototype)) {
  if (key === "constructor") continue
  Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(A.prototype, key))
}
for (let key of Object.getOwnPropertyNames(B.prototype)) {
  if (key === "constructor") continue
  Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(B.prototype, key))
}

let c = new C()
console.log(c.say(), c.hi()) // 1, 2

所以,在React中就有了一個(gè)mixin的概念,用來(lái)將多個(gè)Class的功能復(fù)制到一個(gè)新的Class上。
大致思路就是上邊列出來(lái)的,但是這個(gè)mixinReact中內(nèi)置的一個(gè)操作,我們可以將其轉(zhuǎn)換為更接近裝飾器的實(shí)現(xiàn)。
在不修改原Class的情況下,將其他Class的屬性復(fù)制過(guò)來(lái):

function mixin(constructor) {
  return function (...args) {
    for (let arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === "constructor") continue // 跳過(guò)構(gòu)造函數(shù)
        Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
      }
    }
  }
}

mixin(C)(A, B)

let c = new C()
console.log(c.say(), c.hi()) // 1, 2

以上,就是裝飾器在函數(shù)、Class上的實(shí)現(xiàn)方法(至少目前是的),但是草案中還有一顆特別甜的語(yǔ)法糖,也就是@Decorator了。
能夠幫你省去很多繁瑣的步驟來(lái)用上裝飾器。

@Decorator的使用方法

草案中的裝飾器、或者可以說(shuō)是TS實(shí)現(xiàn)的裝飾器,將上邊的兩種進(jìn)一步地封裝,將其拆分成為更細(xì)的裝飾器應(yīng)用,目前支持以下幾處使用:

Class

函數(shù)

get set訪問(wèn)器

實(shí)例屬性、靜態(tài)函數(shù)及屬性

函數(shù)參數(shù)

@Decorator的語(yǔ)法規(guī)定比較簡(jiǎn)單,就是通過(guò)@符號(hào)后邊跟一個(gè)裝飾器函數(shù)的引用:

@tag
class A { 
  @method
  hi () {}
}

function tag(constructor) {
  console.log(constructor === A) // true
}

function method(target) {
  console.log(target.constructor === A, target === A.prototype) // true, true
}

函數(shù)tagmethod會(huì)在class A定義的時(shí)候執(zhí)行。

@Decorator 在 Class 中的使用

該裝飾器會(huì)在class定義前調(diào)用,如果函數(shù)有返回值,則會(huì)認(rèn)為是一個(gè)新的構(gòu)造函數(shù)來(lái)替代之前的構(gòu)造函數(shù)。

函數(shù)接收一個(gè)參數(shù):

constructor 之前的構(gòu)造函數(shù)

我們可以針對(duì)原有的構(gòu)造函數(shù)進(jìn)行一些改造:

新增一些屬性

如果想要新增一些屬性之類的,有兩種方案可以選擇:

創(chuàng)建一個(gè)新的class繼承自原有class,并添加屬性

針對(duì)當(dāng)前class進(jìn)行修改

后者的適用范圍更窄一些,更接近mixin的處理方式。

@name
class Person {
  sayHi() {
    console.log(`My name is: ${this.name}`)
  }
}

// 創(chuàng)建一個(gè)繼承自Person的匿名類
// 直接返回并替換原有的構(gòu)造函數(shù)
function name(constructor) {
  return class extends constructor {
    name = "Niko"
  }
}

new Person().sayHi()
修改原有屬性的描述符
@seal
class Person {
  sayHi() {}
}

function seal(constructor) {
  let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, "sayHi")
  Object.defineProperty(constructor.prototype, "sayHi", {
    ...descriptor,
    writable: false
  })
}

Person.prototype.sayHi = 1 // 無(wú)效
使用閉包來(lái)增強(qiáng)裝飾器的功能
在TS文檔中被稱為裝飾器工廠

因?yàn)?b>@符號(hào)后邊跟的是一個(gè)函數(shù)的引用,所以對(duì)于mixin的實(shí)現(xiàn),我們可以很輕易的使用閉包來(lái)實(shí)現(xiàn):

class A { say() { return 1 } }
class B { hi() { return 2 } }

@mixin(A, B)
class C { }

function mixin(...args) {
  // 調(diào)用函數(shù)返回裝飾器實(shí)際應(yīng)用的函數(shù)
  return function(constructor) {
    for (let arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === "constructor") continue // 跳過(guò)構(gòu)造函數(shù)
        Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
      }
    }
  }
}

let c = new C()
console.log(c.say(), c.hi()) // 1, 2
多個(gè)裝飾器的應(yīng)用

裝飾器是可以同時(shí)應(yīng)用多個(gè)的(不然也就失去了最初的意義)。
用法如下:

@decorator1
@decorator2
class { }

執(zhí)行的順序?yàn)?b>decorator2 -> decorator1,離class定義最近的先執(zhí)行。
可以想像成函數(shù)嵌套的形式:

decorator1(decorator2(class {}))
@Decorator 在 Class 成員中的使用

類成員上的 @Decorator 應(yīng)該是應(yīng)用最為廣泛的一處了,函數(shù),屬性,getset訪問(wèn)器,這幾處都可以認(rèn)為是類成員。
在TS文檔中被分為了Method DecoratorAccessor DecoratorProperty Decorator,實(shí)際上如出一轍。

關(guān)于這類裝飾器,會(huì)接收如下三個(gè)參數(shù):

如果裝飾器掛載于靜態(tài)成員上,則會(huì)返回構(gòu)造函數(shù),如果掛載于實(shí)例成員上則會(huì)返回類的原型

裝飾器掛載的成員名稱

成員的描述符,也就是Object.getOwnPropertyDescriptor的返回值

Property Decorator不會(huì)返回第三個(gè)參數(shù),但是可以自己手動(dòng)獲取  
前提是靜態(tài)成員,而非實(shí)例成員,因?yàn)檠b飾器都是運(yùn)行在類創(chuàng)建時(shí),而實(shí)例成員是在實(shí)例化一個(gè)類的時(shí)候才會(huì)執(zhí)行的,所以沒(méi)有辦法獲取對(duì)應(yīng)的descriptor
靜態(tài)成員與實(shí)例成員在返回值上的區(qū)別

可以稍微明確一下,靜態(tài)成員與實(shí)例成員的區(qū)別:

class Model {
  // 實(shí)例成員
  method1 () {}
  method2 = () => {}

  // 靜態(tài)成員
  static method3 () {}
  static method4 = () => {}
}

method1method2是實(shí)例成員,method1存在于prototype之上,而method2只在實(shí)例化對(duì)象以后才有。
作為靜態(tài)成員的method3method4,兩者的區(qū)別在于是否可枚舉描述符的設(shè)置,所以可以簡(jiǎn)單地認(rèn)為,上述代碼轉(zhuǎn)換為ES5版本后是這樣子的:

function Model () {
  // 成員僅在實(shí)例化時(shí)賦值
  this.method2 = function () {}
}

// 成員被定義在原型鏈上
Object.defineProperty(Model.prototype, "method1", {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // 設(shè)置不可被枚舉
  configurable: true
})

// 成員被定義在構(gòu)造函數(shù)上,且是默認(rèn)的可被枚舉
Model.method4 = function () {}

// 成員被定義在構(gòu)造函數(shù)上
Object.defineProperty(Model, "method3", {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // 設(shè)置不可被枚舉
  configurable: true
})

可以看出,只有method2是在實(shí)例化時(shí)才賦值的,一個(gè)不存在的屬性是不會(huì)有descriptor的,所以這就是為什么TS在針對(duì)Property Decorator不傳遞第三個(gè)參數(shù)的原因,至于為什么靜態(tài)成員也沒(méi)有傳遞descriptor,目前沒(méi)有找到合理的解釋,但是如果明確的要使用,是可以手動(dòng)獲取的。

就像上述的示例,我們針對(duì)四個(gè)成員都添加了裝飾器以后,method1method2第一個(gè)參數(shù)就是Model.prototype,而method3method4的第一個(gè)參數(shù)就是Model。

class Model {
  // 實(shí)例成員
  @instance
  method1 () {}
  @instance
  method2 = () => {}

  // 靜態(tài)成員
  @static
  static method3 () {}
  @static
  static method4 = () => {}
}

function instance(target) {
  console.log(target.constructor === Model)
}

function static(target) {
  console.log(target === Model)
}
函數(shù),訪問(wèn)器,和屬性裝飾器三者之間的區(qū)別 函數(shù)

首先是函數(shù),函數(shù)裝飾器的返回值會(huì)默認(rèn)作為屬性的value描述符存在,如果返回值為undefined則會(huì)忽略,使用之前的descriptor引用作為函數(shù)的描述符。
所以針對(duì)我們最開(kāi)始的統(tǒng)計(jì)耗時(shí)的邏輯可以這么來(lái)做:

class Model {
  @log1
  getData1() {}
  @log2
  getData2() {}
}

// 方案一,返回新的value描述符
function log1(tag, name, descriptor) {
  return {
    ...descriptor,
    value(...args) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, args)
      } finally {
        let end = new Date().valueOf()
        console.log(`start: ${start} end: ${end} consume: ${end - start}`)
      }
    }
  }
}

// 方案二、修改現(xiàn)有描述符
function log2(tag, name, descriptor) {
  let func = descriptor.value // 先獲取之前的函數(shù)

  // 修改對(duì)應(yīng)的value
  descriptor.value = function (...args) {
    let start = new Date().valueOf()
    try {
      return func.apply(this, args)
    } finally {
      let end = new Date().valueOf()
      console.log(`start: ${start} end: ${end} consume: ${end - start}`)
    }
  }
}
訪問(wèn)器

訪問(wèn)器就是添加有getset前綴的函數(shù),用于控制屬性的賦值及取值操作,在使用上與函數(shù)沒(méi)有什么區(qū)別,甚至在返回值的處理上也沒(méi)有什么區(qū)別。
只不過(guò)我們需要按照規(guī)定設(shè)置對(duì)應(yīng)的get或者set描述符罷了:

class Modal {
  _name = "Niko"

  @prefix
  get name() { return this._name }
}

function prefix(target, name, descriptor) {
  return {
    ...descriptor,
    get () {
      return `wrap_${this._name}`
    }
  }
}

console.log(new Modal().name) // wrap_Niko
屬性

對(duì)于屬性的裝飾器,是沒(méi)有返回descriptor的,并且裝飾器函數(shù)的返回值也會(huì)被忽略掉,如果我們想要修改某一個(gè)靜態(tài)屬性,則需要自己獲取descriptor

class Modal {
  @prefix
  static name1 = "Niko"
}

function prefix(target, name) {
  let descriptor = Object.getOwnPropertyDescriptor(target, name)

  Object.defineProperty(target, name, {
    ...descriptor,
    value: `wrap_${descriptor.value}`
  })
}

console.log(Modal.name1) // wrap_Niko

對(duì)于一個(gè)實(shí)例的屬性,則沒(méi)有直接修改的方案,不過(guò)我們可以結(jié)合著一些其他裝飾器來(lái)曲線救國(guó)。

比如,我們有一個(gè)類,會(huì)傳入姓名和年齡作為初始化的參數(shù),然后我們要針對(duì)這兩個(gè)參數(shù)設(shè)置對(duì)應(yīng)的格式校驗(yàn):

const validateConf = {} // 存儲(chǔ)校驗(yàn)信息

@validator
class Person {
  @validate("string")
  name
  @validate("number")
  age

  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

function validator(constructor) {
  return class extends constructor {
    constructor(...args) {
      super(...args)

      // 遍歷所有的校驗(yàn)信息進(jìn)行驗(yàn)證
      for (let [key, type] of Object.entries(validateConf)) {
        if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`)
      }
    }
  }
}

function validate(type) {
  return function (target, name, descriptor) {
    // 向全局對(duì)象中傳入要校驗(yàn)的屬性名及類型
    validateConf[name] = type
  }
}

new Person("Niko", "18")  // throw new error: [age must be number]

首先,在類上邊添加裝飾器@validator,然后在需要校驗(yàn)的兩個(gè)參數(shù)上添加@validate裝飾器,兩個(gè)裝飾器用來(lái)向一個(gè)全局對(duì)象傳入信息,來(lái)記錄哪些屬性是需要進(jìn)行校驗(yàn)的。
然后在validator中繼承原有的類對(duì)象,并在實(shí)例化之后遍歷剛才設(shè)置的所有校驗(yàn)信息進(jìn)行驗(yàn)證,如果發(fā)現(xiàn)有類型錯(cuò)誤的,直接拋出異常。
這個(gè)類型驗(yàn)證的操作對(duì)于原Class來(lái)說(shuō)幾乎是無(wú)感知的。

函數(shù)參數(shù)裝飾器

最后,還有一個(gè)用于函數(shù)參數(shù)的裝飾器,這個(gè)裝飾器也是像實(shí)例屬性一樣的,沒(méi)有辦法多帶帶使用,畢竟函數(shù)是在運(yùn)行時(shí)調(diào)用的,而無(wú)論是何種裝飾器,都是在聲明類時(shí)(可以認(rèn)為是偽編譯期)調(diào)用的。

函數(shù)參數(shù)裝飾器會(huì)接收三個(gè)參數(shù):

類似上述的操作,類的原型或者類的構(gòu)造函數(shù)

參數(shù)所處的函數(shù)名稱

參數(shù)在函數(shù)中形參中的位置(函數(shù)簽名中的第幾個(gè)參數(shù))

一個(gè)簡(jiǎn)單的示例,我們可以結(jié)合著函數(shù)裝飾器來(lái)完成對(duì)函數(shù)參數(shù)的類型轉(zhuǎn)換:

const parseConf = {}
class Modal {
  @parseFunc
  addOne(@parse("number") num) {
    return num + 1
  }
}

// 在函數(shù)調(diào)用前執(zhí)行格式化操作
function parseFunc (target, name, descriptor) {
  return {
    ...descriptor,
    value (...arg) {
      // 獲取格式化配置
      for (let [index, type] of parseConf) {
        switch (type) {
          case "number":  arg[index] = Number(arg[index])             break
          case "string":  arg[index] = String(arg[index])             break
          case "boolean": arg[index] = String(arg[index]) === "true"  break
        }
      }

      return descriptor.value.apply(this, arg)
    }
  }
}

// 向全局對(duì)象中添加對(duì)應(yīng)的格式化信息
function parse(type) {
  return function (target, name, index) {
    parseConf[index] = type
  }
}

console.log(new Modal().addOne("10")) // 11
使用裝飾器實(shí)現(xiàn)一個(gè)有趣的Koa封裝

比如在寫Node接口時(shí),可能是用的koa或者express,一般來(lái)說(shuō)可能要處理很多的請(qǐng)求參數(shù),有來(lái)自headers的,有來(lái)自body的,甚至有來(lái)自query、cookie的。
所以很有可能在router的開(kāi)頭數(shù)行都是這樣的操作:

router.get("/", async (ctx, next) => {
  let id = ctx.query.id
  let uid = ctx.cookies.get("uid")
  let device = ctx.header["device"]
})

以及如果我們有大量的接口,可能就會(huì)有大量的router.getrouter.post。
以及如果要針對(duì)模塊進(jìn)行分類,可能還會(huì)有大量的new Router的操作。

這些代碼都是與業(yè)務(wù)邏輯本身無(wú)關(guān)的,所以我們應(yīng)該盡可能的簡(jiǎn)化這些代碼的占比,而使用裝飾器就能夠幫助我們達(dá)到這個(gè)目的。

裝飾器的準(zhǔn)備
// 首先,我們要?jiǎng)?chuàng)建幾個(gè)用來(lái)存儲(chǔ)信息的全局List
export const routerList      = []
export const controllerList  = []
export const parseList       = []
export const paramList       = []

// 雖說(shuō)我們要有一個(gè)能夠創(chuàng)建Router實(shí)例的裝飾器
// 但是并不會(huì)直接去創(chuàng)建,而是在裝飾器執(zhí)行的時(shí)候進(jìn)行一次注冊(cè)
export function Router(basename = "") {
  return (constrcutor) => {
    routerList.push({
      constrcutor,
      basename
    })
  }
}

// 然后我們?cè)趧?chuàng)建對(duì)應(yīng)的Get Post請(qǐng)求監(jiān)聽(tīng)的裝飾器
// 同樣的,我們并不打算去修改他的任何屬性,只是為了獲取函數(shù)的引用
export function Method(type) {
  return (path) => (target, name, descriptor) => {
    controllerList.push({
      target,
      type,
      path,
      method: name,
      controller: descriptor.value
    })
  }
}

// 接下來(lái)我們還需要用來(lái)格式化參數(shù)的裝飾器
export function Parse(type) {
  return (target, name, index) => {
    parseList.push({
      target,
      type,
      method: name,
      index
    })
  }
}

// 以及最后我們要處理的各種參數(shù)的獲取
export function Param(position) {
  return (key) => (target, name, index) => {
    paramList.push({
      target,
      key,
      position,
      method: name,
      index
    })
  }
}

export const Body   = Param("body")
export const Header = Param("header")
export const Cookie = Param("cookie")
export const Query  = Param("query")
export const Get    = Method("get")
export const Post   = Method("post")
Koa服務(wù)的處理

上邊是創(chuàng)建了所有需要用到的裝飾器,但是也僅僅是把我們所需要的各種信息存了起來(lái),而怎么利用這些裝飾器則是下一步需要做的事情了:

const routers = []

// 遍歷所有添加了裝飾器的Class,并創(chuàng)建對(duì)應(yīng)的Router對(duì)象
routerList.forEach(item => {
  let { basename, constrcutor } = item
  let router = new Router({
    prefix: basename
  })

  controllerList
    .filter(i => i.target === constrcutor.prototype)
    .forEach(controller => {
      router[controller.type](controller.path, async (ctx, next) => {
        let args = []
        // 獲取當(dāng)前函數(shù)對(duì)應(yīng)的參數(shù)獲取
        paramList
          .filter( param => param.target === constrcutor.prototype && param.method === controller.method )
          .map(param => {
            let { index, key } = param
            switch (param.position) {
              case "body":    args[index] = ctx.request.body[key] break
              case "header":  args[index] = ctx.headers[key]      break
              case "cookie":  args[index] = ctx.cookies.get(key)  break
              case "query":   args[index] = ctx.query[key]        break
            }
          })

        // 獲取當(dāng)前函數(shù)對(duì)應(yīng)的參數(shù)格式化
        parseList
          .filter( parse => parse.target === constrcutor.prototype && parse.method === controller.method )
          .map(parse => {
            let { index } = parse
            switch (parse.type) {
              case "number":  args[index] = Number(args[index])             break
              case "string":  args[index] = String(args[index])             break
              case "boolean": args[index] = String(args[index]) === "true"  break
            }
          })

        // 調(diào)用實(shí)際的函數(shù),處理業(yè)務(wù)邏輯
        let results = controller.controller(...args)

        ctx.body = results
      })
    })

  routers.push(router.routes())
})

const app = new Koa()

app.use(bodyParse())
app.use(compose(routers))

app.listen(12306, () => console.log("server run as http://127.0.0.1:12306"))

上邊的代碼就已經(jīng)搭建出來(lái)了一個(gè)Koa的封裝,以及包含了對(duì)各種裝飾器的處理,接下來(lái)就是這些裝飾器的實(shí)際應(yīng)用了:

import { Router, Get, Query, Parse } from "../decorators"

@Router("")
export default class {
  @Get("/")
  index (@Parse("number") @Query("id") id: number) {
    return {
      code: 200,
      id,
      type: typeof id
    }
  }

  @Post("/detail")
  detail (
    @Parse("number") @Query("id") id: number, 
    @Parse("number") @Body("age") age: number
  ) {
    return {
      code: 200,
      age: age + 1
    }
  }
}

很輕易的就實(shí)現(xiàn)了一個(gè)router的創(chuàng)建,路徑、method的處理,包括各種參數(shù)的獲取,類型轉(zhuǎn)換。
將各種非業(yè)務(wù)邏輯相關(guān)的代碼統(tǒng)統(tǒng)交由裝飾器來(lái)做,而函數(shù)本身只負(fù)責(zé)處理自身邏輯即可。
這里有完整的代碼:GitHub。安裝依賴后npm start即可看到效果。

這樣開(kāi)發(fā)帶來(lái)的好處就是,讓代碼可讀性變得更高,在函數(shù)中更專注的做自己應(yīng)該做的事情。
而且裝飾器本身如果名字起的足夠好的好,也是在一定程度上可以當(dāng)作文檔注釋來(lái)看待了(Java中有個(gè)類似的玩意兒叫做注解)。

總結(jié)

合理利用裝飾器可以極大的提高開(kāi)發(fā)效率,對(duì)一些非邏輯相關(guān)的代碼進(jìn)行封裝提煉能夠幫助我們快速完成重復(fù)性的工作,節(jié)省時(shí)間。
但是糖再好吃,也不要吃太多,容易壞牙齒的,同樣的濫用裝飾器也會(huì)使代碼本身邏輯變得撲朔迷離,如果確定一段代碼不會(huì)在其他地方用到,或者一個(gè)函數(shù)的核心邏輯就是這些代碼,那么就沒(méi)有必要將它取出來(lái)作為一個(gè)裝飾器來(lái)存在。

參考資料

typescript | decorators

koa示例的原版,簡(jiǎn)化代碼便于舉例

One more thing

我司現(xiàn)在大量招人咯,前端、Node方向都有HC
公司名:Blued,坐標(biāo)帝都朝陽(yáng)雙井
主要技術(shù)棧是React,也會(huì)有機(jī)會(huì)玩ReactNative和Electron
Node方向8.x版本+koa 新項(xiàng)目會(huì)以TS為主
有興趣的小伙伴可以聯(lián)系我詳談:
email: [email protected]
wechat: github_jiasm

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/96019.html

相關(guān)文章

  • Decorator:從原理到實(shí)踐,我一點(diǎn)都不虛~

    摘要:描述符必須是這兩種形式之一不能同時(shí)是兩者。可以是任何有效的值數(shù)值,對(duì)象,函數(shù)等。當(dāng)且僅當(dāng)該屬性的為時(shí),才能被賦值運(yùn)算符改變。特點(diǎn)就是不影響之前對(duì)象的特性,而新增額外的職責(zé)功能。 前言 原文鏈接:[Nealyang/personalBlog]() showImg(https://segmentfault.com/img/remote/1460000018958861); ES6 已經(jīng)不必...

    XanaHopper 評(píng)論0 收藏0
  • python

    Python裝飾器為什么難理解? 無(wú)論項(xiàng)目中還是面試都離不開(kāi)裝飾器話題,裝飾器的強(qiáng)大在于它能夠在不修改原有業(yè)務(wù)邏輯的情況下對(duì)代碼進(jìn)行擴(kuò)展,權(quán)限校驗(yàn)、用戶認(rèn)證、日志記錄、性能測(cè)試、事務(wù)處理、緩存等都是裝飾器的絕佳應(yīng)用場(chǎng)景,它能夠最大程度地對(duì)代碼進(jìn)行復(fù)用。 但為什么初學(xué)者對(duì)裝飾器的理解如此困難,我認(rèn)為本質(zhì)上是對(duì)Py… Python 實(shí)現(xiàn)車牌定位及分割 作者用 Python 實(shí)現(xiàn)車牌定位及分割的實(shí)踐。 ...

    chenatu 評(píng)論0 收藏0
  • ::before和::after偽元素的妙用

    摘要:下面是偽元素和偽元素的具體用法和使用規(guī)則作用在標(biāo)簽前后添加字符串使用規(guī)則后面一定要用把要添加的內(nèi)容括起來(lái),否則是無(wú)法顯示的括號(hào)不一定需要添加。場(chǎng)景: 假如有一天,你的在寫一個(gè)前端項(xiàng)目,是關(guān)于一份點(diǎn)餐商家電話信息表,你啪塔啪塔地寫完了,突然間項(xiàng)目經(jīng)理跑過(guò)來(lái)找你,要求你在每一個(gè)商家的電話號(hào)碼前都添加一個(gè)電話符號(hào),來(lái)使得電話號(hào)碼更直觀和頁(yè)面更美觀。這個(gè)時(shí)候你就糾結(jié)了,這不是折磨人嗎?這不是要我在...

    番茄西紅柿 評(píng)論0 收藏0
  • 前端小知識(shí)--TypeSript和JavaScript到底是什么關(guān)系?

    摘要:想學(xué)好前端,真的要主動(dòng),然后對(duì)所有的英文文檔耐心一點(diǎn)。在年月日,國(guó)際組織發(fā)布了的第六版,該版本正式名稱為,但通常被稱為或者。自此,每年發(fā)布一次新標(biāo)準(zhǔn)。但保留了用于依賴注入的構(gòu)造函數(shù)參數(shù)類型。必須在構(gòu)造函數(shù)中聲明屬性,而不是在類的代碼體中。 從 TypeScript 到 ES6 到 ES5 在我初學(xué)前端的很長(zhǎng)一段時(shí)間,不愿意碰git,不愿意碰框架,總是嫌麻煩,連ES6也沒(méi)有怎么去弄明白...

    sixleaves 評(píng)論0 收藏0
  • CSS選擇器的妙用

    摘要:為元素添加邊框,最后一個(gè)去除邊框其實(shí)和也是可以的列表的每一項(xiàng)用逗號(hào)分隔需要注意的是復(fù)制粘貼并不會(huì)帶走生成的內(nèi)容也就是以下代碼中生成的逗號(hào)是不會(huì)被復(fù)制粘貼配合一起使用隱藏沒(méi)有靜音自動(dòng)播放的影片根據(jù)文本內(nèi)容獲取節(jié)點(diǎn)元素控制字體彈性 :not 1)為元素添加邊框,最后一個(gè)去除邊框showImg(https://segmentfault.com/img/bVQMNb?w=742&h=90);(...

    Aklman 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<