Egg 中 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 作为请求的起点,这只是个开始。