EggJS从入门到起飞!
以主项目衍生的子项目框架选型为例,分享一下我们在微服务架构下如何从0到1使用EggJS (由于离职需要整理一份项目交接文档,以便新人能够快速上手维护之前的项目,趁此机会整理一份EggJS使用文档)
- Express/Koa/EggJS之间的关系
Express/Koa
Koa/EggJS - EggJS初始化
$ npm i egg-init -g
$ egg-init egg-example --type=simple
$ cd egg-example
$ npm i
脚手架走起,完成之后会生成一个简易项目结构,经过各种加班,最终目录结构如下
项目主要分为app、config和其他用的不多的目录。
-
app目录 包含controller/service/extend/router/middleware等
a) controller目录下根据业务逻辑以实体为单位,用于解析某一类实体的所有接口的输入和返回结果的标准化封装。
由于我们在项目中引入了egg-swagger-decorator
模块,所以在controller中额外要做的是添加每一个接口的注解,如下图:
从而取代了之前在Express框架中痛苦的swagger经历。
正确添加注解之后,只要是本地项目启动期间都可以通过访问http://localhost:7001/swagger-html
查看接口文档。b) service目录下根据业务逻辑以实体为单位,放置每种实体相关的业务逻辑,一般会出现多个service继承一个baseService,baseService中包含常用公共方法,由于我们的项目数据库这块比较特殊(多个数据库之前来回切换),所以会专门在子类service中传递数据库key,在baseService中根据指定key值切换数据库。
每个子类service一般情况下只做和当前实体有关的事情,并且只操作唯一的一个数据库,各司其职。需要多个service相互交互的话,可以直接在当前对象this中获取,如
this.service.pqm.iqcstd
,此处service之后的名称是由EggJS框架按照”约定大于配置“的思想动态加载的,也就是只要在保证代码正常的情况下,所有放在service下的ts文件中的类继承了EggJS中的Service,都会被自动加载后附加到this对象中去,controller/extend目录也是同理。class IQCStdService extends BaseService
{ constructor(ctx: Context) { super(ctx, ctx.app.config.postgres_key, IQCStd); } } abstract class BaseService\<T extends IEntity\> extends Service{ constructor(ctx: Context, connectKey: string, target: ObjectType\ | EntitySchema\ | string) { super(ctx); this._repository = this.app.getDbConnection(connectKey).getRepository(target); } } // extend/application.ts export default{ /** * 通过id获取连接对象 * @param key config配置唯一标识 */ getDbConnection(key?: string): Connection { const app: any = this; return app[key || app.config.postgres_key].create; } } </code> </pre> c) extend目录下是框架的扩展。 d) router文件用来设置服务器向外暴露接口路由,由于我们项目引入了`egg-swagger-decorator`模块,所以不需要在router.js文件列出所有路由。swagger会自动解析后绑定到上下文的router对象上。 e) middleware中间件,用来存放中间件,概念可以参考koa/koa2。 要注意的是Express中的中间件和koa中的中间件是有些区别的,Express中中间件原理就是存在一个全局数组,在加入中间件时push一个function(req,res,next){ // 中间件内容}。项目运行时,队列按顺序运行每个function,完成之后执行next()跳转到下一个中间件。 koa中的中间件原理是洋葱圈模型,先按顺序从外跳转到内部,然后再反向按顺序往外跳回。EggJS中保留中间件是为了koa项目平稳升级到EggJS。 目前我们在中间件这里只用到了JWT权限验证。 f) 另外,我们在app目录下还添加了一个orm目录,此处存放的是用到的所有数据模型。因为数据模型时我们自己添加的,它不会遵循"约定大于配置",在用的时候添加到config.{env}.ts中数据库配置内部,如上述配置的entities/migrations/subscribers/cli等。因为我们在数据模型这里引入的是`typeorm`模块,它是一个code first模式且支持多种数据库,每次启动会通过最新模型去生成/更新数据库表结构,所以是不需要手动创建/修改表。 关于`typeorm`模块使用方法可以参考[官方说明](https://github.com/typeorm/typeorm) - config目录
存放所有的配置文件,包括开发环境/测试环境/生产环境的各种数据库配置/权限/中间件参数/插件是否启用等。
其中约定的配置文件命名为:config.defalut.ts config.local.ts config.prod.ts config.test.ts
config.defalut.ts文件内容例如:
// config.defalut.ts export default (appInfo: EggAppConfig) => { const config: any = exports = {}; // use for cookie sign key, should change to your own and keep security config.keys = appInfo.name + '_1523605522597_8531'; // JWT code config.jwtKey = '我是马赛克'; config.middleware = ['auth']; config.auth = { ignore: [ '/service/api/hello', '*/swagger-json*', '*/swagger-html*', '/service/api/material/pull' ] }; // 跨域 config.cors = { origin: '*', allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS', }; config.security = { methodnoallow: { enable: false, }, csrf: { enable: false, }, }; ... } // config.local.ts export default () => { const config: any = exports = {}; // 我方数据库连接 config.postgres_key = '5b260fd092d3db4a64bbcae3'; config.postgres = { key: config.postgres_key, type: 'postgres', enable: true, host: '127.0.0.1', port: 5432, username: 'postgres', password: '我又是马赛克', database: 'mydb', synchronize: true, logging: false, entities: [ 'app/orm/entity/**/*.ts', ], migrations: [ 'app/orm/migration/**/*.ts', ], subscribers: [ 'app/orm/subscriber/**/*.ts', ], cli: { entitiesDir: 'app/orm/entity', migrationsDir: 'app/orm/migration', subscribersDir: 'app/orm/subscriber', }, }; // BOHAN_PU数据库连接 config.mssql_key = '5b261dad92d3db4a64bbcae4'; config.mssql = { ... }; return config; }; // config.prod.ts export default () => { const config: any = exports = {}; const pgConfig = JSON.parse(process.env.PGConfig || `{ ... }`); // 我方数据库连接 config.postgres_key = '5b260fd092d3db4a64bbcae3'; config.postgres = { ... }; // BOHAN_PU数据库连接 config.mssql_key = '5b261dad92d3db4a64bbcae4'; config.mssql = { ... }; return config; }; // config.test.ts export default () => { const config: any = exports = {}; // 我方数据库连接 config.postgres_key = '5b260fd092d3db4a64bbcae3'; config.postgres = { ... }; // BOHAN_PU数据库连接 config.mssql_key = '5b261dad92d3db4a64bbcae4'; config.mssql = { ... }; return config; };
如上配置,在不同启动参数下会启用相应的配置,
在本地环境时会采用Object.assign(config.local,config.default); 在测试环境Object.assign(config.test,config.default);
在生产环境Object.assign(config.prod,config.default);
default文件和具体配置文件的关系类似于父子类的关系。plugin.ts文件中用来设置插件的启用状态
export default { cors: { enable: true, package: 'egg-cors', }, logrotator: { enable: true, package: 'egg-logrotator', } };
- 根目录的app.ts,还有做集群时的agent.ts文件等。
我们在app.ts来设置app的一些事件,如beforeStart事件中的创建数据库连接(此处创建一个PG,一个SQL server),配置读取自config
export default (app: Application) => {
console.log(`current env:${app.env}`);
app.beforeStart(async () => {
const configs: any = _.filter([
app.config.postgres,
app.config.mssql
], x => x.enable);
const connections = await createConnections(configs);
_.each(connections, (x: any) => {
app.addSingleton(x.options.key, x);
console.log(`createConnection:${x.options.type},${x.options.host}:${x.options.port}:${x.options.database}`);
});
});
};
- test目录下是单元测试文件,文件命名格式为${fileName}.test.ts,统一使用
egg-bin
运行测试脚本,自动将内置的mocha、co-mocha、power-assert、nyc等模块组合引入测试脚本。
在测试时运行package.json中的脚本npm test
。
- 最后还有根目录的package.json文件中的scripts
start
:用于开启cluster集群,一个nodejs大杀器。启动方式为npm run start时默认开启1个Master主进程,CPU同等数量个Worker工作进程。可以设置参数–workers=n,指定开启n个进程。
stop
:相应的关闭集群命令。由于start命令中带有daemon参数,此参数用于开启守护进程,如果工作进程或主进程被强制关掉,守护进程会自动开启新进程。所以正常关闭带有守护进程的集群需要使用stop正常关闭。
dev
:一般情况下本地开发运行的命令。
tsc
:手动编译ts为js文件,检查代码报错位置。
clean
:一键式清理tsc命令编译后产生的js/map.js文件。
- EggJS的分层依赖关系
框架大多依据”约定大于配置”原则,动态加载子模块,使开发人员更加专注于具体的业务逻辑。