Eggjs实现通用可定制化服务

背景

在开发一些通用服务的情况下, 往往会有不可避免的出现一些特殊条件下的定制需求, 这种情况下比较常见的有三种实现模式

  1. 暴力开发: 使用if/else填充逻辑
  2. 面向对象抽象实现: 使用接口或继承实现特殊条件下的逻辑处理
  3. 定制插件: 在主要代码中加入相关生命周期和hooks入口, 使用不同插件去hook各自需要处理的地方, 耦合性较小, 开发成本较高

对于我目前项目的需求特点来说, 有基本明确统一的流程, 而且是用于后端服务, 不会有太多特殊处理的地方, 使用方案2会是比较合理的选择, 接下来就是看看基于egg如何比较优雅的实现

开发

router

既然是通用服务, 对外暴露的api接口最好是稳定和一致的, 直接按常规定义路由即可. 如何区分项目条件后续再处理.

1
2
3
// router.js
router.get(`${urlPrefix}/info`, controller.api.getInfo);
router.get(`${urlPrefix}/list`, controller.api.getList);

middleware

请求在经过router之前, 就会经过全局middleware处理, 随后再经过router指定的中间件, 但是它们本质上是不会互相影响的, 而且对外往往考虑是先看router, 所以将middleware写在router之后也没有太大的理解问题

在此中间件中需要区分出具体的请求来源和条件, 因为服务前面有一层nginx, 所以选择了简洁的配置方案: 通过http-header传参来区分具体项目, 对于nginx转发配置起来也非常简单, 也便于调试

这里使用http-header来传参的原因是因为项目不会特别大, 为了节省成本只会部署一套通用服务. 如果是用于大型项目的情况下, 更好的方案是通过环境变量在启动时指定服务项目参数, 就不需要通过nginx和其他配置来指定项目了
1
2
3
// middleware/wrapper.js
ctx.project = ctx.header['x-project'];
ctx.validate(ctx.project); // 验证是否符合

controller

router已经将所有同类的请求都转发到了controller/api.js, middleware也已经区分好了访问的项目, 然后分发的逻辑在这里处理就会非常简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// controller/api.js
class ApiController extends Controller {
get projectService() {
const service = this.service[`${this.ctx.project}ProjectApi`];
if (!service) {
return this.service.commonApi;
}
return service;
}
async getInfo() {
return await this.projectService.getInfo();
}
async getList() {
return await this.projectService.getList();
}
}

service

service的架构是通用与可定制化的核心, 将基础服务用baseService实现, 其他定制服务继承它, 实现需要定制的接口

用继承还有个好处是方便代码编辑器自动给出方法提示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// service/common-api.js
class CommonApiService extends Service {
get projectConfig() {
return this.app.config.projectConfig[this.ctx.project];
}
async getInfo() {
return info;
}
async getList() {
return list;
}
}

// service/test-project-api.js
class TestProjectService extends CommonApiService {
async getInfo() {
const info = await super.getInfo();
// 自定义修改逻辑
return info;
}
}

config

可以看到在service中, 有个读取不同项目的配置的地方, 配置的加载和管理也是比较简洁的方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// config/config.default.js
const projectConfig = require('./project.config.default');

config.projectConfig = projectConfig(appInfo);

// config/project.config.default/index.js
const testproject = require('./testproject');

module.exports = (appInfo) => {
const projectConfig = (exports = {});

projectConfig.testproject = testproject(appInfo);

return projectConfig;
};

// config/project.config.default/testproject.js
module.exports = (appInfo) => {
return {
project: 'testproject',
host: 'testproject-host',
};
};

nginx

最后补充一下nginx的自定义header的配置方法, 部署在不同地方的nginxheader头可以根据环境变量在启动时设置进去, 也可以根据server或者其他参数固定写入

1
2
3
4
5
6
7
8
# default.conf

location /api/v1/ {
proxy_pass http://api-server:7001;
proxy_set_header X-project "testproject";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect default;
}

测试

unittest

由于使用header来作为区分方案, 那么单元测试实现也会非常简单和稳定

如果需要只测试某个项目, 可以指定测试参数: npm run test ./test/app/controller/api/testproject.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// test/app/controller/api/common.test.js

const project = path.basename(__filename, '.test.js');

describe(`test/app/controller/${project}.test.js`, () => {
const urlPrefix = '/api/v1';
let app;
let job;
before(async () => {
const _init = await init();
app = _init.app;
job = _init.job;
});

it(`should GET GAME: ${project} [getInfo] ${urlPrefix}/getInfo`, async () => {
const req = await app.httpRequest().get(`${urlPrefix}/getInfo`).query(reqdata).set('x-project', project);
assert.ok(_.isString(req.body.data), `response result is '${req.body.data}'`);
assert.ok(_.isString(req.body.data), `response result is '${req.body.data}'`);
});
});

// test/app/controller/api/testproject.test.js

const project = path.basename(__filename, '.test.js');

describe(`test/app/controller/${project}.test.js`, () => {
const urlPrefix = '/api/v1';
let app;
let job;
before(async () => {
const _init = await init();
app = _init.app;
job = _init.job;
});

it(`should GET GAME: ${project} [getInfo] ${urlPrefix}/getInfo`, async () => {
const req = await app.httpRequest().get(`${urlPrefix}/getInfo`).query(reqdata).set('x-project', project);
assert.ok(_.isString(req.body.data), `response result is '${req.body.data}'`);
assert.ok(_.isString(req.body.data), `response result is '${req.body.data}'`);
});
});

小结

这串流程下来就实现了一个既满足通用性, 又支持高度定制化的可复用的项目框架, 在用于通用的对内/对外服务上可以实现快速稳定的开发迭代

作者

Mosby

发布于

2018-11-26

许可协议

评论