因?yàn)?code>@符號(hào)后邊跟的是一個(gè)函數(shù)的引用,所以對(duì)于mixin的實(shí)現(xiàn),我們可以很輕易的使用閉包來實(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 // 跳過構(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
裝飾器是可以同時(shí)應(yīng)用多個(gè)的(不然也就失去了最初的意義)。
用法如下:
@decorator1 @decorator2 class { }
執(zhí)行的順序?yàn)?code>decorator2 -> decorator1
,離class
定義最近的先執(zhí)行。
可以想像成函數(shù)嵌套的形式:
decorator1(decorator2(class {}))
類成員上的 @Decorator 應(yīng)該是應(yīng)用最為廣泛的一處了,函數(shù),屬性,get
、set
訪問器,這幾處都可以認(rèn)為是類成員。
在TS文檔中被分為了Method Decorator
、Accessor Decorator
和Property 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í)例成員的區(qū)別:
class Model { // 實(shí)例成員 method1 () {} method2 = () => {} // 靜態(tài)成員 static method3 () {} static method4 = () => {} }
method1
和method2
是實(shí)例成員,method1
存在于prototype
之上,而method2
只在實(shí)例化對(duì)象以后才有。
作為靜態(tài)成員的method3
和method4
,兩者的區(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)成員也沒有傳遞descriptor
,目前沒有找到合理的解釋,但是如果明確的要使用,是可以手動(dòng)獲取的。
就像上述的示例,我們針對(duì)四個(gè)成員都添加了裝飾器以后,method1
和method2
第一個(gè)參數(shù)就是Model.prototype
,而method3
和method4
的第一個(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ù),函數(shù)裝飾器的返回值會(huì)默認(rèn)作為屬性的value
描述符存在,如果返回值為undefined
則會(huì)忽略,使用之前的descriptor
引用作為函數(shù)的描述符。
所以針對(duì)我們最開始的統(tǒng)計(jì)耗時(shí)的邏輯可以這么來做:
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}`) } } }
訪問器就是添加有get
、set
前綴的函數(shù),用于控制屬性的賦值及取值操作,在使用上與函數(shù)沒有什么區(qū)別,甚至在返回值的處理上也沒有什么區(qū)別。
只不過我們需要按照規(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ì)于屬性的裝飾器,是沒有返回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í)例的屬性,則沒有直接修改的方案,不過我們可以結(jié)合著一些其他裝飾器來曲線救國(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è)裝飾器用來向一個(gè)全局對(duì)象傳入信息,來記錄哪些屬性是需要進(jìn)行校驗(yàn)的。
然后在validator
中繼承原有的類對(duì)象,并在實(shí)例化之后遍歷剛才設(shè)置的所有校驗(yàn)信息進(jìn)行驗(yàn)證,如果發(fā)現(xiàn)有類型錯(cuò)誤的,直接拋出異常。
這個(gè)類型驗(yàn)證的操作對(duì)于原Class
來說幾乎是無感知的。
最后,還有一個(gè)用于函數(shù)參數(shù)的裝飾器,這個(gè)裝飾器也是像實(shí)例屬性一樣的,沒有辦法單獨(dú)使用,畢竟函數(shù)是在運(yùn)行時(shí)調(diào)用的,而無論是何種裝飾器,都是在聲明類時(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ù)裝飾器來完成對(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
比如在寫Node接口時(shí),可能是用的koa
或者express
,一般來說可能要處理很多的請(qǐng)求參數(shù),有來自headers
的,有來自body
的,甚至有來自query
、cookie
的。
所以很有可能在router
的開頭數(shù)行都是這樣的操作:
router.get('/', async (ctx, next) => { let id = ctx.query.id let uid = ctx.cookies.get('uid') let device = ctx.header['device'] })
以及如果我們有大量的接口,可能就會(huì)有大量的router.get
、router.post
。
以及如果要針對(duì)模塊進(jìn)行分類,可能還會(huì)有大量的new Router
的操作。
這些代碼都是與業(yè)務(wù)邏輯本身無關(guān)的,所以我們應(yīng)該盡可能的簡(jiǎn)化這些代碼的占比,而使用裝飾器就能夠幫助我們達(dá)到這個(gè)目的。
// 首先,我們要?jiǎng)?chuàng)建幾個(gè)用來存儲(chǔ)信息的全局List export const routerList = [] export const controllerList = [] export const parseList = [] export const paramList = [] // 雖說我們要有一個(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)聽的裝飾器 // 同樣的,我們并不打算去修改他的任何屬性,只是為了獲取函數(shù)的引用 export function Method(type) { return (path) => (target, name, descriptor) => { controllerList.push({ target, type, path, method: name, controller: descriptor.value }) } } // 接下來我們還需要用來格式化參數(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')
上邊是創(chuàng)建了所有需要用到的裝飾器,但是也僅僅是把我們所需要的各種信息存了起來,而怎么利用這些裝飾器則是下一步需要做的事情了:
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)搭建出來了一個(gè)Koa的封裝,以及包含了對(duì)各種裝飾器的處理,接下來就是這些裝飾器的實(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)交由裝飾器來做,而函數(shù)本身只負(fù)責(zé)處理自身邏輯即可。
這里有完整的代碼:GitHub。安裝依賴后npm start
即可看到效果。
這樣開發(fā)帶來的好處就是,讓代碼可讀性變得更高,在函數(shù)中更專注的做自己應(yīng)該做的事情。
而且裝飾器本身如果名字起的足夠好的好,也是在一定程度上可以當(dāng)作文檔注釋來看待了(Java中有個(gè)類似的玩意兒叫做注解)。
合理利用裝飾器可以極大的提高開發(fā)效率,對(duì)一些非邏輯相關(guān)的代碼進(jìn)行封裝提煉能夠幫助我們快速完成重復(fù)性的工作,節(jié)省時(shí)間。
但是糖再好吃,也不要吃太多,容易壞牙齒的,同樣的濫用裝飾器也會(huì)使代碼本身邏輯變得撲朔迷離,如果確定一段代碼不會(huì)在其他地方用到,或者一個(gè)函數(shù)的核心邏輯就是這些代碼,那么就沒有必要將它取出來作為一個(gè)裝飾器來存在。
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com