从零开发ts装饰器管理koa路由

从零开发ts装饰器管理koa路由

超人鸭IP属地: 广东
0.3252022.05.31 18:18:44字数 4,882阅读 1,518

前言

两年前刚学ts,当时搭了个简单的koa的demo,介绍了如何用装饰器管理koa的路由:TS装饰器初体验,用装饰器管理koa接口
但是当时还只是demo学习,并没有真正在公司的项目中使用起来,后面博主搭建开发公司真正的koa项目中,一开始并没有使用到装饰器这个语法来管理路由,还是传统的函数方式,随着模块、接口的累加,越来越觉得传统的路由开发方式不满足于复杂业务的开发,最后结合之前写的demo重新设计了一套装饰器管理路由的模块,将接口全部修改为装饰器管理。
下面我将对比两种路由开发方式,介绍如何使用装饰器来更好的管理koa的路由。

至于如何搭建ts+koa项目,可以参考网上的其他教程,这里不展开了。

koa入口文件:


 
image.png

创建koa实例,使用了基础的中间件,并监听端口。

传统路由模式

文件结构

先看一下文件结构:

 
image.png

src/server.ts 为入口文件

 

路由文件

这里创建了一个 standard-router 文件夹代表传统路由,在test.ts中编写传统路由,通常一个文件代表一个模块
src/standard-router/test.ts:

import Router from 'koa-router'
import commonMiddleware from '../middlewares/common'
import validateParams from '../middlewares/validateParams'
import { testSchema } from '../validator/test'

const router = new Router({
  prefix: '/standard-test'
})
router.allowedMethods()

router.get(
  '/name',
  commonMiddleware,
  validateParams('get', testSchema),
  async (ctx, next) => {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }
)

