装饰器优化koa路由
新建KOA项目
# 创建文件夹mkdir koa-decorator && cd koa-decorator# 初始化项目npm init -y# 初始化tstsc --init# 安装koa 和 koa-routernpm i koa koa-router reflect-metadata# 安装相关类型文件npm i @types/koa @types/koa-router -D# 全局安装辅助能力 nodemon 能够监听文件变化的时候重新执行, ts-node可以不用将ts转为js就可以执行npm i nodemon ts-node -g# 创建文件夹 mkdir koa-decorator && cd koa-decorator # 初始化项目 npm init -y # 初始化ts tsc --init # 安装koa 和 koa-router npm i koa koa-router reflect-metadata # 安装相关类型文件 npm i @types/koa @types/koa-router -D # 全局安装辅助能力 nodemon 能够监听文件变化的时候重新执行, ts-node可以不用将ts转为js就可以执行 npm i nodemon ts-node -g# 创建文件夹 mkdir koa-decorator && cd koa-decorator # 初始化项目 npm init -y # 初始化ts tsc --init # 安装koa 和 koa-router npm i koa koa-router reflect-metadata # 安装相关类型文件 npm i @types/koa @types/koa-router -D # 全局安装辅助能力 nodemon 能够监听文件变化的时候重新执行, ts-node可以不用将ts转为js就可以执行 npm i nodemon ts-node -g
初始化相关文件
- 在
koa-decorator
文件夹中新建src
文件夹 - 在根目录文件夹中新建
app.ts
作为项目入口文件
添加测试用例,保证项目正常运行
app.ts
import Koa from 'koa';import Router from "koa-router";// 实例化Koaconst app = new Koa();// 实例化Routerconst router = new Router();// 添加get路由router.get("/", async (ctx) => {ctx.body = "hello word";})// 添加路由“/post”router.get("/post", async (ctx) => {ctx.body = "post hello word";})// koa 加载 router插件app.use(router.routes());// 监听3000端口,如果运行成功,那么就会输出consoleapp.listen(3000, () => {console.log("server start: http://localhost:3000/")});import Koa from 'koa'; import Router from "koa-router"; // 实例化Koa const app = new Koa(); // 实例化Router const router = new Router(); // 添加get路由 router.get("/", async (ctx) => { ctx.body = "hello word"; }) // 添加路由“/post” router.get("/post", async (ctx) => { ctx.body = "post hello word"; }) // koa 加载 router插件 app.use(router.routes()); // 监听3000端口,如果运行成功,那么就会输出console app.listen(3000, () => { console.log("server start: http://localhost:3000/") });import Koa from 'koa'; import Router from "koa-router"; // 实例化Koa const app = new Koa(); // 实例化Router const router = new Router(); // 添加get路由 router.get("/", async (ctx) => { ctx.body = "hello word"; }) // 添加路由“/post” router.get("/post", async (ctx) => { ctx.body = "post hello word"; }) // koa 加载 router插件 app.use(router.routes()); // 监听3000端口,如果运行成功,那么就会输出console app.listen(3000, () => { console.log("server start: http://localhost:3000/") });
这里添加了两个路由
/
和/post
并在完成相关事宜后输出
server start: http://localhost:3000/
,这个很重要,不然运行起来没有反应,会觉得没有运行
添加指令,方便运行
"scripts": {"dev": "nodemon --watch src/**/* -e ts,js --exec ts-node app.ts"}"scripts": { "dev": "nodemon --watch src/**/* -e ts,js --exec ts-node app.ts" }"scripts": { "dev": "nodemon --watch src/**/* -e ts,js --exec ts-node app.ts" }
使用
nodemon
监听src文件夹
下的所有ts,js
文件如果变化,就执行指令
ts-node ./src/app.ts
现在运行指令
npm run dev
即可了
配置Ts支持装饰器能力
tsconfig.json
{"compilerOptions": {"target": "ES5","experimentalDecorators": true,"emitDecoratorMetadata": true,.....},....}{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true, ..... }, .... }{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true, ..... }, .... }
要使ts支持装饰器,需要在
tsconfig.json
添加以上配置这里主要是打开
experimentalDecorators
和emitDecoratorMetadata
experimentalDecorators
: 允许使用装饰器能力
emitDecoratorMetadata
:允许发射装饰器元数据
更改目录,符合工程化
每个功能尽量有单独的文件管理,以防止文件过多而导致最后项目单文件过大而无法维护,所以我们应该将router抽离放入一个单独的文件中,如果后期路由过多可能还需要进行分文件【但是我们使用装饰器就不用考虑路由的分文件,后面就能看到为什么】
- 在src根目录下新建
router.ts
文件 - 将
app.ts
文件中的路由部分移动到router.ts
文件中 - 新建controller文件夹,这个文件夹是为后面控制器的家
router.ts
import Router from "koa-router";// 实例化Routerconst router = new Router();// 添加get路由router.get("/", async (ctx) => {ctx.body = "hello word";})// 添加路由“/post”router.get("/post", async (ctx) => {ctx.body = "post hello word";})// 添加路由“/get”router.get("/get", async (ctx) => {ctx.body = "get hello word";})export default router;import Router from "koa-router"; // 实例化Router const router = new Router(); // 添加get路由 router.get("/", async (ctx) => { ctx.body = "hello word"; }) // 添加路由“/post” router.get("/post", async (ctx) => { ctx.body = "post hello word"; }) // 添加路由“/get” router.get("/get", async (ctx) => { ctx.body = "get hello word"; }) export default router;import Router from "koa-router"; // 实例化Router const router = new Router(); // 添加get路由 router.get("/", async (ctx) => { ctx.body = "hello word"; }) // 添加路由“/post” router.get("/post", async (ctx) => { ctx.body = "post hello word"; }) // 添加路由“/get” router.get("/get", async (ctx) => { ctx.body = "get hello word"; }) export default router;
app.ts
import Koa from 'koa';import router from './src/router';// 实例化Koaconst app = new Koa();// koa 加载 router插件app.use(router.routes());// 监听3000端口,如果运行成功,那么就会输出consoleapp.listen(3000, () => {console.log("server start: http://localhost:3000/")});import Koa from 'koa'; import router from './src/router'; // 实例化Koa const app = new Koa(); // koa 加载 router插件 app.use(router.routes()); // 监听3000端口,如果运行成功,那么就会输出console app.listen(3000, () => { console.log("server start: http://localhost:3000/") });import Koa from 'koa'; import router from './src/router'; // 实例化Koa const app = new Koa(); // koa 加载 router插件 app.use(router.routes()); // 监听3000端口,如果运行成功,那么就会输出console app.listen(3000, () => { console.log("server start: http://localhost:3000/") });
到此,项目基本就建立起来,可以实现我们的装饰器能力了
目录结构
|-- koa-decorator|-- app.ts|-- package-lock.json|-- package.json|-- tsconfig.json|-- src|-- router.ts|-- controller # 存放所有的controller|-- koa-decorator |-- app.ts |-- package-lock.json |-- package.json |-- tsconfig.json |-- src |-- router.ts |-- controller # 存放所有的controller|-- koa-decorator |-- app.ts |-- package-lock.json |-- package.json |-- tsconfig.json |-- src |-- router.ts |-- controller # 存放所有的controller
定义装饰器
import "reflect-metadata";// 装饰器类型export enum DecoratorKey {Controller = "controller",Method = "method",}// 请求类型export enum MethodTyp {Get = "get",POST = "post"}// 请求装饰器元数据类型export interface MethodMetadata {method: MethodTyp,route: string,fn: Function}/*** controller 只用于收集路由前缀* @param name 一般为文件名全小写,使用controller[name]能访问到该方法* @param prefix 路径前缀,选填,默认为`/${name}`* @returns*/export const controller = (name: string, noPrefix = false, prefix?: string, ) => (target: any) => {if (!prefix && !noPrefix) {prefix = "/" + name;}Reflect.defineMetadata(DecoratorKey.Controller, { name, prefix }, target);}// 创建工厂const method = (method: any) => {return (route: string) => {return (target: any, key: string, descriptor: PropertyDescriptor) => {Reflect.defineMetadata(DecoratorKey.Method, {method, route, fn: descriptor.value}, target, key)}}}/*** 创建一个get请求* @param route 路由路径*/export const get = method(MethodTyp.Get);/*** 创建一个post请求* @param route 路由路径*/export const post = method(MethodTyp.POST);import "reflect-metadata"; // 装饰器类型 export enum DecoratorKey { Controller = "controller", Method = "method", } // 请求类型 export enum MethodTyp { Get = "get", POST = "post" } // 请求装饰器元数据类型 export interface MethodMetadata { method: MethodTyp, route: string, fn: Function } /** * controller 只用于收集路由前缀 * @param name 一般为文件名全小写,使用controller[name]能访问到该方法 * @param prefix 路径前缀,选填,默认为`/${name}` * @returns */ export const controller = (name: string, noPrefix = false, prefix?: string, ) => (target: any) => { if (!prefix && !noPrefix) { prefix = "/" + name; } Reflect.defineMetadata(DecoratorKey.Controller, { name, prefix }, target); } // 创建工厂 const method = (method: any) => { return (route: string) => { return (target: any, key: string, descriptor: PropertyDescriptor) => { Reflect.defineMetadata(DecoratorKey.Method, { method, route, fn: descriptor.value }, target, key) } } } /** * 创建一个get请求 * @param route 路由路径 */ export const get = method(MethodTyp.Get); /** * 创建一个post请求 * @param route 路由路径 */ export const post = method(MethodTyp.POST);import "reflect-metadata"; // 装饰器类型 export enum DecoratorKey { Controller = "controller", Method = "method", } // 请求类型 export enum MethodTyp { Get = "get", POST = "post" } // 请求装饰器元数据类型 export interface MethodMetadata { method: MethodTyp, route: string, fn: Function } /** * controller 只用于收集路由前缀 * @param name 一般为文件名全小写,使用controller[name]能访问到该方法 * @param prefix 路径前缀,选填,默认为`/${name}` * @returns */ export const controller = (name: string, noPrefix = false, prefix?: string, ) => (target: any) => { if (!prefix && !noPrefix) { prefix = "/" + name; } Reflect.defineMetadata(DecoratorKey.Controller, { name, prefix }, target); } // 创建工厂 const method = (method: any) => { return (route: string) => { return (target: any, key: string, descriptor: PropertyDescriptor) => { Reflect.defineMetadata(DecoratorKey.Method, { method, route, fn: descriptor.value }, target, key) } } } /** * 创建一个get请求 * @param route 路由路径 */ export const get = method(MethodTyp.Get); /** * 创建一个post请求 * @param route 路由路径 */ export const post = method(MethodTyp.POST);
这里是装饰器的基本写法,基本就是装饰器其实就是一个函数,装饰器函数会返回一个函数,至于返回函数的参数,类装饰器和函数装饰器是不同的,这个在TypeScript官网有介绍
更多详细内容可以查看TypeScript的官网
这里值得注意的是
Reflect.defineMetadata
方法,这个方法将元数据存储起来,后面调用Reflect.getMetadata
的时候就可以取出来了
定义Controller
src/controller/Auth.ts
import { controller, get, post } from "../decorator";import type Application from "koa";import type Router from "koa-router";export type CtxType = Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>, any>;@controller("auth")export default class Auth {@get("/login")login() {return "login";}@post("/register")register() {return {code: 200, msg: "注册成功"}}@post("/user")user(ctx: CtxType) {ctx.body = { id: 12, name: "test", sex: "男" }}}import { controller, get, post } from "../decorator"; import type Application from "koa"; import type Router from "koa-router"; export type CtxType = Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>, any>; @controller("auth") export default class Auth { @get("/login") login() { return "login"; } @post("/register") register() { return {code: 200, msg: "注册成功"} } @post("/user") user(ctx: CtxType) { ctx.body = { id: 12, name: "test", sex: "男" } } }import { controller, get, post } from "../decorator"; import type Application from "koa"; import type Router from "koa-router"; export type CtxType = Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>, any>; @controller("auth") export default class Auth { @get("/login") login() { return "login"; } @post("/register") register() { return {code: 200, msg: "注册成功"} } @post("/user") user(ctx: CtxType) { ctx.body = { id: 12, name: "test", sex: "男" } } }
src/controller/index.ts
import Auth from "./Auth";export default {Auth}import Auth from "./Auth"; export default { Auth }import Auth from "./Auth"; export default { Auth }
这里只定义了几个简单的路由,作为简单的演示
为了后续方便获取对象,我们又添加了一个
index.ts
输出我们所有的Controller,这个后续可以通过读取文件的方式进行
加载路由
router.ts
import Router from "koa-router";import { loadRoutes } from "./utils/loadRouter";// 实例化Routerconst router: Router = new Router();// router基本格式// router.get("/", async (ctx) => {// ctx.body = "hello word";// })loadRoutes(router);export default router;import Router from "koa-router"; import { loadRoutes } from "./utils/loadRouter"; // 实例化Router const router: Router = new Router(); // router基本格式 // router.get("/", async (ctx) => { // ctx.body = "hello word"; // }) loadRoutes(router); export default router;import Router from "koa-router"; import { loadRoutes } from "./utils/loadRouter"; // 实例化Router const router: Router = new Router(); // router基本格式 // router.get("/", async (ctx) => { // ctx.body = "hello word"; // }) loadRoutes(router); export default router;
项目启动的时候就会加载该文件,然后就会调用
loadRoutes方法
,我们只要在loadRoutes
方法中添加所有路由即可【我们将router对象传入进去,然后组装成对应基本格式即可】
utils/loadRouter.ts
import Router from "koa-router";import Controllers from "../controller";import { DecoratorKey, MethodMetadata } from "../decorator";export function loadRoutes(router: Router) {Object.keys(Controllers).forEach((controllerName) => {const localControllers: any = Controllers;const Controller = localControllers[controllerName];// 获取类的装饰器元数据let { prefix } = Reflect.getMetadata(DecoratorKey.Controller, Controller);const Prototype = Controller.prototype;Object.getOwnPropertyNames(Prototype).forEach(key => {// 构造函数去掉if (key === "constructor") {return;}// 获取类函数的装饰器元数据const config: MethodMetadata = Reflect.getMetadata(DecoratorKey.Method, Prototype, key);let { method, route, fn } = config || {};// 没有method方法的可以不用管if (!method) {return;}const path = prefix + route;console.log(`add a ${method} Router: `, path);// 添加路由router[method](path, async (ctx) => {// 拓展位let body = fn(ctx);// 判断是否有返回值,如果有就使用ctx.body返回,如果没有,说明函数内部已经做了返回if(body){ctx.body = body;}})});});}import Router from "koa-router"; import Controllers from "../controller"; import { DecoratorKey, MethodMetadata } from "../decorator"; export function loadRoutes(router: Router) { Object.keys(Controllers).forEach((controllerName) => { const localControllers: any = Controllers; const Controller = localControllers[controllerName]; // 获取类的装饰器元数据 let { prefix } = Reflect.getMetadata(DecoratorKey.Controller, Controller); const Prototype = Controller.prototype; Object.getOwnPropertyNames(Prototype).forEach(key => { // 构造函数去掉 if (key === "constructor") { return; } // 获取类函数的装饰器元数据 const config: MethodMetadata = Reflect.getMetadata(DecoratorKey.Method, Prototype, key); let { method, route, fn } = config || {}; // 没有method方法的可以不用管 if (!method) { return; } const path = prefix + route; console.log(`add a ${method} Router: `, path); // 添加路由 router[method](path, async (ctx) => { // 拓展位 let body = fn(ctx); // 判断是否有返回值,如果有就使用ctx.body返回,如果没有,说明函数内部已经做了返回 if(body){ ctx.body = body; } }) }); }); }import Router from "koa-router"; import Controllers from "../controller"; import { DecoratorKey, MethodMetadata } from "../decorator"; export function loadRoutes(router: Router) { Object.keys(Controllers).forEach((controllerName) => { const localControllers: any = Controllers; const Controller = localControllers[controllerName]; // 获取类的装饰器元数据 let { prefix } = Reflect.getMetadata(DecoratorKey.Controller, Controller); const Prototype = Controller.prototype; Object.getOwnPropertyNames(Prototype).forEach(key => { // 构造函数去掉 if (key === "constructor") { return; } // 获取类函数的装饰器元数据 const config: MethodMetadata = Reflect.getMetadata(DecoratorKey.Method, Prototype, key); let { method, route, fn } = config || {}; // 没有method方法的可以不用管 if (!method) { return; } const path = prefix + route; console.log(`add a ${method} Router: `, path); // 添加路由 router[method](path, async (ctx) => { // 拓展位 let body = fn(ctx); // 判断是否有返回值,如果有就使用ctx.body返回,如果没有,说明函数内部已经做了返回 if(body){ ctx.body = body; } }) }); }); }
拓展位:
如果添加了参数装饰器,那么就可以在这个位置将ctx内的参数取出,然后传入fn中
如果添加了参数校验装饰器,那么就可以在这个位置先进行参数校验,然后判断参数是否校验成功,而决定是否调用fn函数
数据走向
上图仅仅是根据调试做的一个帮助理解的大致数据走向图图,不代表
reflect
实际逻辑
-
在带装饰器文件加载的时候就会调用装饰器返回的函数,这个时候传入的元数据已经存储在了Reflect中
装饰器文件
// 创建工厂const method = (method: any) => {return (route: string) => {return (target: any, key: string, descriptor: PropertyDescriptor) => {console.log("888")Reflect.defineMetadata(DecoratorKey.Method, {method, route, fn: descriptor.value}, target, key)}}}// 创建工厂 const method = (method: any) => { return (route: string) => { return (target: any, key: string, descriptor: PropertyDescriptor) => { console.log("888") Reflect.defineMetadata(DecoratorKey.Method, { method, route, fn: descriptor.value }, target, key) } } }
// 创建工厂 const method = (method: any) => { return (route: string) => { return (target: any, key: string, descriptor: PropertyDescriptor) => { console.log("888") Reflect.defineMetadata(DecoratorKey.Method, { method, route, fn: descriptor.value }, target, key) } } }
loadRouter文件引入部分
console.log(4777)import Controllers from "../controller";console.log(88888)console.log(4777) import Controllers from "../controller"; console.log(88888)
console.log(4777) import Controllers from "../controller"; console.log(88888)
控制台输出
4777888 // get装饰器888 // post装饰器888 // post装饰器888884777 888 // get装饰器 888 // post装饰器 888 // post装饰器 88888
4777 888 // get装饰器 888 // post装饰器 888 // post装饰器 88888
-
调用
Reflect.getMetadata
获取存储起来的元数据
最后
到这里,我们的优化路由就完成了,只是Controller是手动导入的,不是很方便。以下有两个分支代码,一个是手动导入,一个是自动导入,具体实现过程就不写了,可以查看提交记录获取修改点。
其实这个是以前优化Eggjs开发体验做的,最近不记得装饰器使用了,所以想做个笔记,发现Eggjs已经升级到3.x版本,支持装饰器,这篇笔记也是一拖一年左右了,唉,拖延症晚期了。