欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Egg 中 Controller 最佳实践

程序员文章站 2022-07-30 17:06:23
得益于 JavaScript 加入的 decorator 特性,可以使我们跟 Java/C 一样,更加直观自然的,做面向切面编程。而随着 TypeScript 的成熟,类型系统也让我们增强了信心,面对复杂的业务逻辑,也更有底气。 "egg controller" 是集合了一些在 Controller ......

得益于 javascript 加入的 decorator 特性,可以使我们跟 java/c# 一样,更加直观自然的,做面向切面编程。而随着 typescript 的成熟,类型系统也让我们增强了信心,面对复杂的业务逻辑,也更有底气。

是集合了一些在 controller 层开发中常见问题解决方案的插件。

controller 路由定义

export class homecontroller {
  @route('/api/xxx', { name: '获取xxx数据' })
  async getxxx(size: number, page: number) {
    return 'homeindex';
  }
}

可以看到,使用 decorator 的形式来声明 controller 非常直观,而且方便扩展,添加/修改 controller 规则直接修改 decorator 的类型定义就好。这种形式也是 java/c# 的常规操作。

这里的改进除了使用 decorator 替代了 router.js 来进行 controller 声明以外,还添加了出入参支持,省去了需要手动读写 ctx 的过程,非常直观的声明 controller 函数的参数需求,以及返回数据类型。

基于 decorator 的写法与之前最大的区别是,在 controller 这个横切面,之前只有 loader 可以掌控,而现在可以在 decorator 中加以集中控制,再结合 typescript 的元信息,可以做出很多扩展,比如:

参数格式化

在 eggjs 中,因为没有类型信息的原因,从 params 和 query 中获取的信息都会是字符串类型,都需要在 controller 中手动转换。而改造之后的写法,参数直接暴露在函数入参里,我们就可以直接拿写在入参的类型定义作为格式化的依据,根据类型尝试转换,保证参数类型正确,可以初步防止类型不符的参数进入到 controller,省去手动判断、转换的逻辑。

参数校验

参数格式化只能保证参数的类型一致性,而我们的需求不止这些,比如必选参数为空时需要拦截,有时参数是复杂对象为了防止恶意构造数据,需要对数据格式做深度检测,所以这里引入了参数校验库,,通过它来解决复杂的校验问题。

export class homecontroller {
  @route('/api/xxx', { name: '获取xxx数据', validatemetainfo: [{
    name: 'data',
    rule: {
      type: 'object',
      str: { type: 'string', max: 20 },
      count: { type: 'number', max: 10, required: false },
    },
   }] })
  async getxxx(data: { str: string, count?: number }>) {
    return data.str;
  }
}

这里有个问题,在类型是复杂类型时,typescript 默认生成的元数据里,类型一律为object,所以,想要在定义类型的同时,复用类型的定义,只能在编译时做工作,typescript 也开放出了编译时插件api,在不用编译时插件的情况下,就需要单独写一份规则的数据。

有插件后:

export class homecontroller {
  @route('/api/xxx', { name: '获取xxx数据' })
  async getxxx(data: basevalidaterule<{
    type: 'object',
    rule: {
      str: { type: 'string', max: 20 },
      count: { type: 'number', max: 10, required: false },
    },
  }>) {
    return data.str;
  }
}

路由级中间件

函数类型跟 egg 定义稍有不同:

(app: application, typeinfo: routetype) => (ctx: any, next: any) => any

egg 已经定义了中间件,为什么在路由上还定义一个?在路由定义的中间件跟全局的中间件区别在于范围,全局中间件更适合大规模的统一处理,用来统一处理特定业务功能接口就大材小用了,还需要设置过滤逻辑,甚至需要在 config 中设置黑白名单。而路由级中间件适合只有部分接口需要的统一处理,配合从 @route 上收集的类型信息处理更佳。

api文档 & 前端sdk

既然已经收集到了那么多元数据,根据这些数据生成api文档就很简单了无非就是前端的展示,也可以把数据转换对接其他的api文档平台。

更近一步,直接生成前端调用sdk?当然没问题。本插件支持通过模板生成,如果没有找到模板,会在sdk生成目录生成默认模板。

// controller
export class metacontroller {

  @route({ url: '/meta/index', name: '首页' })
  async index(id: string, n: number, e: 'enuma' | 'enumb', d: date) {
    return 'metaindex';
  }

}

// 生成代码
export class metaservice extends base {

  /** 首页  */
  async index(id: string, n: number, e: string, d: date) {
    const __data = { id, n, e, d };
    return await this.request({
      method: `get`,
      url: `/meta/index`,
      data: __data,
    });
  }

}

export const metaservice = new metaservice();
export default new metaservice();

开发时

在配置中开启即可,根据需要自定义其他配置。当 controller 中文件修改时,会同时重新生成对应的前端sdk文件。

构建打包时

在 前端打包流程前 可以使用 egg-controller gensdk 命令生成前端sdk,需要注意,如果为 typescript 项目,需要先将 typescript 编译,然后执行生成命令。

controller 作为请求的起点,这只是个开始。

详细文档