背景
在egg中,通常是这么写路由的
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router } = app;
// 摆大巴
router.get('/', controller.home.index);
router.get('/user', controller.home.getUser);
router.get('/users', controller.home.getUsers);
router.put('/user', controller.home.updateUser);
router.post('/user', controller.home.addUser);
router.delete('/user', controller.home.deleteUser);
// ...
};
由于产品迭代,接口多了,router.ts就乱了,模块区分不显著。写个接口,还要切文件,很不舒服。
改造后
router.ts长这样,Bootstrap方法作为入口,全部的接口去中心化,提高接口开发效率
import { Application } from 'egg'
import { Bootstrap } from './owlet'
export default (app: Application) => {
Bootstrap(app, { prefix: '/owlet' })
}
controller长这样
import { Context, Controller } from 'egg'
import { BizCode, BizMsg } from '../bizconfig/http.status'
import { Get, Post } from '../owlet/decorator/http'
import { Prefix } from '../owlet/decorator/router'
@Prefix('user')
export default class UserController extends Controller {
/**
* 示例:Get
*/
@Get('/ddd')
async d() {
const { ctx } = this
ctx.body = { code: BizCode.OK, msg: BizMsg.OK, data: 123 }
}
/**
* 示例:Post
*/
@Post('/dto')
async f() {
const { ctx } = this
ctx.body = { code: BizCode.OK, msg: `owlet` }
}
/**
* 示例:路由拦截
*/
@Get('/p', (ctx: Context) => {
console.log('忍法!水遁·水阵壁', ctx.request.url)
return false
})
async p() {
const { ctx } = this
ctx.body = '你将不会看见返回值'
}
}
曾经我也忍不住打开npmjs.com,搜索egg-decorator-xxx,试图直接找个轮子,但毕竟不是自己写的,非常不舒服,一种被克制的感觉。
准备工作:创建egg-ts空项目
# 全局安装 egg-init cli
npm i egg-init -g
# 初始化项目,选择 ts - Simple egg && typescript app boilerplate
egg-init owlet
# 装依赖,启项目 http://localhost:7001
cd owlet & yarn & yarn dev
趁着终端跑命令,干掉一些默认文件,app文件夹下创建相关文件,结构如下:
├── controller
│ └── demo.ts
├── owlet
│ ├── decorator.ts
│ └── index.ts
└── router.ts
owlet/index.ts中,暴露一个Bootstrap方法
// owlet/index.ts
import { Application } from "egg";
/**
* @param {string} prefix 项目全局api前缀
*/
interface IOptions {
prefix?: string;
}
export const Bootstrap = (app: Application, options?: IOptions) => {
//TODO
console.log(app, options);
};
在router.ts中引入
// router.ts
import { Application } from "egg";
import { Bootstrap } from "./owlet";
export default (app: Application) => {
Bootstrap(app, { prefix: "/owlet" });
};
实现Get方法装饰器
有关方法装饰器,此处略....装饰器基础知识 • 官方文档
基本思路:
- 造个Get装饰器
- Get装饰器收集控制器方法元信息
- 注册收集的元信息到
egg路由对象中
废话不多说,直接开搞
第一步:造Get方法装饰器
在owlet/decorator.ts中写入
/**
* @param {string} path api 前缀
*/
export const Get = (path: string): MethodDecorator => {
/**
* @param {BaseContextClass} target 方法装饰所属类
* @param {string} 控制器方法名
*/
return (target: any, handlerName: any) => {
const proto = target.constructor;
// 打印看下收集到了什么...有惊喜
console.log(path, target, proto, handlerName);
};
};
然后在app/controller/demo.ts中使用装饰器
import { Controller } from "egg";
import { Get } from "../owlet/decorator";
export default class DemoController extends Controller {
@Get("/hi")
public async index() {
const { ctx } = this;
ctx.body = 123;
}
}
Get装饰器参数和demo.ts对应关系如下图,画的比较乱,辅助理解:
此时我看了控制台,打印出了console.log(path,target,proto,handlerName)信息,我陷入了沉思,为什么控制台会打印?
查了下egg文档,有这么一个说法
所有的 Controller 文件都必须放在
app/controller目录下,可以支持多级目录,访问的时候可以通过目录名级联访问。Controller 支持多种形式进行编写,可以根据不同的项目场景和开发习惯来选择。
于是,我猜egg生命周期中,应该是会自动去递归扫描controller下面的全部文件,然后根据commonjs规范,挨个require(xxx.controler.ts)解析一下
基于此,甭管对不对,那就太舒服了,省去了咱们手动扫描controller文件夹的麻烦!
回到最初的起点,一个基本的路由,长这样:
router.get('/', controller.home.index);
翻译一下
router.get(api前缀, 类.类名.方法名)
这些『元信息』Get装饰器都能拿的到,太好了!赶紧造个容器,收集一下。
第二步:Get方法装饰器收集元信息
新建owlet/hub.ts增加 routeHub用来存储Get装饰器收集到的元信息
type ThttpMethod = "GET" | "POST";
interface IRouteMeta {
clazz: any;
clazzName: string;
httpMethodType: ThttpMethod;
handlerName: string;
}
interface IRouteHub {
[key: string]: IRouteMeta;
}
// 收集Get、Post等http请求装饰器
export const routeHub: IRouteHub = {};
然后回到 owlet/decorator.ts,在Get装饰器里引入routeHub对象收集一下
import { routeHub } from "./hub";
/**
* @param {string} path api 前缀
*/
export const Get = (path: string): MethodDecorator => {
/**
* @param {BaseContextClass} target 方法装饰所属类
* @param {string} 控制器方法名
*/
return (target: any, handlerName: any) => {
const proto = target.constructor;
// 把元信息存到routeHub中,key就是接口 api 前缀
routeHub[path] = {
clazz: proto,
clazzName: proto.name,
httpMethodType: "GET",
handlerName,
};
};
};
最后,在之前咱们声明的入口Bootstrap函数,打印下routeHub
// owlet/index.ts
import { Application } from "egg";
import { routeHub } from "./hub";
/**
* @param {string} prefix 项目全局api前缀
*/
interface IOptions {
prefix?: string;
}
export const Bootstrap = (app: Application, options?: IOptions) => {
// ...
console.warn(routeHub)
};
输出
{
'/index': {
clazz: [class DemoController extends BaseContextClass],
clazzName: 'DemoController',
httpMethodType: 'GET',
handlerName: 'index'
}
}
第三步:路由注册元信息
这一步就非常轻松了,在Bootstrap入口方法注册元信息到egg的router对象中
// owlet/index.ts
import { Application, Context } from "egg";
import { routeHub } from "./hub";
/**
* @param {string} prefix 项目全局api前缀
*/
interface IOptions {
prefix?: string;
}
export const Bootstrap = (app: Application, options?: IOptions) => {
const { router } = app;
// 1. 项目全局api 前缀
const globalPrefix = options?.prefix;
// 2. 注册路由
Object.keys(routeHub).forEach((apiPrefix: string) => {
const { clazz, httpMethodType, handlerName } = routeHub[apiPrefix];
// 拼接完成的 api 前缀
const fullPrefix = globalPrefix + apiPrefix;
// 使用 router 对象添加 route
router[httpMethodType.toLowerCase()](fullPrefix, async (ctx: Context) => {
// 类的示例构造方法传入ctx对象
const instance = new clazz(ctx);
// 类的成员方法同步执行
await instance[handlerName]();
});
});
};
Post装饰器
思路同Get装饰器完全一样,默念口诀:
- 造个装饰器
- 收集元信息
- 批量注册到路由
Prefix(Controller)装饰器
类装饰器实现上有细微不同,思路仍然同上
结语
上述代码纯属虚构,只是简单实现,希望可以帮到你。不足之处,轻喷。