背景
在开发一些通用服务的情况下, 往往会有不可避免的出现一些特殊条件下的定制需求, 这种情况下比较常见的有三种实现模式
- 暴力开发: 使用
if/else
填充逻辑
- 面向对象抽象实现: 使用接口或继承实现特殊条件下的逻辑处理
- 定制插件: 在主要代码中加入相关生命周期和
hooks
入口, 使用不同插件去hook
各自需要处理的地方, 耦合性较小, 开发成本较高
对于我目前项目的需求特点来说, 有基本明确统一的流程, 而且是用于后端服务, 不会有太多特殊处理的地方, 使用方案2
会是比较合理的选择, 接下来就是看看基于egg
如何比较优雅的实现
开发
router
既然是通用服务, 对外暴露的api
接口最好是稳定和一致的, 直接按常规定义路由即可. 如何区分项目条件后续再处理.
1 2 3
| 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
| 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
| 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
| class CommonApiService extends Service { get projectConfig() { return this.app.config.projectConfig[this.ctx.project]; } async getInfo() { return info; } async getList() { return list; } }
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
| const projectConfig = require('./project.config.default');
config.projectConfig = projectConfig(appInfo);
const testproject = require('./testproject');
module.exports = (appInfo) => { const projectConfig = (exports = {});
projectConfig.testproject = testproject(appInfo);
return projectConfig; };
module.exports = (appInfo) => { return { project: 'testproject', host: 'testproject-host', }; };
|
nginx
最后补充一下nginx
的自定义header
的配置方法, 部署在不同地方的nginx
的header
头可以根据环境变量在启动时设置进去, 也可以根据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
|
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}'`); }); });
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}'`); }); });
|
小结
这串流程下来就实现了一个既满足通用性, 又支持高度定制化的可复用的项目框架, 在用于通用的对内/对外服务上可以实现快速稳定的开发迭代