装饰器优化KOA路由

装饰器优化koa路由

TypeScript 官方文档

新建KOA项目

# 创建文件夹
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
# 创建文件夹 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";
// 实例化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/")
});
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添加以上配置

这里主要是打开experimentalDecoratorsemitDecoratorMetadata

experimentalDecorators: 允许使用装饰器能力

emitDecoratorMetadata:允许发射装饰器元数据

更改目录,符合工程化

每个功能尽量有单独的文件管理,以防止文件过多而导致最后项目单文件过大而无法维护,所以我们应该将router抽离放入一个单独的文件中,如果后期路由过多可能还需要进行分文件【但是我们使用装饰器就不用考虑路由的分文件,后面就能看到为什么】

  • 在src根目录下新建router.ts文件
  • app.ts文件中的路由部分移动到router.ts文件中
  • 新建controller文件夹,这个文件夹是为后面控制器的家

router.ts

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;
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';
// 实例化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/")
});
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";
// 实例化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;
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函数

数据走向

数据流向.png

上图仅仅是根据调试做的一个帮助理解的大致数据走向图图,不代表reflect实际逻辑

  1. 在带装饰器文件加载的时候就会调用装饰器返回的函数,这个时候传入的元数据已经存储在了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)

    控制台输出

    4777
    888 // get装饰器
    888 // post装饰器
    888 // post装饰器
    88888
    4777  
    888     // get装饰器
    888    // post装饰器
    888   // post装饰器
    88888
    4777 888 // get装饰器 888 // post装饰器 888 // post装饰器 88888
  2. 调用Reflect.getMetadata获取存储起来的元数据

最后

到这里,我们的优化路由就完成了,只是Controller是手动导入的,不是很方便。以下有两个分支代码,一个是手动导入,一个是自动导入,具体实现过程就不写了,可以查看提交记录获取修改点。

其实这个是以前优化Eggjs开发体验做的,最近不记得装饰器使用了,所以想做个笔记,发现Eggjs已经升级到3.x版本,支持装饰器,这篇笔记也是一拖一年左右了,唉,拖延症晚期了。

Git 项目地址

手动导入Controller版本

自动导入Controller版本

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYHcAWNF' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片