最近新开了一个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