router.post(
  '/name',
  commonMiddleware,
  validateParams('post', testSchema),
  async (ctx, next) => {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
)

export default router

这就是test.ts的全部内容,也是传统路由的写法,在真正的接口处理方法前可以插入中间件,比如日志打印、参数校验等等。

相关中间件介绍

下面介绍一下引入的几个方法,首先是最上面的 commonMiddleware,这里我用来表示基本每个路由都会使用的中间件:

// src/middlewares/common.ts

import { RouterCtx, MiddleNext } from '../utils/types'

async function commonMiddleware(ctx: RouterCtx, next: MiddleNext ) {
  console.log('common middleware')
  await next()
}

export default commonMiddleware

然后是 validateParams,这是一个参数校验的中间件,是一个工厂函数,传入请求方法和 Joi 校验规则:

// src/middlewares/validateParams.ts
import { RouterCtx, MiddleNext } from '../utils/types'
import Joi from 'joi'
import { ErrorModel } from '../utils/ResModel'
import { paramsErrorInfo } from '../utils/ErrorInfo'

function genValidateParams(method: string, schema: Joi.Schema) {
  async function validateParams(ctx: RouterCtx, next: MiddleNext ) {
    let data: any
    if (method === 'get') {
      data = ctx.request.query
    } else {
      data = ctx.request.body
    }
    const { error } = schema.validate(data)
    if (error) {
      ctx.body = new ErrorModel({
        ...paramsErrorInfo,
        message: error.message || paramsErrorInfo.message
      })
      return
    }
    await next()
  }
  return validateParams
}

export default genValidateParams

关于在koa中如何使用Joi进行参数校验,博主在之前的文章已经进行了介绍:koa中使用joi进行参数校验

统一引入

这样一个文件也就是一个模块就需要创建一个 koa-router 的实例,需要将这个路由实例挂载至koa的实例上,传统的做法是在入口文件将每一个文件引入,再使用 app.use() 进行挂载。这里可以对整个文件夹进行统一引入,封装挂载的方法。
standard-router 文件夹下的 index.ts 中进行封装:

// src/standard-router/index.ts
import fs from 'fs'
import Koa from 'koa'
import Router from 'koa-router'

type RouterFile = {
  default: Router<any, {}>
}

const useRoutes = (app: Koa) => {
  fs.readdirSync(__dirname).forEach(file => {
    if (file.indexOf('index') === 0) return
    import(`./${file}`)
      .then((res: RouterFile) => {
        const router = res.default
        app.use(router.routes())
      })
      .catch(e => {
        console.error(e)
      })
  })
}

export default useRoutes

然后在入口文件 server.ts中引入并调用,传入koa实例:

...
...
import useRoutes from './standard-router'

...
...
useRoutes(app)

app.listen(5200)

分析传统写法的短板

上面写了一个很简单的test.ts的路由模块,可能你会觉得很清晰阿,想要什么功能都能实现,这是肯定的,这可是官方的写法,但是实现和开发成本又是另外一回事,特别是当业务复杂起来后,在真正的业务上,一个模块不可能只有两个简单的接口,根据上面的test.ts可以分析传统写法的短板,重点是我引入的两个中间件上:

  1. 多个模块需要创建多个路由实例
  2. 无法对项目的全部路由统一添加前缀
  3. 多个中间件之间无法进行数据传递,无法感知其他中间件的使用,完全隔离
  4. 无法对一个模块的接口统一添加中间件

1、2点非常明显。
第三点对应的是上面的 validateParams 中间件,需要传入请求方法和joi校验规则,因为不同的请求方法,koa在拿参数的方式不同,这里就体现了在使用 validateParams 中间件时,无法感知当前接口是什么请求方法,需要手动传入。
第四点对应的是上面的 commonMiddleware,当每个接口都需要使用到时,传统写法只能一个个接口进行添加,无法对整个模块进行使用。

真正驱动我设计装饰器去管理路由是第四点,真实业务有很多场景需要对整个模块进行统一添加中间件,比如:登录校验、权限校验,每一个接口都加一遍的操作鸭子都忍不了。

在替换为装饰器管理后,上面列举的传统路由的不足之处全部解决,并且发现新的优点:

  1. 写法更清晰,更容易维护
  2. 更灵活、更加容易拓展功能,复杂功能实现起来更简单
  3. 更能体现ts类型校验的优势

装饰器语法介绍

ts装饰器官方文档
先看官方的解释:

装饰器是一种特殊类型的声明,它能够被附加到类声明方法, 访问符属性参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

注意这里的运行时被调用,这里指的是文件运行,而不是被附加的方法或者类调用时才调用,也就是引入文件就会执行相关的装饰器方法。

根据上面的描述知道,装饰器是一个函数,定义在不同的变量上会有不同的效果,主要是传入的参数不同。

上面说到每个路由文件是一个单独模块,里面每一个接口都属于这个模块,刚好对应到类和类的方法这两者的关系,所以设计的装饰器我只用到了类装饰器和方法装饰器,下面也只介绍这两种装饰器。

在编写装饰器语法的时候,需要在项目的 tsconfig.json 中添加配置:

{
  "compilerOptions": {
    ...
    "experimentalDecorators": true
    ...
  }
}

类装饰器

类装饰器只有一个参数,那就是类本身,也就是构造函数,ts的class编译后就是es5的构造函数。
看一个简单的例子:

function classDecorator(target: new (...args: any[]) => any) {
  target.prototype.getName = function () {
    return this.age
  }
}

@classDecorator
class Test {
  name = '超人鸭'
  age = 18

  getName() {
    return this.name
  }
}

const test = new Test()
console.log(test.getName()) // 18

这里通过装饰器改变了 getName 这个方法,class的方法编译后全部在构造函数的 prototype 上,对这个不熟的可以复习一下es5的原型和原型链,然后看一下ts文件编译后的js代码。

方法装饰器

方法装饰器有三个参数,分别是构造函数的prototype ,方法的名称,方法在 prototype 的属性描述符也就是使用 Object.getOwnPropertyDescriptor()获取属性描述对象

function fnDecorator(target: any, key: string, desc: any) {
  console.log(key)
  console.log(desc)
  console.log(Object.getOwnPropertyDescriptor(target, key))
}

class Test {
  name = '超人鸭'
  age = 18

  @fnDecorator
  getName() {
    return this.name
  }
}

打印结果:


 
image.png

装饰器执行顺序与工厂模式

每一个方法或者每一个类都可以添加多个装饰器,同个方法或同个类上的装饰器的执行顺序为由近至远,越靠近被附加方法的装饰器先执行,也就是从下往上的。
一个类上同时有类装饰器与方法装饰器的情况,先执行方法装饰器,再执行类装饰器。

对于装饰器,传入的参数是固定的,如果想对其实现一些不同的功能,可以通过工厂模式,也就是一个函数里面再返回装饰器函数。

下面是为了体现执行顺序和工厂模式的例子:

function classDecorator(str: string) {
  return function (target: new (...args: any[]) => any) {
    console.log(str)
  }
}

function fnDecorator(str: string) {
  return function (target: any, key: string, desc: any) {
    console.log(str)
  }
}

@classDecorator('class 2')
@classDecorator('class 1')
class Test {
  @fnDecorator('fn 2')
  @fnDecorator('fn 1')
  getName() {
    return '超人鸭'
  }
}

// 打印顺序为:fn 1 、 fn 2 、 class 1 、 class 2

reflect-metadata

上面介绍了装饰器的用法,如果单纯依靠装饰器的语法特点,还不足以对类与方法做更多操作,还需要其他功能来辅助操作。

reflect-metadata 意思为元数据,可以为对象或对象的属性定义元数据,先来看下这个库如何使用

使用前需要先安装:

npm install reflect-metadata --save

使用这个库的时候只需引入即可

首先定义元数据:

import 'reflect-metadata'

const obj = {
  name: '超人鸭'
}

Reflect.defineMetadata('objMetadata', 'object metadata', obj)
Reflect.defineMetadata('propertyMetadata', 'property metadata', obj, 'name')

上面的代码就是为对象和对象上的一个属性添加了元数据,下面看一下定义的语法:

如果是对象:
Reflect.defineMetadata(metadataKey, metadataValue, target)

如果是对象上的属性:
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)

