Javascript裝飾器的妙用

来源:https://www.cnblogs.com/jiasm/archive/2018/07/08/9281113.html
-Advertisement-
Play Games

最近新開了一個Node項目,採用TypeScript來開發,在資料庫及路由管理方面用了不少的裝飾器,發覺這的確是一個好東西。裝飾器是一個還處於草案中的特性,目前木有直接支持該語法的環境,但是可以通過 babel 之類的進行轉換為舊語法來實現效果,所以在TypeScript中,可以放心的使用@Deco ...


最近新開了一個Node項目,採用TypeScript來開發,在資料庫及路由管理方面用了不少的裝飾器,發覺這的確是一個好東西。
裝飾器是一個還處於草案中的特性,目前木有直接支持該語法的環境,但是可以通過 babel 之類的進行轉換為舊語法來實現效果,所以在TypeScript中,可以放心的使用@Decorator

什麼是裝飾器

裝飾器是對類、函數、屬性之類的一種裝飾,可以針對其添加一些額外的行為。
通俗的理解可以認為就是在原有代碼外層包裝了一層處理邏輯。
個人認為裝飾器是一種解決方案,而並非是狹義的@Decorator,後者僅僅是一個語法糖罷了。

裝飾器在身邊的例子隨處可見,一個簡單的例子,水龍頭上邊的起泡器就是一個裝飾器,在裝上以後就會把空氣混入水流中,摻雜很多泡泡在水裡。
但是起泡器安裝與否對水龍頭本身並沒有什麼影響,即使拆掉起泡器,也會照樣工作,水龍頭的作用在於閥門的控制,至於水中摻不摻雜氣泡則不是水龍頭需要關心的。

所以,對於裝飾器,可以簡單地理解為是非侵入式的行為修改。

為什麼要用裝飾器

可能有些時候,我們會對傳入參數的類型判斷、對返回值的排序、過濾,對函數添加節流、防抖或其他的功能性代碼,基於多個類的繼承,各種各樣的與函數邏輯本身無關的、重覆性的代碼。

函數中的作用

可以想像一下,我們有一個工具類,提供了一個獲取數據的函數:

