因为@
符号后边跟的是一个函数的引用,所以对于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
不会返回第三个参数,但是可以自己手动获取 可以稍微明确一下,静态成员与实例成员的区别:
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}`) } } } } // 方案二、修改现有描述符 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中有个类似的玩意儿叫做注解)。
合理利用装饰器可以极大的提高开发效率,对一些非逻辑相关的代码进行封装提炼能够帮助我们快速完成重复性的工作,节省时间。
但是糖再好吃,也不要吃太多,容易坏牙齿的,同样的滥用装饰器也会使代码本身逻辑变得扑朔迷离,如果确定一段代码不会在其他地方用到,或者一个函数的核心逻辑就是这些代码,那么就没有必要将它取出来作为一个装饰器来存在。