最近在搞 nest.js,学习装饰器想起,以前刚接触 koa/experss 时,就想着怎么能把路由写的优雅一点
不想写一堆的router.get('/', (ctx, next) => {});
当时只是简单的把文件分开
直到用了nest.js,如果在 koa/express 中也实现路由装饰器,岂不是也很好,就在此简单记录一下,koa 中实现路由装饰器
先准备一个极简的 ts+koa 的基础项目
新建 koa-adorner 文件夹
初始化项目
npm init
npm i typescript ts-node-dev tslint @types/node -D
npm i koa koa-router
"start": "ts-node-dev ./src/index.ts -P tsconfig.json --no-cache"
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"moduleResolution": "node",
"experimentalDecorators": true, // 使用装饰器
"allowSyntheticDefaultImports": true,
"target": "es2017",
"lib": ["es2015"],
"outDir": "./dist",
"typeRoots": ["./node_modules/@types"]
},
"include": ["src/**/*"]
}
此时启动项目,访问 3000 端口可以看到响应了 get
import * as Koa from "koa";
import * as Router from "koa-router";
const app = new Koa();
const router = new Router(); // 实例化路由
router.get("/", async (ctx: Koa.Context) => {
ctx.body = "get";
});
router.post("/", async (ctx: Koa.Context) => {
ctx.body = "post";
});
app.use(router.routes());
app.listen(3000, () => {
console.log("The service starts at port 3000");
});
在实际项目中,router 是复杂的,可能在 n 多个文件,然后 n 多个 router.get
而我们现在就是要消灭掉并且实现多文件自动加载,优雅得书写我们的路由文件
/src/utils/decorator.ts
import * as glob from "glob";
// 路由在此实例化
const router = new KoaRouter();
// 自动注册 {folder} 文件下的所有路由
export const load = (folder: string, options: LoadOptions = {}): KoaRouter => {
const extname = options.extname || ".{js,ts}";
glob
.sync(require("path").join(folder, `./**/*${extname}`))
.forEach((item) => require(item));
return router;
};
/src/index.ts
注释原有的 router,使用 load 导入 src/routes 下的路由文件
import * as Koa from "koa";
// import * as Router from "koa-router";
import { load } from "./utils/decorator";
import { resolve } from "path";
const app = new Koa();
// const router = new Router(); // 实例化路由
// router.get("/", async (ctx: Koa.Context) => {
// ctx.body = "get";
// });
// router.post("/", async (ctx: Koa.Context) => {
// ctx.body = "post";
// });
const router = load(resolve(__dirname, "./routes"), {
// extname: "",
});
app.use(router.routes());
app.listen(3001, () => {
console.log("The service starts at port 3000");
});
/src/routes/test.ts
此时我们期望这样书写即可完成路由的注册
其中@Controller("test")
类装饰器将作为路由的前缀,我们可以在此组织相关模块的代码
@get("list")
也就是说我们可以通过 get 请求访问/test/list
@post()
如果不传参数,就默认获取add
作为路径,也就是说我们可以通过 post 请求访问/test/add
import * as Koa from "koa";
import { Controller, get, post } from "../utils/decorator";
@Controller("test")
export default class testController {
@get("list")
public async list(ctx: Koa.Context) {
const data = [{ id: 1, name: "测试" }];
ctx.body = { ok: 1, data };
}
@post()
public async add(ctx: Koa.Context) {
const data = [{ id: 1, name: "测试" }];
ctx.body = { ok: 1, data };
}
}
/src/utils/decorator.ts
同样在 decorator 中,我们新增如下代码
import * as Koa from "koa";
import * as KoaRouter from "koa-router";
type LoadOptions = {
extname?: string,
};
type RouteOptions = {
prefix?: string,
middlewares?: Array<Koa.Middleware>,
};
type HTTPMethod = "get" | "post";
// Controller类装饰器 作用:给controller类添加路由前缀,当然愿意的话也可以不使用Controller类装饰器
export const Controller = (path = "") => {
// 给controller类添加路由前缀
return function(target) {
target.prefix = "/" + path;
};
};
// 方法装饰器
const decorate = (
method: HTTPMethod,
path: string,
options: RouteOptions = {},
router: KoaRouter
) => {
return (target, property: string, descriptor) => {
process.nextTick(() => {
const { prefix = "" } = target.constructor; //Controller前缀
const roterPath = "/" + (path || property); //去参数路径或者方法名作为路径
// 添加中间件数组
const middlewares = [];
if (options.middlewares) {
middlewares.push(...options.middlewares);
}
if (target.middlewares) {
middlewares.push(...target.middlewares);
}
middlewares.push(target[property]);
//router rul
const url = options.prefix
? prefix + options.prefix + roterPath
: prefix + roterPath;
router[method](url, ...middlewares);
});
};
};
// method多包一层
const method = (method: HTTPMethod) => (
path?: string,
options?: RouteOptions
) => decorate(method, path, options, router);
// 中间件 改天专门写一下
export const middlewares = (middlewares: Koa.Middleware[]) => {
return function(target: { prototype: { middlewares: any[] } }) {
target.prototype.middlewares = middlewares;
};
};
// 最终导出get、post ,此处以这两个为例
export const get = method("get");
export const post = method("post");
至此,我们在 koa 中已经简单得实现了路由装饰器,通过Controller
@get
等装饰器语法减去了重复繁琐的工作
也有利于我们组织代码与维护,其中跟多请求方式的,Controller 甚至根据类名来构建,请求数据校验等,可以根据业务进行完善,有时间再分享下 koa 的内容