第一个参数为定义元数据的key;第二个参数为定义元数据的value;第三个参数为定义的对象;如果是定义在属性上面,就需要第四个参数,为属性名称。

下面看一下如何定义完数据后如何取数据:

import 'reflect-metadata'

const obj = {
  name: '超人鸭'
}

Reflect.defineMetadata('objMetadata', 'object metadata', obj)
Reflect.defineMetadata('propertyMetadata', 'property metadata', obj, 'name')

const objMetadata = Reflect.getMetadata('objMetadata', obj)
const propertyMetadata = Reflect.getMetadata('propertyMetadata', obj, 'name')

console.log(objMetadata, propertyMetadata) // object metadata  property metadata

语法为:

如果是对象:
Reflect.getMetadata('metadataKey', 'target')

如果是对象上的属性:
Reflect.getMetadata('metadataKey', 'target', 'propertyKey')

原理:
'reflect-metadata' 是在内部定义了一个 weakmap 将对象和定义的值做了映射
大致过程如下:

const obj = {
  name: '超人鸭'
}

Reflect.defineMetadata('objMetadata', 'object metadata', obj)

const weakmap = new WeakMap()
const metadata = new Map()

metadata.set('objMetadata', 'object metadata')
weakmap.set(obj, metadata)

如果是定义在对象的属性上面,那就再多一层map做映射:

const obj = {
  name: '超人鸭'
}
Reflect.defineMetadata('propertyMetadata', 'property metadata', obj, 'name')

const weakmap = new WeakMap()
const metadata = new Map()
const metadataMap = new Map()

metadata.set('objMetadata', 'object metadata')
metadataMap.set('name', metadata)
weakmap.set(obj, metadataMap)

这便是 reflect-metadata 的作用和用法,将它和装饰器语法再结合类与方法的关系,就可以实现管理整个模块路由的功能。

装饰器管理koa路由

思路

先回顾一下传统写法:

import Router from 'koa-router'
import commonMiddleware from '../middlewares/common'
import validateParams from '../middlewares/validateParams'
import { testSchema } from '../validator/test'

const router = new Router({
  prefix: '/standard-test'
})
router.allowedMethods()

