EggJS从入门到起飞!

以主项目衍生的子项目框架选型为例,分享一下我们在微服务架构下如何从0到1使用EggJS (由于离职需要整理一份项目交接文档,以便新人能够快速上手维护之前的项目,趁此机会整理一份EggJS使用文档)

  1. Express/Koa/EggJS之间的关系
    Express/Koa
    Koa/EggJS
  2. EggJS初始化
    
      $ npm i egg-init -g
      $ egg-init egg-example --type=simple
      $ cd egg-example
      $ npm i
    
  

脚手架走起,完成之后会生成一个简易项目结构,经过各种加班,最终目录结构如下 _config.yml

项目主要分为app、config和其他用的不多的目录。

  • app目录 包含controller/service/extend/router/middleware等

    a) controller目录下根据业务逻辑以实体为单位,用于解析某一类实体的所有接口的输入和返回结果的标准化封装。
    由于我们在项目中引入了egg-swagger-decorator模块,所以在controller中额外要做的是添加每一个接口的注解,如下图:
    _config.yml _config.yml 从而取代了之前在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

如何整合UnitTest到EggJS项目

  • 最后还有根目录的package.json文件中的scripts
    _config.yml 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文件。

  1. EggJS的分层依赖关系
    _config.yml
    框架大多依据”约定大于配置”原则,动态加载子模块,使开发人员更加专注于具体的业务逻辑。
Written on August 26, 2018