最近新開了一個Node項目,採用TypeScript來開發,在數據庫及路由管理方面用了很多的裝飾器,發覺這的確是一個好東西。
裝飾器是一個還處於草案中的特性,目前木有直接支持該語法的環境,可是能夠經過 babel 之類的進行轉換爲舊語法來實現效果,因此在TypeScript中,能夠放心的使用@Decorator
。javascript
裝飾器是對類、函數、屬性之類的一種裝飾,能夠針對其添加一些額外的行爲。
通俗的理解能夠認爲就是在原有代碼外層包裝了一層處理邏輯。
我的認爲裝飾器是一種解決方案,而並不是是狹義的@Decorator
,後者僅僅是一個語法糖罷了。html
裝飾器在身邊的例子隨處可見,一個簡單的例子,水龍頭上邊的起泡器就是一個裝飾器,在裝上之後就會把空氣混入水流中,摻雜不少泡泡在水裏。
可是起泡器安裝與否對水龍頭自己並無什麼影響,即便拆掉起泡器,也會照樣工做,水龍頭的做用在於閥門的控制,至於水中摻不摻雜氣泡則不是水龍頭須要關心的。前端
因此,對於裝飾器,能夠簡單地理解爲是非侵入式的行爲修改。java
可能有些時候,咱們會對傳入參數的類型判斷、對返回值的排序、過濾,對函數添加節流、防抖或其餘的功能性代碼,基於多個類的繼承,各類各樣的與函數邏輯自己無關的、重複性的代碼。git
能夠想像一下,咱們有一個工具類,提供了一個獲取數據的函數:github
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
中進行修改:typescript
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' } ]
複製代碼
這樣在調用方法後咱們就能夠在控制檯看到耗時的輸出了。
可是這樣直接修改原函數代碼有如下幾個問題:數據庫
因此,爲了讓統計耗時的邏輯變得更加靈活,咱們將建立一個新的工具函數,用來包裝須要設置統計耗時的函數。
經過將Class
與目標函數的name
傳遞到函數中,實現了通用的耗時統計:express
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
的函數不可被其餘人修改覆蓋,因此要添加一些新的邏輯:npm
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')
複製代碼
到了這一步之後,咱們就能夠稱log
和seal
爲裝飾器了,能夠很方便的讓咱們對一些函數添加行爲。
而拆分出來的這些功能能夠用於將來可能會有須要的地方,而不用從新開發一遍相同的邏輯。
就像上邊提到了,現階段在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
上。
大體思路就是上邊列出來的,可是這個mixin
是React
中內置的一個操做,咱們能夠將其轉換爲更接近裝飾器的實現。
在不修改原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
了。
可以幫你省去不少繁瑣的步驟來用上裝飾器。
草案中的裝飾器、或者能夠說是TS實現的裝飾器,將上邊的兩種進一步地封裝,將其拆分紅爲更細的裝飾器應用,目前支持如下幾處使用:
@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
}
複製代碼
函數tag
與method
會在class A
定義的時候執行。
該裝飾器會在class定義前調用,若是函數有返回值,則會認爲是一個新的構造函數來替代以前的構造函數。
函數接收一個參數:
咱們能夠針對原有的構造函數進行一些改造:
若是想要新增一些屬性之類的,有兩種方案能夠選擇:
class
繼承自原有class
,並添加屬性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 應該是應用最爲普遍的一處了,函數,屬性,get
、set
訪問器,這幾處均可以認爲是類成員。
在TS文檔中被分爲了Method Decorator
、Accessor Decorator
和Property Decorator
,實際上一模一樣。
關於這類裝飾器,會接收以下三個參數:
Object.getOwnPropertyDescriptor
的返回值
Property Decorator
不會返回第三個參數,可是能夠本身手動獲取
前提是靜態成員,而非實例成員,由於裝飾器都是運行在類建立時,而實例成員是在實例化一個類的時候纔會執行的,因此沒有辦法獲取對應的descriptor
能夠稍微明確一下,靜態成員與實例成員的區別:
class Model {
// 實例成員
method1 () {}
method2 = () => {}
// 靜態成員
static method3 () {}
static method4 = () => {}
}
複製代碼
method1
和method2
是實例成員,method1
存在於prototype
之上,而method2
只在實例化對象之後纔有。
做爲靜態成員的method3
和method4
,二者的區別在因而否可枚舉描述符的設置,因此能夠簡單地認爲,上述代碼轉換爲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
,目前沒有找到合理的解釋,可是若是明確的要使用,是能夠手動獲取的。
就像上述的示例,咱們針對四個成員都添加了裝飾器之後,method1
和method2
第一個參數就是Model.prototype
,而method3
和method4
的第一個參數就是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}`)
}
}
}
}
// 方案2、修改現有描述符
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}`)
}
}
}
複製代碼
訪問器就是添加有get
、set
前綴的函數,用於控制屬性的賦值及取值操做,在使用上與函數沒有什麼區別,甚至在返回值的處理上也沒有什麼區別。
只不過咱們須要按照規定設置對應的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
來講幾乎是無感知的。
最後,還有一個用於函數參數的裝飾器,這個裝飾器也是像實例屬性同樣的,沒有辦法單獨使用,畢竟函數是在運行時調用的,而不管是何種裝飾器,都是在聲明類時(能夠認爲是僞編譯期)調用的。
函數參數裝飾器會接收三個參數:
一個簡單的示例,咱們能夠結合着函數裝飾器來完成對函數參數的類型轉換:
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
複製代碼
好比在寫Node接口時,多是用的koa
或者express
,通常來講可能要處理不少的請求參數,有來自headers
的,有來自body
的,甚至有來自query
、cookie
的。
因此頗有可能在router
的開頭數行都是這樣的操做:
router.get('/', async (ctx, next) => {
let id = ctx.query.id
let uid = ctx.cookies.get('uid')
let device = ctx.header['device']
})
複製代碼
以及若是咱們有大量的接口,可能就會有大量的router.get
、router.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')
複製代碼
上邊是建立了全部須要用到的裝飾器,可是也僅僅是把咱們所須要的各類信息存了起來,而怎麼利用這些裝飾器則是下一步須要作的事情了:
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中有個相似的玩意兒叫作註解)。
合理利用裝飾器能夠極大的提升開發效率,對一些非邏輯相關的代碼進行封裝提煉可以幫助咱們快速完成重複性的工做,節省時間。
可是糖再好吃,也不要吃太多,容易壞牙齒的,一樣的濫用裝飾器也會使代碼自己邏輯變得撲朔迷離,若是肯定一段代碼不會在其餘地方用到,或者一個函數的核心邏輯就是這些代碼,那麼就沒有必要將它取出來做爲一個裝飾器來存在。
我司如今大量招人咯,前端、Node方向都有HC
公司名:Blued,座標帝都朝陽雙井 主要技術棧是React,也會有機會玩ReactNative和Electron Node方向8.x版本+koa 新項目會以TS爲主 有興趣的小夥伴能夠聯繫我詳談: email: jiashunming@blued.com wechat: github_jiasm