router.get(
  '/name',
  commonMiddleware,
  validateParams('get', testSchema),
  async (ctx, next) => {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }
)

router.post(
  '/name',
  commonMiddleware,
  validateParams('post', testSchema),
  async (ctx, next) => {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
)

export default router

如何将这个路由模块改为装饰器模式呢。
koa的接口开发可以理解为就是路由定义,定义好这个路由的请求方法、请求路径、执行的中间件、接口主逻辑,改造成装饰器的最后也是要实现这个功能,路由定义。
每一个路由文件代表一个模块,文件下的每一个接口都属于这个模块,对应类与类的方法这个关系。
然后这个方法我们只处理接口的主逻辑,请求方法、请求路径、中间件我们都放到装饰器去处理,这些信息可以通过元数据定义到方法上。
然后利用装饰器的执行顺序,先方法然后再是类,我们可以在类的装饰器取到所有经过装饰器处理的方法,统一进行路由注册。
大概是这个思路,下面我将一步步实现。

将传统写法改造成class

 

 
image.png

创建同级的装饰器路由文件夹,在 test.ts 中编写改造的路由:

 

// src/decorator-router/test.ts
import Router from 'koa-router'
import commonMiddleware from '../middlewares/common'
import validateParams from '../middlewares/validateParams'
import { testSchema } from '../validator/test'
import { RouterCtx } from '../utils/types'

class TestRouteModule {
  async getName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }

  async postName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }
}

现在只是定义了class,还没有任何功能。

请求方法装饰器

下面开发一个装饰器,实现请求方法和请求路径的定义,实现效果为:

@get(path)

@post(path)

不同的方法对应不同的装饰器,传入请求路径,使用工厂模式。
先创建文件:

 
image.png

在 request.ts 中编写:

 

// src/decorator/request.ts
function get(path: string){
  // 往方法上存上路径与请求方法
  return function (target: any, key: string) {
    Reflect.defineMetadata('path', path, target, key)
    Reflect.defineMetadata('method', 'get', target, key)
  }
}

function post(path: string){
  return function (target: any, key: string) {
    Reflect.defineMetadata('path', path, target, key)
    Reflect.defineMetadata('method', 'post', target, key)
  }
}

将请求方法、请求路径定义到方法的元数据上,上面的代码可以再做一层封装,将 get、post 当成参数传入,相当于再包一层工厂函数:

// src/decorator/request.ts
import 'reflect-metadata'

function genRequestDecorator(type: string) {
  return function (path: string) {
    return function (target: any, key: string) {
      Reflect.defineMetadata('path', path, target, key)
      Reflect.defineMetadata('method', type, target, key)
    }
  }
}

export const get = genRequestDecorator('get')
export const post = genRequestDecorator('post')

外部的使用方式没变

在 decorator/index.ts 中统一导出:

// src/decorator/index.ts
export * from './request'

在 decorator-router/test.ts 中引入并使用:

...
import { get, post } from '../decorator/index'

class TestRouteModule {
  @get('/name')
  async getName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }

  @post('/name')
  async postName(ctx: RouterCtx) {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
}

到这里也只是把请求方法、请求路径定义到方法的元数据上,还没有将它们取出来并注册路由。

类装饰器完成路由注册

根据装饰器的特点,先执行方法装饰器,再执行类装饰器,我们在上面已经引入了方法装饰器,在执行类装饰器的时候,相关的信息已经添加至方法的元数据。然后类的装饰器的参数就是构造函数,类上的方法在存在构造函数的 prototype 上,所以我们在类装饰器中,通过参数同样可以取得定义在方法上的元数据,包括请求方法、请求路径,还有方法本身。
一个最基本的路由定义为:

router[method](path, handler)

这三个信息都可以拿到,所以在这里就可以完成路由注册。

在这之前,回忆一下传统路由的写法,每一个文件都需要创建一个新的路由实例,但是最后被 koa 实例use之后这些不同的实例并无区别,都是对请求路径进行判断处理。

在使用装饰器之后,我们完全可以只创建一个路由实例,不同的模块唯一的区别只是请求路径前缀不同而已,注册过程并没有区别。

