装饰器(Decorator)是用来修改类行为的一个函数(语法糖),在许多面向对象语言中都有这个东西。node
装饰器是一个函数,接受3个参数target
name
descriptor
git
定义一个装饰器函数github
function setName(target, name, descriptor) {
target.prototype.name = 'hello world'
}
@setName
class A {
}
console.log((new A()).name) // hello world
复制代码
装饰器装饰不一样类型的目标是有一些差别的,这些差别体如今装饰函数接受的参数里面。typescript
首先对一个类的装饰是由内到外的,先从类的属性开始,从上到下,按顺序修饰,若是类的属性是个方法,那么会先装饰这个方法的属性,再装饰这个方法。如上demo的consoleexpress
装饰函数接收到的参数 target
是类的自己,name
与descriptor
都是undefined
api
装饰函数接收到的参数target
是类的原型,也就是class.prototype
数组
name
为该属性的名字浏览器
当这个属性是个函数时:bash
descriptor
为该方法的描述,经过Object.getOwnPropertyDescriptor(obj, prop)
得到数据结构
当这个属性非函数时:
descriptor
为undefined
装饰函数接受到的参数target
是类的原型
name
为该参数的名字
descriptor
为该参数是这个函数的第几个参数,index:number
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-router
就是个中间件。
路由其实就是映射一个controller
方法到一个path
字符串上。
经过ctx
去match
匹配到的path
而后调用这个controller
方法。
在这个例子里面,经过装饰器,来实现绑定一个Controller
方法到路由上。
首先如上所说的,有如下思路:
Controller
元数据,实现一个Bind方法,取出元数据绑定到路由上实现一个装饰器Router(path)
用来装饰Controller
的方法
import * as koa from "koa";
import * as router from "koa-router";
const koaRouter = new router();
const app = new koa();
function Router(path) {
return function(target, name) {};
}
function bind(router, controller) {}
class Controller {
@Router("/hello")
sayHello(ctx) {
ctx.body = "say hello";
}
}
bind(koaRouter, Controller);
app.use(koaRouter.routes());
app.listen(8080);
复制代码
来实现bind
方法和Router
装饰器
首先是Router
装饰器
function Router(path) {
return function(target, name) {
Reflect.defineMetadata("path", { path, name }, target);
};
}
// 装饰器若是须要传参得再装饰器上层封装一个函数,而后再返回这个装饰器函数
复制代码
使用Reflect.metadata
须要在程序的开始import "reflect-metadata";
首先是bind
function bind(router, controller) {
const meta = Reflect.getMetadata("path", controller.prototype);
console.log(meta);
const instance = new controller();
router.get(meta.path, ctx => {
instance[meta.name](ctx);
});
}
复制代码
这里的bind
也很简单,首先是,装饰器装饰一个方法的target
是类的原型,因此这边getMetadata
的target
应该是controller.prototype
,meta的属性path
对应的是/hello
name
对应的是sayHello
,而后就是实例化controller
,而后经过router去绑定这个path
和方法。
打开例子在右边的浏览器输入/hello
就能看到say hello
的输出。
进入正题,开始封装一个不是那么完整的装饰器框架。
先定义一堆的constants
export enum METHODS {
GET = 'get',
POST = 'post',
PUT = 'put',
DEL = 'del',
ALL = 'all'
}
export const PATH = 'DEC_PATH'
export const PARAM = 'DEC_PARAM'
复制代码
首先是各类请求方法GET
POST
PUT
DELETE
由于如今有了请求方法的区分,因此在收集信息的时候须要加一个字段。
如今收集信息的方法变为
import { METHODS, PATH } from "./constants";
export function Route(path: string, verb: METHODS) {
return function(target, name, descriptor) {
const meta = Reflect.getMetadata(PATH, target) || []
meta.push({
name,
verb,
path
})
Reflect.defineMetadata(PATH, meta, target)
}
}
复制代码
能够看见,多了一个verb
参数表示该controller
的请求方法
这边用数组是由于,target
只有这个controller
要记录的信息不止一个有不少。
经过这个基础方法,再封装一下其余装饰器
export function ALL(path: string) {
return Route(path, METHODS.ALL)
}
export function GET(path: string) {
return Route(path, METHODS.GET)
}
export function POST(path: string) {
return Route(path, METHODS.POST)
}
export function PUT(path: string) {
return Route(path, METHODS.PUT)
}
export function DEL(path: string) {
return Route(path, METHODS.DEL)
}
复制代码
装饰器写完,这里的bind
应该和以前的不同,毕竟metadata
是个数组,处理起来其实没有区别,加个循环罢了。
import * as Router from 'koa-router'
import * as Koa from 'koa'
import { PATH } from './constants';
export function BindRoutes(koaRouter: Router, controllers: any[]) {
for(const ctrl of controllers) {
const pathMeta = Reflect.getMetadata(PATH, ctrl.prototype) || []
console.log(pathMeta)
const instance = new ctrl()
for(const item of pathMeta) {
const { path, verb, name } = item
koaRouter[verb](path, (ctx: Koa.Context) => {
instance[name](ctx)
})
}
}
}
复制代码
这里的pathMeta
的输出:
[ { name: 'sayHello', verb: 'get', path: '/hello' },
{ name: 'postMessage', verb: 'post', path: '/post' },
{ name: 'putName', verb: 'put', path: '/put' },
{ name: 'delMe', verb: 'del', path: '/del' } ]
复制代码
点开例子右边的浏览输入/get
就能预览获得,控制台也打印出来上面的输出。
请求方法处理完了,处理一下请求参数。
举个例子
getUser(@Body() user, @Param('id') id) {
}
复制代码
想要的是,这个user
参数自动变成ctx.body
, id
变为ctx.params.id
。
如上,绑定路由的时候,controller
的参数是传进去的,而且,在装饰器对函数参数进行装饰的时候,能够经过descriptor
得到到这个参数在全部参数里面的第几个位置。因此经过这些特性,能够实现想要的需求。
只要把bind
方法改写成:
instance[name](arg1, arg2, arg3)
// arg1 = ctx.body
// arg2 = ctx.params.id
// arg3 = .....
复制代码
全部能从ctx中获取到的,均可以ctx.body
ctx.params
ctx.query
一样的,实现一个基础方法,叫作Inject
来收集参数的信息
export function Inject(fn: Function) {
return function(target, name, descriptor) {
const meta = Reflect.getMetadata(PARAM, target) || []
meta.push({
name,
fn,
index: descriptor
})
Reflect.defineMetadata(PARAM, meta, target)
}
}
复制代码
这里的的fn
必须是个函数,由于须要经过请求的ctx
拿到须要的值。这里的index
是该变量在参数中的位置。
实现了Inject
接下来继续实现其余的装饰器
export function Ctx() {
return Inject(ctx => ctx)
}
export function Body() {
return Inject(ctx => ctx.request.body)
}
export function Req() {
return Inject(ctx => ctx.req)
}
export function Res() {
return Inject(ctx => ctx.res)
}
export function Param(arg) {
return Inject(ctx => ctx.params[arg])
}
export function Query(arg) {
return Inject(ctx => ctx.query[arg])
}
复制代码
这些装饰器都很简单,都是基于Inject
,这个装饰器的函数会先收集起来,后面会用到。
经过本身实现的bind
函数能够很容易的把须要的参数传入到controller
中
看一下修改之后的bind
函数
import * as Router from 'koa-router'
import * as Koa from 'koa'
import { PATH, PARAM } from './constants';
export function BindRoutes(koaRouter: Router, controllers: any[]) {
for(const ctrl of controllers) {
const pathMeta = Reflect.getMetadata(PATH, ctrl.prototype) || []
const argsMeta = Reflect.getMetadata(PARAM, ctrl.prototype) || []
console.log(argsMeta)
const instance = new ctrl()
for(const item of pathMeta) {
const { path, verb, name } = item
koaRouter[verb](path, (ctx: Koa.Context) => {
const args = argsMeta.filter(i => i.name === name).sort((a, b) => a.index - b.index).map(i => i.fn(ctx))
instance[name](...args, ctx)
})
}
}
}
复制代码
args
先filter
出这个controller
方法有关的参数,再根据这些参数的index
排序,排序之后就是args[i]
的fn函数ctx => ctx.xxx
的形式,经过执行fn(ctx)
能够拿到须要的值。
最后执行controller
的时候把这些值传入,就获得了想要的结果。
因此上面bind
函数的args
就是经过装饰器获得的所须要的参数。
这样来使用它们:
import { GET, PUT, DEL, POST, Ctx, Param, Body } from "../src";
export class Controller {
@GET('/:id')
sayHello (@Ctx() Ctx, @Param('id') id, @Query('name') name) {
Ctx.body = 'hello' + id + name
}
@POST('/post')
postMessage(@Body() body, ctx) {
console.log(body)
ctx.body = 'post'
}
}
复制代码
当请求进入sayHello
绑定的路由的时候, sayHello
会被执行,而且会传入如下参数执行。
sayHello(ctx, ctx.params['id'], ctx.query['name'], ctx)
至此,就封装出了一个很简陋的装饰器风格的框架。
能够在右边的浏览地址输入123?name=w2fzu
能够看到hello123w2fzu
装饰器还能够作不少事情,在这里主要使用装饰器来记录一些信息,而后经过其余方法获取这些信息出来,进行处理。
装饰器风格的框架能够参考nestjs
这是一个彻底装饰器风格的框架,和Sprint boot
很是像,能够尝试体验一下。
还有一些装饰器风格的库:
ORM
框架express
和koa
作底层