class Model1 {
  getData() {
    // 此處省略獲取數據的邏輯
    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' } ]

 

現在我們想要添加一個功能,記錄該函數執行的耗時。
因為這個函數被很多人使用,在調用方添加耗時統計邏輯是不可取的,所以我們要在Model1中進行修改:

class Model1 {
  getData() {
+   let start = new Date().valueOf()
+   try {
      // 此處省略獲取數據的邏輯
      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' } ]

 

這樣在調用方法後我們就可以在控制台看到耗時的輸出了。
但是這樣直接修改原函數代碼有以下幾個問題:

  1. 統計耗時的相關代碼與函數本身邏輯並無一點關係,影響到了對原函數本身的理解,對函數結構造成了破壞性的修改
  2. 如果後期還有更多類似的函數需要添加統計耗時的代碼,在每個函數中都添加這樣的代碼顯然是低效的,維護成本太高

所以,為了讓統計耗時的邏輯變得更加靈活,我們將創建一個新的工具函數,用來包裝需要設置統計耗時的函數。
通過將Class與目標函數的name傳遞到函數中,實現了通用的耗時統計:

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

  // 獲取函數對應的描述符
  let descriptor = Object.getOwnPropertyDescriptor(target, key)

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

  // 將修改後的函數重新定義到原型鏈上
  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' } ]

 

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

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

  // 獲取函數對應的描述符
  let descriptor = Object.getOwnPropertyDescriptor(target, key)

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

wrap(Model1, 'getData')

 



Model1.prototype.getData = 1 // 無效

可以看出,兩個wrap函數中有不少重覆的地方,而修改程式行為的邏輯,實際上依賴的是Object.defineProperty中傳遞的三個參數。
所以,我們針對wrap在進行一次修改,將其變為一個通用類的轉換:

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) {
  // 將修改後的函數重新定義到原型鏈上
  Object.defineProperty(target, key, {
    ...descriptor,
    value: function (...arg) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, arg) // 調用之前的函數
      } 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
  })
}

// 參數的轉換處理
log = wrap(log)
seal = warp(seal)

// 添加耗時統計
log(Model1, 'getData')
log(Model2, 'getData')

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

 

到了這一步以後,我們就可以稱logseal為裝飾器了,可以很方便的讓我們對一些函數添加行為。
而拆分出來的這些功能可以用於未來可能會有需要的地方,而不用重新開發一遍相同的邏輯。

Class 中的作用

就像上邊提到了,現階段在JS中繼承多個Class是一件頭疼的事情,沒有直接的語法能夠繼承多個 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中就有了一個mixin的概念,用來將多個Class的功能複製到一個新的Class上。
大致思路就是上邊列出來的,但是這個mixinReact中內置的一個操作,我們可以將其轉換為更接近裝飾器的實現。
在不修改原Class的情況下,將其他Class的屬性複製過來:

function mixin(constructor) {
  return function (...args) {
    for (let arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === 'constructor') continue // 跳過構造函數
        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

 

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

@Decorator的使用方法

草案中的裝飾器、或者可以說是TS實現的裝飾器,將上邊的兩種進一步地封裝,將其拆分成為更細的裝飾器應用,目前支持以下幾處使用:

  1. Class
  2. 函數
  3. get set訪問器
  4. 實例屬性、靜態函數及屬性
  5. 函數參數

@Decorator的語法規定比較簡單,就是通過@符號後邊跟一個裝飾器函數的引用:

@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
}

 

函數tagmethod會在class A定義的時候執行。

@Decorator 在 Class 中的使用

該裝飾器會在class定義前調用,如果函數有返回值,則會認為是一個新的構造函數來替代之前的構造函數。

函數接收一個參數:

  1. constructor 之前的構造函數

我們可以針對原有的構造函數進行一些改造:

新增一些屬性

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

  1. 創建一個新的class繼承自原有class,並添加屬性
  2. 針對當前class進行修改

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

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

// 創建一個繼承自Person的匿名類
// 直接返回並替換原有的構造函數
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 // 無效

 

使用閉包來增強裝飾器的功能

在TS文檔中被稱為裝飾器工廠

因為@符號後邊跟的是一個函數的引用,所以對於mixin的實現,我們可以很輕易的使用閉包來實現:

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

@mixin(A, B)
class C { }

function mixin(...args) {
  // 調用函數返回裝飾器實際應用的函數
  return function(constructor) {
    for (let arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === 'constructor') continue // 跳過構造函數
        Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
      }
    }
  }
}

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

 

多個裝飾器的應用

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

@decorator1
@decorator2
class { }

 

執行的順序為decorator2 -> decorator1,離class定義最近的先執行。
可以想像成函數嵌套的形式:

decorator1(decorator2(class {}))

 

@Decorator 在 Class 成員中的使用

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

關於這類裝飾器,會接收如下三個參數:

  1. 如果裝飾器掛載於靜態成員上,則會返回構造函數,如果掛載於實例成員上則會返回類的原型
  2. 裝飾器掛載的成員名稱
  3. 成員的描述符,也就是Object.getOwnPropertyDescriptor的返回值

Property Decorator不會返回第三個參數,但是可以自己手動獲取
前提是靜態成員,而非實例成員,因為裝飾器都是運行在類創建時,而實例成員是在實例化一個類的時候才會執行的,所以沒有辦法獲取對應的descriptor

靜態成員與實例成員在返回值上的區別

可以稍微明確一下,靜態成員與實例成員的區別:

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

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

 

method1method2是實例成員,method1存在於prototype之上,而method2只在實例化對象以後才有。
作為靜態成員的method3method4,兩者的區別在於是否可枚舉描述符的設置,所以可以簡單地認為,上述代碼轉換為ES5版本後是這樣子的:

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

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

// 成員被定義在構造函數上,且是預設的可被枚舉
Model.method4 = function () {}

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

 

可以看出,只有method2是在實例化時才賦值的,一個不存在的屬性是不會有descriptor的,所以這就是為什麼TS在針對Property Decorator不傳遞第三個參數的原因,至於為什麼靜態成員也沒有傳遞descriptor,目前沒有找到合理的解釋,但是如果明確的要使用,是可以手動獲取的。

就像上述的示例,我們針對四個成員都添加了裝飾器以後,method1method2第一個參數就是Model.prototype,而method3method4的第一個參數就是Model

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

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

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

function static(target) {
  console.log(target === Model)
}

 

函數,訪問器,和屬性裝飾器三者之間的區別

函數

首先是函數,函數裝飾器的返回值會預設作為屬性的value描述符存在,如果返回值為undefined則會忽略,使用之前的descriptor引用作為函數的描述符。
所以針對我們最開始的統計耗時的邏輯可以這麼來做:

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}`)
      }
    }
  }
}

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

  // 修改對應的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}`)
    }
  }
}

 