首先在一个文件上创建路由实例并导出:

 
image.png

routerInstance.ts

 

import Router from 'koa-router'

const router = new Router()

router.allowedMethods()

export default router

接下来开发类装饰器:

 
image.png

在 decorator 下创建 controller.ts 文件:

import 'reflect-metadata'
import router from '../routerInstance'

export function controller(root: string) {
  return function (target: new (...args: any[]) => any) {
    const handlerKeys = Object.getOwnPropertyNames(target.prototype).filter(
      key => key !== 'constructor'
    )
    handlerKeys.forEach(key => {
      const path: string = Reflect.getMetadata('path', target.prototype, key)
      const method: string = Reflect.getMetadata(
        'method',
        target.prototype,
        key
      )

      const handler = target.prototype[key]

      if (path && method) {
        const fullPath = root === '/' ? path : `${root}${path}`
        router[method](fullPath, handler)
      }
    })
  }
}

这个类装饰器允许传入一个参数,代表模块路由的前缀。
通过 Object.getOwnPropertyNames 取得类上的所有方法,因为经过编译之后,类上的方法在构造函数的 prototype 上的属性描述是不可枚举的,没有办法通过 for in 来获取,这个可能不同的 typescript 版本表现会有不同。
然后通过 Reflect.getMetadata 取得之前在方法装饰器上定义的信息,最后完成路由注册。

同样的,在decorator/index.ts 导出:

// src/decorator/index.ts
export * from './request'
export * from './controller'

然后回到路由文件decorator-router/test.ts 中引入并使用

...
import { get, post, controller } from '../decorator/index'

@controller('/decorator-test')
class TestRouteModule {
  @get('/name')
  async getName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }

  @post('/name')
  async postName(ctx: RouterCtx) {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
}

引入文件使装饰器执行

上面在装饰器中完成了路由注册,但是这些装饰器还没执行,现在需要让它们执行起来,我们只需要将路由文件引入就可以,在 decorator-router/index.ts 中统一导入

// src/decorator-router/index.ts
import fs from 'fs'

fs.readdirSync(__dirname).forEach(file => {
  if (file.indexOf('index') === 0) return
  import(`./${file}`)
})

然后在入口文件中引入此文件还有路由实例:
src/server.ts

import Koa from 'koa'
import json from 'koa-json'
import koaBody from 'koa-body'
import logger from 'koa-logger'
import useRoutes from './standard-router'
import './decorator-router/index' // 引入装饰器路由文件,使装饰器运行
import router from './routerInstance' // 引入路由实例

const app = new Koa()

// middlewares
app.use(
  koaBody({
    multipart: true
  })
)
app.use(json())
app.use(logger())

useRoutes(app)
app.use(router.routes()) // 挂载路由实例

app.listen(5200)

引入后就完成了路由注册,这里我们可以测试一下:
vscode可以安装一个插件:

 
image.png

可以用它来发送http请求,具体使用方法参考网上的教程
下面是结果:

 
image.png

接口正常响应。

开发中间件装饰器

回到我们的路由文件 decorator-router/test.ts :

import Router from 'koa-router'
import commonMiddleware from '../middlewares/common'
import validateParams from '../middlewares/validateParams'
import { testSchema } from '../validator/test'
import { RouterCtx } from '../utils/types'
import { get, post, controller } from '../decorator/index'

@controller('/decorator-test')
class TestRouteModule {
  @get('/name')
  async getName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }

  @post('/name')
  async postName(ctx: RouterCtx) {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
}

到这里只是完成了基础的路由逻辑,并没有包含中间件,中间件说白了就是在路由主逻辑前执行的一个函数,同样的,我们可以将这个中间件函数定义在方法的元数据中,然后在最后的类装饰器将方法取出来,插入至路由注册中。

通用中间件装饰器

创建使用中间件装饰器:

 
image.png
// src/decorator/use.ts
import 'reflect-metadata'
import { RouterCtx, MiddleNext } from '../utils/types'

