了解装饰器
装饰器(Decorator)是用来修改类行为的一个函数(语法糖),在许多面向对象语言中都有这个东西。
语法
装饰器是一个函数,接受 3 个参数target
name
descriptor
- target 是被修饰的目标对象
- name 是被修饰的属性名
- descriptor 是属性的描述
定义一个装饰器函数
1 | function setName(target, name, descriptor) { |
差异
装饰器装饰不同类型的目标是有一些差异的,这些差异体现在装饰函数接受的参数里面。
首先对一个类的装饰是由内到外的,先从类的属性开始,从上到下,按顺序修饰,如果类的属性是个方法,那么会先装饰这个方法的属性,再装饰这个方法。如上 demo 的 console
装饰 Class
装饰函数接收到的参数 target
是类的本身,name
与descriptor
都是undefined
装饰 Class 的属性
装饰函数接收到的参数target
是类的原型,也就是class.prototype
name
为该属性的名字
当这个属性是个函数时:
descriptor
为该方法的描述,通过Object.getOwnPropertyDescriptor(obj, prop)
获得
当这个属性非函数时:
descriptor
为undefined
装饰 Class 方法的参数
装饰函数接受到的参数target
是类的原型
name
为该参数的名字
descriptor
为该参数是这个函数的第几个参数,index:number
了解 Reflect.metadate
Reflect 可以理解为反射,可以改变Object
的一些行为。
reflect.metadata从名字上看,就是对对象设置一些元数据。
有 2 个比较重要的 api
Reflect.getMetadata(key, target)
通过key
获得在target
上设置的元数据
Reflect.defineMetadata(key, value, target)
通过key
设置value
到target
上
实现这个 2 个 api 不难,通过weakMap
和Map
就可以实现了。
这样的数据结构
weakMap[target, Map[key, value]]
koa 路由
koa 的中间件模型不做介绍,koa-router
就是个中间件。
路由其实就是映射一个controller
方法到一个path
字符串上。
通过ctx
去match
匹配到的path
然后调用这个controller
方法。
简单的例子
在这个例子里面,通过装饰器,来实现绑定一个Controller
方法到路由上。
首先如上所说的,有以下思路:
- 装饰器记录
Controller
元数据,实现一个 Bind 方法,取出元数据绑定到路由上
实现一个装饰器Router(path)
用来装饰Controller
的方法
1 | import * as koa from "koa"; |
来实现bind
方法和Router
装饰器
首先是Router
装饰器
1 | function Router(path) { |
使用Reflect.metadata
需要在程序的开始import "reflect-metadata";
首先是bind
1 | function bind(router, controller) { |
这里的bind
也很简单,首先是,装饰器装饰一个方法的target
是类的原型,所以这边getMetadata
的target
应该是controller.prototype
,meta 的属性path
对应的是/hello
name
对应的是sayHello
,然后就是实例化controller
,然后通过 router 去绑定这个path
和方法。
打开例子在右边的浏览器输入/hello
就能看到say hello
的输出。
进入正题
进入正题,开始封装一个不是那么完整的装饰器框架。
先定义一堆的constants
1 | export enum METHODS { |
请求方法
首先是各种请求方法GET
POST
PUT
DELETE
因为现在有了请求方法的区分,所以在收集信息的时候需要加一个字段。
现在收集信息的方法变为
1 | import { METHODS, PATH } from "./constants"; |
可以看见,多了一个verb
参数表示该controller
的请求方法
这边用数组是因为,target
只有这个controller
要记录的信息不止一个有很多。
通过这个基础方法,再封装一下其他装饰器
1 | export function ALL(path: string) { |
装饰器写完,这里的bind
应该和之前的不一样,毕竟metadata
是个数组,处理起来其实没有区别,加个循环罢了。
1 | import * as Router from "koa-router"; |
这里的pathMeta
的输出:
1 | [ { name: 'sayHello', verb: 'get', path: '/hello' }, |
点开例子右边的浏览输入/get
就能预览得到,控制台也打印出来上面的输出。
请求参数
请求方法处理完了,处理一下请求参数。
举个例子
1 | getUser('id') id) { () user, ( |
想要的是,这个user
参数自动变成ctx.body
, id
变为ctx.params.id
。
如上,绑定路由的时候,controller
的参数是传进去的,并且,在装饰器对函数参数进行装饰的时候,可以通过descriptor
获得到这个参数在所有参数里面的第几个位置。所以通过这些特性,可以实现想要的需求。
只要把bind
方法改写成:
1 | instance[name](arg1, arg2, arg3); |
所有能从 ctx 中获取到的,都可以ctx.body
ctx.params
ctx.query
同样的,实现一个基础方法,叫做Inject
来收集参数的信息
1 | export function Inject(fn: Function) { |
这里的的fn
必须是个函数,因为需要通过请求的ctx
拿到需要的值。这里的index
是该变量在参数中的位置。
实现了Inject
接下来继续实现其他的装饰器
1 | export function Ctx() { |
这些装饰器都很简单,都是基于Inject
,这个装饰器的函数会先收集起来,后面会用到。
通过自己实现的bind
函数可以很容易的把需要的参数传入到controller
中
看一下修改以后的bind
函数
1 | import * as Router from "koa-router"; |
args
先filter
出这个controller
方法有关的参数,再根据这些参数的index
排序,排序以后就是args[i]
的 fn 函数ctx => ctx.xxx
的形式,通过执行fn(ctx)
可以拿到需要的值。
最后执行controller
的时候把这些值传入,就得到了想要的结果。
所以上面bind
函数的args
就是通过装饰器得到的所需要的参数。
这样来使用它们:
1 | import { GET, PUT, DEL, POST, Ctx, Param, Body } from "../src"; |
当请求进入sayHello
绑定的路由的时候, sayHello
会被执行,并且会传入以下参数执行。
sayHello(ctx, ctx.params['id'], ctx.query['name'], ctx)
至此,就封装出了一个很简陋的装饰器风格的框架。
可以在右边的浏览地址输入123?name=chs97
可以看到hello123chs97
总结
装饰器还可以做很多事情,在这里主要使用装饰器来记录一些信息,然后通过其他方法获取这些信息出来,进行处理。
装饰器风格的框架可以参考nestjs
这是一个完全装饰器风格的框架,和Sprint boot
非常像,可以尝试体验一下。
还有一些装饰器风格的库:
- Typeorm装饰器风格的
ORM
框架 - routing-controllers 装饰器风格的框架可以使用
express
和koa
做底层 - trafficlight装饰器风格的框架,底层为 koa
- nestjs一个非常棒的 node 框架,开发体验非常好