前言
熟悉java的小伙伴都知道, 可以通过以下形式定义路由
@Slf4j @Controller @RequestMapping(value = "/user") public class HomeController { @RequestMapping(value = "/list", method = RequestMethod.GET) @ResponseBody ... @RequestMapping(value = "/update", method = RequestMethod.POST) @ResponseBody ... } 复制代码
但作为前端developer,我们应该很少亲自写java代码,更多接触的是nodejs,在nodejs的相关框架中,也有类似的写法,比如 Nest.js ,通过不同的 装饰器 也可以实现相同的效果
import { Controller, Get, Post, Body, Query } from '@nestjs/common'; import { UserDto } from './dto/user.dto'; @Controller('user') export class UserController { @Get('list') async list(@Query('id') id: number) { ... } @Post('update') async update(@Body() userDto: UserDto) { ... } } 复制代码
But, 萝卜青菜,各有所爱,有不少小伙伴喜欢用 egg.js ,在egg应用中,我们要通常这样定义一个路由
import { Application, IController, Router } from 'egg'; export default (app: Application) => { const controller: IController = app.controller; const router: Router = app.router; router.get('/user/list', controller.user.list); router.post('/user/update', controller.user.update); ... }; 复制代码
这样的写法虽然比较清晰,但是每当我们在controller中定义一个方法,都要在router中定义一个路由,但对于重度懒癌患者的我来说,还是有点儿不够简洁
实现
于是想能不能想Nestjs那样,通过装饰器进行路由注册呢,于是乎,就有了下面的代码(水平有限,代码可能有点儿糟糕:cold_sweat:)
// app/router.ts import { Application, Context } from 'egg'; import 'reflect-metadata'; import DocumentRouter from './router/document.router'; const CONTROLLER_PREFIX: string = 'CONTROLLER_PREFIX'; const methodMap: Map<string, any> = new Map<string, any>(); const rootApiPath: string = ''; interface CurController { pathName: string; fullPath: string; } /** * controller 装饰器,设置api公共前缀 * @param pathPrefix {string} * @constructor */ export const SelfController = (pathPrefix?: string): ClassDecorator => (targetClass): void => { // 在controller上定义pathPrefix的元数据 // https://github.com/rbuckton/reflect-metadata Reflect.defineMetadata(CONTROLLER_PREFIX, pathPrefix, targetClass); }; const methodWrap = (path: string, requestMethod: string): MethodDecorator => (target, methodName): void => { // 路由装饰器参数为空时,路由为方法名 const key = path ? `${requestMethod}·${path}·${String(methodName)}` : `${requestMethod}·${String(methodName)}·/${String(methodName)}`; methodMap.set(key, target); }; // Post 请求 export const Post = (path: string = ''): MethodDecorator => methodWrap(path, 'post'); // Get 请求 export const Get = (path: string = ''): MethodDecorator => methodWrap(path, 'get'); export default (app: Application): void => { const { router } = app; // 遍历methodMap, 注册路由 methodMap.forEach((curController: CurController, configString: string) => { // 请求方法, 请求路径, 方法名 const [ requestMethod, path, methodName ] = configString.split(`·`); // 获取controller装饰器设置的公共前缀 // 如果controller没有添加SelfController装饰器,则取文件名作为路径 let controllerPrefix: string | undefined | null = Reflect.getMetadata(CONTROLLER_PREFIX, curController.constructor); if (!Reflect.hasMetadata(CONTROLLER_PREFIX, curController.constructor)) { controllerPrefix = `/${curController.pathName.split(`.`).reverse()[0]}`; } const wrap: (this: Context, ...args: any[]) => Promise<any> = async function (...args: any[]): Promise<any> { return new (curController.constructor as any)(this)[methodName](...args); }; // 注册路由 router[requestMethod](rootApiPath + controllerPrefix + path, wrap); }); // 其他路由 DocumentRouter(app); router.post('/dingTalk', controller.message.dingTalkRobot); }; 复制代码
使用
// app/controller/user.ts import { Controller } from 'egg'; import { SelfController, Get, Post } from '../router'; @SelfController('/user') export default class UserController extends Controller { @Get() async list() { const { ctx } = this; ... } @Post('/update') public async update() { const { ctx } = this; ... } } 复制代码
然后启动服务,就可以通过 http://127.0.0.1:7001/user/list
和 http://127.0.0.1:7001/user/update
请求了
小结:
controller
时,通过 Reflect.defineMetadata
设置同一个 controller
下不同方法的公共前缀 controller
中声明方法时,通过方法装饰器指定接口路径,若方法装饰器参数为空时,则使用方法名为接口路径 controller
中只使用方法装饰器,未使用 controller
装饰器 SelfController
时,则是由文件名作为同一个 controller
下不同方法的公共前缀