# koa-decorator
**Repository Path**: wangzhaoyv/koa-decorator
## Basic Information
- **Project Name**: koa-decorator
- **Description**: 使用装饰器来优化KOA框架开发体验
- **Primary Language**: TypeScript
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2023-06-22
- **Last Updated**: 2023-06-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 装饰器优化KOA路由
[TypeScript 官方文档](https://www.tslang.cn/docs/handbook/decorators.html)
## 新建KOA项目
```bash
# 创建文件夹
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**
```typescript
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/`,这个很重要,不然运行起来没有反应,会觉得没有运行
### 添加指令,方便运行
```json
"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**
```json
{
"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**
```typescript
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**
```typescript
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/")
});
```
**到此,项目基本就建立起来,可以实现我们的装饰器能力了**
### 目录结构
```bash
|-- koa-decorator
|-- app.ts
|-- package-lock.json
|-- package.json
|-- tsconfig.json
|-- src
|-- router.ts
|-- controller
```
## 定义装饰器
```typescript
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**
```typescript
import { controller, get, post } from "../decorator";
import type Application from "koa";
import type Router from "koa-router";
export type CtxType = Application.ParameterizedContext, 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**
```typescript
import Auth from "./Auth";
export default {
Auth
}
```
> 这里只定义了几个简单的路由,作为简单的演示
>
> 为了后续方便获取对象,我们又添加了一个`index.ts`输出我们所有的Controller,这个后续可以通过读取文件的方式进行
### 加载路由
**router.ts**
```typescript
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**
```typescript
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`实际逻辑
1. 在带装饰器文件加载的时候就会调用装饰器返回的函数,这个时候传入的元数据已经存储在了Reflect中
**装饰器文件**
```typescript
// 创建工厂
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文件引入部分**
```typescript
console.log(4777)
import Controllers from "../controller";
console.log(88888)
```
**控制台输出**
```bash
4777
888 // get装饰器
888 // post装饰器
888 // post装饰器
88888
```
2. 调用`Reflect.getMetadata`获取存储起来的元数据