export function use(
  middleware: (ctx: RouterCtx, next: MiddleNext) => Promise<any>,
  position: 'last' | number = 'last'
) {
  return function (target: any, key: string) {
    const middlewares = Reflect.getMetadata('middlewares', target, key) || []
    if (position === 'last') {
      middlewares.push(middleware)
    } else {
      middlewares.splice(position, 0, middleware)
    }
    Reflect.defineMetadata('middlewares', middlewares, target, key)
  }
}

传入一个中间件函数,通常通过装饰器挂载的顺序来决定中间件执行的顺序,但还是拓展了第二个参数,支持添加至任意位置来控制中间件执行的顺序。
定义 middlewares 元数据代表中间件函数

然后改造类装饰器 controller,添加中间件注册逻辑:

// // src/decorator/controller.ts
import 'reflect-metadata'
import router from '../routerInstance'

export function controller(root: string) {
  return function (target: new (...args: any[]) => any) {
    const handlerKeys = Object.getOwnPropertyNames(target.prototype).filter(
      key => key !== 'constructor'
    )
    handlerKeys.forEach(key => {
      const path: string = Reflect.getMetadata('path', target.prototype, key)
      const method: string = Reflect.getMetadata(
        'method',
        target.prototype,
        key
      )

      const handler = target.prototype[key]

      const middlewares =
        Reflect.getMetadata('middlewares', target.prototype, key) || [] // 取出中间件

      if (path && method) {
        const fullPath = root === '/' ? path : `${root}${path}`
        router[method](fullPath, ...middlewares, handler) // 注册进去
      }
    })
  }
}

同样在 decorator/index.ts 中导出 use 装饰器,路由文件引入并使用:

// decorator-router/test.ts
import commonMiddleware from '../middlewares/common'
import validateParams from '../middlewares/validateParams'
import { testSchema } from '../validator/test'
import { RouterCtx } from '../utils/types'
import { get, post, controller, use } from '../decorator/index'

@controller('/decorator-test')
class TestRouteModule {
  @use(validateParams('get', testSchema))
  @use(commonMiddleware)
  @get('/name')
  async getName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }

  @use(validateParams('post', testSchema))
  @use(commonMiddleware)
  @post('/name')
  async postName(ctx: RouterCtx) {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
}

参数校验中间件装饰器

我封装的参数校验中间件需要传入请求方法和 Joi 校验规则两个参数,前面分析传统写法的一个短板就是不同的中间件之间,数据无法传递,现在使用了装饰器写法,并将相关信息定义在方法的元数据上,那么同个方法的中间件通过获取元数据就可以达到数据传递的目的,那么在参数校验中间件上就能实现拿到接口的请求方法。
而参数校验基本每个接口都需要用到,所以它值得我去开发一个装饰器:

 
image.png

 

// src/decorator/validate.ts
import 'reflect-metadata'
import Joi from 'joi'
import genValidateParams from '../middlewares/validateParams'

export function validate(schema: Joi.Schema) {
  return function (target: any, key: string) {
    const method = Reflect.getMetadata('method', target, key)
    const validateParamsMiddleware = genValidateParams(method, schema)

    const middlewares = Reflect.getMetadata('middlewares', target, key) || []
    middlewares.push(validateParamsMiddleware)
    Reflect.defineMetadata('middlewares', middlewares, target, key)
  }
}

在这里就可以取到在 request 装饰器定义的请求方法元数据,同样是操作 middlewares 这个元数据
同样在 index.ts 导出
回到路由文件,进行改造:

import commonMiddleware from '../middlewares/common'
import { testSchema } from '../validator/test'
import { RouterCtx } from '../utils/types'
import { get, post, controller, use, validate } from '../decorator/index'

@controller('/decorator-test')
class TestRouteModule {
  @validate(testSchema)
  @use(commonMiddleware)
  @get('/name')
  async getName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }

  @validate(testSchema)
  @use(commonMiddleware)
  @post('/name')
  async postName(ctx: RouterCtx) {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
}

更加清晰且少传一个手写的参数

对模块接口统一添加中间件装饰器

