eggjs 作为国内做的最好的企业级 nodejs 框架,整体设计以及很多细节功能个人都非常喜欢。我在 2017 年负责组织腾讯 IMWebConf
时,就曾经邀请 eggjs 的核心开发者天猪分享关于 eggjs 的一些经验。
自从 eggjs 发布后,我个人做的一些项目也逐步从 restify 切换至 eggjs。由于我使用的基本是 Mongodb,配合 mongoose 使用,随着项目的增多,针对数据表的增删改查接口成为其中最常用的一个需求。
按照 eggjs 的约定,如果要实现一个表的增删改查需求,例如用户表,我们需要新建 3 个文件:
schema/user.js
描述用户表结构service/user.js
针对用户表的增删改查操作,引用schema/user.js
,其中根据 mongoose 的规则定义 model。controller/user.js
针对增删改查请求做一些参数的校验以及处理后,调用server/user.js
中的接口完成对应操作,并作出响应。
此外,还需要修改 router.js
,如果 controller 中定义的方法是按照 router 的约定来定义的话,则只需要添加一行:
// router.post('/api/user', controller.user.create)
// router.delete('/api/user/:id', controller.user.destroy)
// router.put('/api/user/:id', controller.user.update)
// router.get('/api/user/:id', controller.user.show)
// router.get('/api/user', controller.user.index)
router.resources('user', '/api/user', controller.user); // 用户
如果按照这个模式,当数据表增加至十几个甚至几十个时,重复的文件和代码就太多了。当然,我们可以定义增删改查的基类,其他表的 service 和 controller 都继承自该基类,但还是有些繁琐,很多文件看着也不便管理。
所以我期望的最终效果是:
- 只需要定义
schema/user.js
,不需要新建任何文件,即可自动生成对应的 service 和 controller,之后只需要根据需求修改router.js
,暴露相关的接口。 - 可以根据需求的变更自定义 service 和 controller。例如新建
service/user.js
即可自动继承基础的增删改查方法,同时可以自定义一些其他的方法。
在查看 eggjs 的官方文档后,我们需要做的第一件事情是能够自动加载 schema。这里赞一下 eggjs 的文档,写的非常详细,对于如何扩展框架也有非常详细的描述。
首先新建一个 eggjs 的框架项目,添加文件 lib/framework.js
,添加基础代码,使用 loader 加载 app/schema
目录的所有文件。
class AppWorkerLoader extends egg.AppWorkerLoader {
loadRouter(opt) {
this.loadSchema();
super.loadRouter(opt);
}
loadSchema() {
const { app } = this;
const schemaPaths = this.getLoadUnits().map(unit => path.join(unit.path, 'app/schema'));
// 先加载schema
if (app.config.schema) {
this.loadToApp(schemaPaths, 'schema');
}
}
}
这样既可通过 app.schema
访问到所有定义的表结构。
第二步,自动生成 service,以及支持自定义 service。这里,我翻阅了 eggjs 的源码,其中加载 service 的部分,核心是把 service 挂载到 app.serviceClasses
这个对象上。所以我们可以直接把需要自动生成的 service 挂到 app.serviceClasses
对象即可。
我们在 loadSchema
方法下面添加如下代码:
// 根据schema生成对应的service和controller
Object.keys(app.schema).forEach(name => {
const customService = this.app.serviceClasses[name]; // 自定义的 service
this.app.serviceClasses[name] = this.createService(name);
// 如果有自定义 service,合并原型方法。
if (customService) {
Object.assign(this.app.serviceClasses[name].prototype, customService);
}
});
然后在 AppWorkerLoader 类添加一个创建自定义 service 的 createService 方法:
createService(name) {
class NewService extends this.app.serviceClasses.base {
get name() { return name; }
}
return NewService;
}
其中只定义了一个 name 属性的 getter 方法,方便 service 可以知道自己对应的是哪个 schema,可以通过 this.app.schema[this.name] 访问到 schema。
第三步,自动生成 controller,并支持继承。这里查看 eggjs 源码后,发现在完成 loader 加载 controller 之后,会对 controller 对一些特殊处理,以适应 router.js 中对 controller 的调用。所以我这里没有考虑太复杂,只是简单的复制同样的代码对新建的 controller 做同样处理。
在外围添加对 controller 处理的方法,然后继续修改 loadSchema 方法:
function callFn(fn, args, ctx) {
args = args || [];
return ctx ? fn.call(ctx, ...args) : fn(...args);
}
// wrap the class, yield a object with middlewares
function wrapClass(Controller) {
let proto = Controller.prototype;
const ret = {};
// tracing the prototype chain
while (proto !== Object.prototype) {
const keys = Object.getOwnPropertyNames(proto);
for (const key of keys) {
// getOwnPropertyNames will return constructor
// that should be ignored
if (key === 'constructor') {
continue;
}
// skip getter, setter & non-function properties
const d = Object.getOwnPropertyDescriptor(proto, key);
// prevent to override sub method
if (typeof d.value === 'function' && !ret.hasOwnProperty(key)) {
ret[key] = methodToMiddleware(Controller, key);
}
}
proto = Object.getPrototypeOf(proto);
}
return ret;
function methodToMiddleware(Controller, key) {
return function classControllerMiddleware(...args) {
const controller = new Controller(this);
if (!this.app.config.controller || !this.app.config.controller.supportParams) {
args = [ this ];
}
return callFn(controller[key], args, controller);
};
}
}
class AppWorkerLoader extends egg.AppWorkerLoader {
createController(name) {
class NewController extends this.app.BaseController {
constructor(ctx) {
super(ctx);
this.name = name;
}
}
return NewController;
}
loadSchema() {
const { app } = this;
const schemaPaths = this.getLoadUnits().map(unit => path.join(unit.path, 'app/schema'));
// 先加载schema
if (app.config.schema) {
this.loadToApp(schemaPaths, 'schema');
}
// 根据schema生成对应的service和controller
Object.keys(app.schema).forEach(name => {
const customService = this.app.serviceClasses[name]; // 自定义的 service
this.app.serviceClasses[name] = this.createService(name);
// 如果有自定义 service,合并原型方法。
if (customService) {
Object.assign(this.app.serviceClasses[name].prototype, customService);
}
// 生成 controller
this.app.controller[name] = wrapClass(this.createController(name));
});
}
}
至此,我的大部分需求就已经实现了,除了自定义 controller 之外(这个等会再讲)。
那么,到现在,应该如何使用这个框架呢?
- 把这个
framework
发布到 npm ,例如叫做eggooo
。 - 在新项目中,
npm i eggoo —save
,并在 package.json 中添加 egg 属性:"egg": { "framework": "eggooo" }
- 添加
service/base.js
作为所有表的 service 基类,其中定义增删改查方法。 - 添加
controller/base.js
作为 controller 的基类。这里使用了一个简单的技巧:
const egg = require('egg');
module.exports = app => {
class Controller extends egg.Controller {
constructor(ctx) {
super(ctx);
this.name = 'base';
}
getService() {
const service = this.service[this.name];
if (!service) throw new Error(`没有找到对应的service:${this.name}`);
return service;
}
// 创建
async create() {
}
// 删除单个
async destroy() {
}
// 修改
async update() {
}
// 获取单个
async show() {
}
// 获取所有(分页/模糊)
async index() {
}
// 删除所选(条件id[])
// {id: ["5a452a44ab122b16a0231b42","5a452a3bab122b16a0231b41"]}
async removes() {
}
}
app.BaseController = Controller;
};
由于我们把 controller 挂到了 app.BaseController
,所以如果需要自定义 controller 的话,新建的 controller 继承这个 app.BaseController
即可。例如自定义 user 的 controller:
module.exports = app => {
class Controller extends app.BaseController {
constructor(ctx) {
super(ctx);
this.name = 'user';
}
// 自定义的方法
async customMethod(){
}
}
return Controller;
};
OK,到这里就大概差不多了,完整代码可以在这里看到:https://github.com/yisbug/ooo
当然,由于时间关系,其中对 controller 的处理其实不太优雅(复制源码,更新版本可能导致问题),有兴趣的同学可以进一步研究,也欢迎留言交流。