# 装饰器的使用

# 装饰器实现路由

我们经常可以看到如下装饰器的用法:

@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 请求装饰器编写完成,下面一口气完成所有请求方法的装饰器编写

我们需要以此编写 GETPOSTPUTDELETE 的装饰器

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#参数装饰器

以此定义 QueryParamBodyReqRes 参数装饰器

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,现在再去试试 ParamQuery 等方法发现可以正常获取到参数,我们至此完成了一些很粗糙的装饰器来辅助我们的开发

更新时间: 11/24/2020, 10:17:53 AM