訪問器

訪問器就是添加有getset首碼的函數,用於控制屬性的賦值及取值操作,在使用上與函數沒有什麼區別,甚至在返回值的處理上也沒有什麼區別。
只不過我們需要按照規定設置對應的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

 

屬性

對於屬性的裝飾器,是沒有返回descriptor的,並且裝飾器函數的返回值也會被忽略掉,如果我們想要修改某一個靜態屬性,則需要自己獲取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

 

對於一個實例的屬性,則沒有直接修改的方案,不過我們可以結合著一些其他裝飾器來曲線救國。

比如,我們有一個類,會傳入姓名和年齡作為初始化的參數,然後我們要針對這兩個參數設置對應的格式校驗:

const validateConf = {} // 存儲校驗信息

@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)

      // 遍歷所有的校驗信息進行驗證
      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) {
    // 向全局對象中傳入要校驗的屬性名及類型
    validateConf[name] = type
  }
}

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

 

首先,在類上邊添加裝飾器@validator,然後在需要校驗的兩個參數上添加@validate裝飾器,兩個裝飾器用來向一個全局對象傳入信息,來記錄哪些屬性是需要進行校驗的。
然後在validator中繼承原有的類對象,併在實例化之後遍歷剛纔設置的所有校驗信息進行驗證,如果發現有類型錯誤的,直接拋出異常。
這個類型驗證的操作對於原Class來說幾乎是無感知的。

函數參數裝飾器

最後,還有一個用於函數參數的裝飾器,這個裝飾器也是像實例屬性一樣的,沒有辦法單獨使用,畢竟函數是在運行時調用的,而無論是何種裝飾器,都是在聲明類時(可以認為是偽編譯期)調用的。

函數參數裝飾器會接收三個參數:

  1. 類似上述的操作,類的原型或者類的構造函數
  2. 參數所處的函數名稱
  3. 參數在函數中形參中的位置(函數簽名中的第幾個參數)

一個簡單的示例,我們可以結合著函數裝飾器來完成對函數參數的類型轉換:

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

// 在函數調用前執行格式化操作
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)
      }
    }
  }
}

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

console.log(new Modal().addOne('10')) // 11

 

使用裝飾器實現一個有趣的Koa封裝

比如在寫Node介面時,可能是用的koa或者express,一般來說可能要處理很多的請求參數,有來自headers的,有來自body的,甚至有來自querycookie的。
所以很有可能在router的開頭數行都是這樣的操作:

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

 

以及如果我們有大量的介面,可能就會有大量的router.getrouter.post
以及如果要針對模塊進行分類,可能還會有大量的new Router的操作。

這些代碼都是與業務邏輯本身無關的,所以我們應該儘可能的簡化這些代碼的占比,而使用裝飾器就能夠幫助我們達到這個目的。

裝飾器的準備

// 首先,我們要創建幾個用來存儲信息的全局List
export const routerList      = []
export const controllerList  = []
export const parseList       = []
export const paramList       = []

// 雖說我們要有一個能夠創建Router實例的裝飾器
// 但是並不會直接去創建,而是在裝飾器執行的時候進行一次註冊
export function Router(basename = '') {
  return (constrcutor) => {
    routerList.push({
      constrcutor,
      basename
    })
  }
}

