# 装饰器的使用
# 装饰器实现路由
我们经常可以看到如下装饰器的用法:
@Controller
class Test {
@GET('/login')
login() {
console.log('调用login接口');
}
@GET('/logout')
logout() {
console.log('调用logout接口');
}
}
那我们如何利用 Reflect + 装饰器来实现路由的改写呢?
上章节简单介绍了 Reflect 的用法、可以通过 Reflect 给类或者方法设置一些元数据、我们也简单写了一个 get 的装饰器 传入了 path、并在类装饰器成功的获取到了,现在我们在上面继续改造一下:
import 'reflect-metadata'
import Router from 'koa-router'
export const router = new Router();
export function Controller(target: any) {
for (const key in target.prototype) {
const handle = target.prototype[key]
const path = Reflect.getMetadata('path', target.prototype, key)//获取存的路径
if (path) {
router.get(path, handle)
}
}
}
export function GET(path: string) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('path', path, target, propertyKey)
}
}
我们判断有没有 path 属性、如果有就调用 Router 路由去加载、我们通常调用路由的方式也需要修改一下
export const router = new Router();
router.get('/getData', checkLogin, (req: BodyRequest, res: Response) => {
...
});
修改成下面装饰器的写法
import { Controller, GET } from './decorator'
@Controller
class Test {
@GET('/login')
login() {
console.log('调用登录接口');
}
@GET('/logout')
logout() {
console.log('调用logout接口');
}
}
有没有觉得代码一丝丝的清爽呢?前面也讲了,类装饰器会在类定义的时候就会执行。OK,现在去主入口那里加载一下:
import './controller/LoginController'
// 装饰器文件导出的 router
import { router } from './controller/decorator'
const app = new koa()
app.use(router.routes())
启动程序、发现接口也都是正常的运行。我们一个基本的 get 请求装饰器编写完成,下面一口气完成所有请求方法的装饰器编写
我们需要以此编写 GET、POST、PUT、DELETE 的装饰器
import 'reflect-metadata'
import Router from 'koa-router'
export const router = new Router();
enum Method {
get = 'get',
post = 'post',
put = 'put',
delete = 'delete'
}
export function Controller(target: any) {
for (const key in target.prototype) {
const handle = target.prototype[key]
const path = Reflect.getMetadata('path', target.prototype, key)
const method: Method = Reflect.getMetadata('method', target.prototype, key)
const handler = target.prototype[key];
if (path && method && handler) {
router[method](path, handle)
}
}
}
function getRequestDecorator(type: string) {
return function (path: string) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('path', path, target, propertyKey)
Reflect.defineMetadata('method', type, target, propertyKey)
}
}
}
export const GET = getRequestDecorator('get');
export const POST = getRequestDecorator('post');
export const PUT = getRequestDecorator('put');
export const DEL = getRequestDecorator('delete');
实现还是很简单的、通过一个工厂函数返回不同的 装饰器,然后 router 使用不同的 method 请求方式即可
# 中间件装饰器
前面讲了如何使用装饰器以及多个装饰器、按顺序写就行了。那我现在想在装饰器里面使用中间件呢、如下伪代码那样:
function checkLogin(ctx,next)=>{
}
@use(checkLogin)
getData() {
。。。
}
一看这代码感觉就有那味了、装饰器里面使用中间件,不用说,我们肯定需要利用 Reflect 添加一个use 元属性
export function use(middleware: Router.IMiddleware) {
return function (target: any, key: string) {
Reflect.defineMetadata('middleware', middleware, target, key);
};
}
然后在类 Controller 给增强一下:
...
const handler = target.prototype[key];
const middleware = Reflect.getMetadata('middleware', target.prototype, key);
if (path && method && handler) {
if (middleware) {
router[method](path, middleware, handle)
} else {
router[method](path, handle)
}
}
OK,在 checkLogin 中间件写点东西
const checkLogin = (ctx: Context, next: Next) => {
if (false) {
next()
} else {
console.log('中间件拦截');
}
}
运行程序、发现中间件已经起了作用
# 使用多个中间件
在上面我们使用了一个中间件 checkLogin,那想使用多个中间件该怎么实现呢?如下:
@GET('/logout')
@use(checkLogin)
@use(test)
logout() {
console.log('调用logout接口');
}
在前面我们使用一个中间方式如下:
router[method](path, middleware, handle)
我们通过类型申明可以得知 router 可以接受多个中间件就像这样
get(
name: string,
path: string | RegExp,
...middleware: Array<Router.IMiddleware<StateT, CustomT>>
): Router<StateT, CustomT>;
因此我们只需要把 controller 那里使用一个中间件改成多个就行了、代码改写下:
const middlewares: Router.IMiddleware[] = Reflect.getMetadata('middlewares', target.prototype, key);
if (path && method && handler) {
if (middlewares && middlewares.length) {
router[method](path, ...middlewares, handle)
} else {
router[method](path, handle)
}
}
现在我们只需要把 元数据 middlewares 修改成数组即可:
export function use(middleware: Router.IMiddleware) {
return function (target: any, key: string) {
const originMiddlewares = Reflect.getMetadata('middlewares', target, key) || []
originMiddlewares.push(middleware)
Reflect.defineMetadata('middlewares', originMiddlewares, target, key);
};
}
现在再去测试:
const test = (ctx: Context, next: Next) => {
console.log('我是test中间件');
next()
}
@GET('/logout')
@use(checkLogin)
@use(test)
logout() {
console.log('调用logout接口');
}
现在我们所编写的两个中间件都生成了
# 给接口添加前缀
对于同一模块、接口同一模块前缀 prefix 应该一样,我们现在实现这个功能
@controller('/user')
export class LoginController {
@post('/login')
login(): void {
}
@get('/logout')
logout(): void {
}
}
在 controller 上添加了 prefix,所有的接口都会添加这个前缀。很明显我们要用到之前讲的 装饰器工厂
export default function controller(prefix: string) {
return function (target: new (...args: any[]) => any) {
for (const key in target.prototype) {
let path: string = Reflect.getMetadata('path', target.prototype, key)
const method: Method = Reflect.getMetadata('method', target.prototype, key)
const handler = target.prototype[key];
const middlewares: Router.IMiddleware[] = Reflect.getMetadata('middlewares', target.prototype, key);
path = prefix === '/' ? path : `${prefix}${path}`
if (path && method && handler) {
if (middlewares && middlewares.length) {
router[method](path, ...middlewares, handler)
} else {
router[method](path, handler)
}
}
}
}
}
ok,现在再去访问,发现在 controller 添加的前缀在该模块全部生效了
# 获取参数中间件
我们获取一般都会使用 koa-bodyparser 来通过 ctx.request 来获取,如何通过编写中间件实现下面的参数读取
@GET('/login')
login(@Query("age") age: number, @Query("name") name: string, ctx: Context) {
console.log(age, name);
ctx.body = `调用登录接口`
}
这里得使用到前面说的 /blog/ts/装饰器.html#参数装饰器
以此定义 Query,Param,Body,Req,Res 参数装饰器
export const Query = (arg: string) => getParamDecorator((ctx: Context) => ctx.query[arg]);
export const Param = (arg: string) => getParamDecorator((ctx: Context) => ctx.params[arg]);
export const Body = () => getParamDecorator((ctx: Context) => ctx.request.body);
export const Req = () => getParamDecorator((ctx: Context) => ctx.req);
export const Res = () => getParamDecorator((ctx: Context) => ctx.res);
接下来就是 getParamDecorator 方法,它是一个装饰器工厂
function getParamDecorator(fn: any) {
return function (target: any, name: string, paramIndex: number) {
const preMetadata: Array<paramType> = Reflect.getMetadata('param', target, name) || [];
preMetadata.push({
name,
fn,
index: paramIndex,
})
Reflect.defineMetadata('param', preMetadata, target, name)
}
}
通过 Reflect 增加了一个 param,并把多个装饰器组装成一个数组,
注意参数顺序问题
通过元编程我们已经把 装饰器都添加上去了,现在只需要让每个装饰器 fn 执行,返回对应的参数即可。现在去改造下 controller
let params: Array<paramType> = Reflect.getMetadata('param', target.prototype, key) || []
// 根据传入的参数顺序进行排序
if (params.length) {
params = params.sort((a: paramType, b: paramType) => a.index - b.index);
}
现在让 handler 执行,并以此传入参数
router[method](path, ...middlewares, (ctx: Context) => {
const args = params.map(item => item.fn(ctx))
handler(...args, ctx)
})
ok,现在再去试试 Param,Query 等方法发现可以正常获取到参数,我们至此完成了一些很粗糙的装饰器来辅助我们的开发
← 装饰器 Redux | 实现原理 →