上面的代码中,我写了一个 commonMiddleware 来表示每个接口都需要添加的中间件,目前是每个接口都添加了一遍。
如果想要为模块的每一个接口都统一添加,就需要拿到类上的每一个方法,那么这个装饰器就应该添加类上面,同样的,操作中间件还是操作 middlewares 这个元数据
新建装饰器文件:

 
image.png

 

// src/decorator/unifyUse.ts

import 'reflect-metadata'
import { RouterCtx, MiddleNext } from '../utils/types'

/**
 * 对同一个路由模块统一添加中间件
 * @param middleware 中间件函数
 * @param excludes 排除的路由
 * @param inLast 是否添加在最后,默认塞在最前面
 */
export function unifyUse<T extends string>(
  middleware: (ctx: RouterCtx, next: MiddleNext) => Promise<any>,
  excludes: Array<T> = [],
  inLast = false
) {
  return function (target: new (...args: any[]) => any) {
    const handlerKeys = Object.getOwnPropertyNames(target.prototype).filter(
      key => key !== 'constructor'
    )
    handlerKeys.forEach(key => {
      if (!excludes.includes(key as T)) {
        const middlewares =
          Reflect.getMetadata('middlewares', target.prototype, key) || []
        if (inLast) {
          middlewares.push(middleware)
        } else {
          middlewares.unshift(middleware)
        }
        Reflect.defineMetadata(
          'middlewares',
          middlewares,
          target.prototype,
          key
        )
      }
    })
  }
}

首先第一个参数传入需要统一添加的中间件函数。
第二个参数传入不需要添加这个中间件的方法名称集合,可能会有些接口是特殊处理的,需要在统一添加的时候排除掉。
第三个参数可以指定统一添加的中间件在最后执行,默认添加在第一位,通常需要统一添加的中间件都是最先执行,比如登录校验、日志打印等。
回到这个函数的类型定义上,可以接收一个泛型,主要是第二个参数:排除的方法名称用到,表示传入的方法名必须符合传入的泛型类型,在引入的地方会传入。

同样的,在 index.ts 中导出
回到路由文件,进行改造:

// src/decorator-router/test.ts
import commonMiddleware from '../middlewares/common'
import { testSchema } from '../validator/test'
import { RouterCtx } from '../utils/types'
import {
  get,
  post,
  controller,
  use,
  validate,
  unifyUse
} from '../decorator/index'

/** 装饰器router clsss */
export type RouterController<T extends string> = {
  [key in T]: (ctx: RouterCtx) => Promise<void>
}

type MethodName = 'getName' | 'postName'

@controller('/decorator-test')
@unifyUse<MethodName>(commonMiddleware)
class TestRouteModule implements RouterController<MethodName> {
  @validate(testSchema)
  @get('/name')
  async getName(ctx: RouterCtx) {
    const { name } = ctx.request.query
    ctx.body = {
      name: name
    }
  }

  @validate(testSchema)
  @post('/name')
  async postName(ctx: RouterCtx) {
    const { name } = ctx.request.body
    ctx.body = {
      name: name
    }
  }
}

将 unifyUse 附加到类上面,注意执行顺序,controller 装饰器必须在最后执行。
传入了 commonMiddleware表示该模块的所有接口都引入这个中间件。

同时定义了 MethodName 类型与 RouterController 类型,我们的类去实现 RouterController 这个类型,表示我们的类必须实现 MethodName 所定义的方法名称的方法,然后将 MethodName 传递给 unifyUse 函数,如果此时需要传入第二个参数,表示排除掉传入的方法,那么传入的方法名称就必须在 MethodName 中定义了,效果:

 
image.png

 

总结

上面已经介绍了几个装饰器,也是我日常开发中使用频率最高的。装饰器写法对比传统写法的好处已经显而易见了,一些基于 koa 封装的框架都是使用装饰器进行管理,这里我选择自己从零开始设计开发装饰器,也是为了更加灵活的使用,满足自己所需要的功能。除了上面所说的几个装饰器,装饰器还可以实现更多复杂的功能,这里就不展开。

如果你有更好的见解和用法,欢迎指教。

作者微信:Promise_fulfilled

posted on 2024-08-12 11:39  漫思  阅读(22)  评论(0编辑  收藏  举报

导航