// 然後我們在創建對應的Get Post請求監聽的裝飾器
// 同樣的,我們並不打算去修改他的任何屬性,只是為了獲取函數的引用
export function Method(type) {
  return (path) => (target, name, descriptor) => {
    controllerList.push({
      target,
      type,
      path,
      method: name,
      controller: descriptor.value
    })
  }
}

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

// 以及最後我們要處理的各種參數的獲取
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服務的處理

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

const routers = []

// 遍歷所有添加了裝飾器的Class,並創建對應的Router對象
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 = []
        // 獲取當前函數對應的參數獲取
        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
            }
          })

        // 獲取當前函數對應的參數格式化
        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
            }
          })

        // 調用實際的函數,處理業務邏輯
        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'))

 

上邊的代碼就已經搭建出來了一個Koa的封裝,以及包含了對各種裝飾器的處理,接下來就是這些裝飾器的實際應用了:

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

 

很輕易的就實現了一個router的創建,路徑、method的處理,包括各種參數的獲取,類型轉換。
將各種非業務邏輯相關的代碼統統交由裝飾器來做,而函數本身只負責處理自身邏輯即可。
這裡有完整的代碼:GitHub。安裝依賴後npm start即可看到效果。

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

總結

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

參考資料

    1. typescript | decorators
    2. koa示例的原版,簡化代碼便於舉例

 

 

One more thing

我司現在大量招人咯,前端、Node方向都有HC
公司名:Blued,坐標帝都朝陽雙井
主要技術棧是React,也會有機會玩ReactNative和Electron
Node方向8.x版本+koa 新項目會以TS為主
有興趣的小伙伴可以聯繫我詳談:
email: [email protected]
wechat: github_jiasm


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 分組 在使用正則的時候,有時候會想要匹配一串字元串連續出現多次的情況,比如:我想匹配字元串 連續出現3次的情況。 有些人會直接寫: 但是,這種情況僅僅會匹配 加上三個 ,顯然,這樣是錯誤的。 要想實現之前的需求,我們需要使用正則表達式的分組功能:使用 可以達到分組的功能,使量詞作用於分組。所以,如下 ...
  • offset大家族 ...
  • h5圖片獲取展示,非同步上傳至伺服器。( 如果有問題,歡迎留言 ^_^ ) ...
  • react有很多好玩的組件,react-grid-gallery就是其中一個,主要處理圖片展示,對圖片進行放大與縮小 文檔:https://www.npmjs.com/package/react-grid-gallery demo:https://benhowell.github.io/react- ...
  • 基本選擇器 回顧選擇器 通配符選擇器 元素選擇器 類選擇器 ID選擇器 後代選擇器 新增基本選擇器 子元素選擇器 相鄰兄弟選擇器 通用兄弟選擇器 群組選擇器 子元素選擇器 概念:子元素選擇器只能選擇某元素的子元素 語法:父元素 子元素 (Father Children) 相容性:IE8+、Firef ...
  • Sublime Text是一款輕量級的易於使用的前端編寫軟體,個人比較推薦。 找到Sublime的官網,下載對應的版本後,點擊安裝。安裝完成後需要下載相應的插件才能進行更加 有效率的開發工作。編寫前端的插件常用的就是Emmet。 首先在Sublime中使用快捷鍵ctrl+shift+p進入頁面,輸入 ...
  • 隨著業務的增加,可能存在這麼一種需求,就是需要從h5中直接跳轉到app。如果沒有安裝app的話,則提示到應用市場或者app store下載安裝。不過問題就在這個地方,單純的用h5是沒有方法判斷是否安裝過app的,不過這些是難不倒程式員的,他們通常會用這種代碼來解決 其實代碼很簡單,先去跳轉公司無線組 ...
  • 1.行內元素和塊級元素?img算什麼?行內元素怎麼轉化為塊級元素? 2.將多個元素設置為同一行?清除浮動有幾種方式? 3.怪異盒模型box-sizing?彈性盒模型|盒佈局? 4.簡述幾個css hack? 5.href和src區別? title和alt 6.transform?animation? ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...