Skip to content

Instantly share code, notes, and snippets.

@AlloVince
Last active May 7, 2019 08:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlloVince/cb92c08e0b1c9e5b8b4f6becae12cc5d to your computer and use it in GitHub Desktop.
Save AlloVince/cb92c08e0b1c9e5b8b4f6becae12cc5d to your computer and use it in GitHub Desktop.
npm install -g reveal-md && wget -q -O EvaEngine.js.md https://gist.githubusercontent.com/AlloVince/cb92c08e0b1c9e5b8b4f6becae12cc5d/raw/EvaEngine.js.md && reveal-md EvaEngine.js.md

EvaEngine.js

2016-2018 @AlloVince


什么是EvaEngine.js


一个基于Node.js的快速构建微服务的开发引擎


Engine

  • Not framework
  • Not production

  1. 约定项目代码结构
  2. 约定API及代码规范
  3. 约定运维及发布规范
  4. 集成常用工具

约定优于配置


Key points

  • async / await
  • Babel
  • Dependency Injection
  • Exception
  • ORM
  • Mock
  • async_hooks

重要依赖

  • Node.js >= v6.0
  • Web Server: Express
  • DI: constitute
  • Logger: winston
  • Unit test: ava
  • ORM: sequelize
  • Http Client: request
  • CLI: yargs
  • API doc: swagger

组成

  • Router = Controller
  • Entity = Pure ORM schema define
  • Model = Main business processor based on entities
  • Service = Global dependency (Without entities)
  • Middleware = Special service, output is function

代码风格

Airbnb JavaScript Style Guide

Webstorm导入项目下airbnb_code_style.xml文件


快速开始

基于脚手架 EvaSkeleton.js

  • 开发: npm run dev
  • 构建: npm run build
  • 测试: npm run ava
  • 文档: npm run swagger
  • 生成 ORM Entity: ./engine make:entity
  • 生成 Graphql Schema: ./engine make:graphql

主要功能


环境变量切换

NODE_ENV=production node build/app.js
  • production
  • test
  • development
  • 缺少unittest

可以配合使用 dotenv


Config Merge

- config.default.js
  - config.production.js
    - *config.local.production.js (git ignored)
      - *Sprint Config Service

DI支持

注册一个服务

DI.bindClass('redis', Redis);

获得一个服务实例

const redis = DI.get('redis');

Mock一个服务

DI.bindClass(Redis, MockRedis);

ES语法支持

router.post('/register', wrapper(async(req, res) => {
  const user = await (new models.User()).registerByMobile(req.body, req);
  res.json(user);
}));

CLI支持

注册一个指令

class CreateUser extends Command {
  static getName() {
    return 'user:create';
  }
  async run() {
    const userModel = new models.User();
    return await userModel.create(this.getOptions());
  }
}

运行指令

node build/cli.js user:create 

显示所有支持的指令

node build/cli.js

ORM为主的数据库操作

const transaction = await entities.getTransaction();
try {
  contact = await entities.get('Contacts').create(input, { transaction });
  await transaction.commit();
} catch (e) {
  await transaction.rollback();
  throw new exceptions.DatabaseIOException(e);
}

基于AVA和mock的快速测试

test.serial('通过手机号密码登陆', async(t) => {
  const res = await runController(authController, mockRequest({
    method: 'POST',
    url: '/login/password',
    body: {
      identify: mobile,
      password
    }
  }));
  t.is(res.uid, user.id);
});

Log级别

  • error
  • info
  • verbose
  • debug

Web模式

LOG_LEVEL=debug node build/app.js
development default: debug
production default: error

CLI模式

node build/cli.js hello:world -vvv
-vvv debug
-vv  verbose
-v   info
default: info

支持分布式追踪(Zipkin compatible)

app.use(DI.get('trace')('SampleApp'));
DI.get('rest_client').request({ url: 'http://example.com'});
X-B3-ParentSpanId:
X-B3-Sampled:1
X-B3-SpanId:molvsy94rzybpcjp
X-B3-TraceId:molvsy94rzybpcjp
X-Powered-By:Express
X-Requested-At:1481616515182000
X-Response-Milliseconds:79.474
X-Service-Name:SampleApp
2018-04-19T15:49:14+08:00 - verbose: [web3000] Executed (default): SELECT `id`, `userId`, `content`, `createdAt` FROM `eva_riskcontrol_sms_records` AS `SmsRecords` WHERE `SmsRecords`.`userId` = 68990; 580  molvsy94rzybpcjp

内置AccessLog

app.use(DI.get('debug')());

日志规范: NCSA Combined Log Format

2018-04-19T15:51:18+08:00 - info: [web3000] ::1 - - [19/Apr/2018:07:51:18 +0000] "POST /v2/graphql/api? HTTP/1.1" 400 - "http://localhost:3000/v2/graphql/ui?query=%7B%0A%20%20health%0A%20%20smsRecords%20(userId%3A68990)%20%7B%0A%20%20%20%20recordCount%0A%20%20%20%20records%20%7B%0A%20%20%20%20%20%20%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20callRecords(userId%3A68990)%20%7B%0A%20%20%20%20serialCallPairs%0A%20%20%20%20liveCityCode%0A%20%20%20%20twoWayNumbers%0A%20%20%20%20monthlyBill%0A%20%20%7D%0A%0A%7D%0A" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" "::1" localhost:3000 2.178 - 1.1 3000 AlloVincedeMacBook-Pro.local - - - paydayloan k92jqbiodiqc76dh k92jqbiodiqc76dh - 1 k92jqbiodiqc76dh
NCSA是National Center for Supercomputing Applications
(美国国家超级电脑应用中心)的缩写,NCSA研发了CGI与Apache,
因此NCSA经常被指带Log日志规范,也是Apache的日志规范

基于注释生成Swagger Docs

/**
 @swagger
 /blog/posts:
   get:
     summary: List blog posts
     tags:
       - Blog
     responses:
       200:
         description: response here
*/
npm run swagger

基于JWT+Redis的权限验证

if(loginSuccess) {
  const tokenString = await DI.get('jwt').save(uid, {
    uid,
    expiredAt
  });
}
GET /me
X-Token: $tokenString
router.get('/me', DI.get('auth')(), wrapper(async (req, res) => {
  const { uid, token } = req.auth;
  ...
}));

基于Joi的输入校验

router.post('/mobile', validator(v => ({
  body: {
    mobile: v.string().required().mobile(),
    ticket: v.string().required()
  }
})), wrapper(async (req, res) => {
}));

定时任务

Crontab like job

engine.runCrontab('0/1 * * * * ', `${TransactionCommands.SyncTimeoutProcessingOrder.getName()} --type=agency`, false);
//每1分钟    同步一笔30分钟前状态仍为processing的代收付订单状态

Worker

DI.get('mq').getProducer().produce(new Message({
  command: `rc:audit_evaluation --id=${evaluation.id}`
}));

扩展

  • EvaQueue.js
  • EvaPermission.js
  • EvaUser.js

Dive Into...

  • 源代码目录结构
  • Service一览
  • Middleware一览
  • Swagger生成原理
  • 自定义错误处理
  • 如何联调
  • 环境变量

源代码目录结构

├── commands   // CLI相关
├── config     // 默认配置
├── di.js      // DI
├── engine.js  //入口
├── entities   // ORM entity 基类
├── exceptions  // 异常
├── index.js    
├── middlewares  //中间件
├── services     //服务
├── swagger      //文档生成
└── utils        //工具类

服务一览

├── cache.js           //缓存管理
├── config.js          //配置文件管理
├── env.js             //环境变量管理
├── event_manager.js   //事件注册管理
├── http_client.js     //http客户端
├── joi.js             //用户输入校验
├── jwt_token.js       //Json web token
├── jwt_token_kong.js  //Json web token with kong
├── logger.js          //Logger
├── namespace.js       //Thread local
├── now.js             //时间
├── providers.js       //Service providers
├── redis.js           //Redis
└── rest_client.js     //对应RESTFul接口的http客户端

中间件一览

├── auth.js           //基于Token的权限验证
├── auth_kong.js      //基于Token + Kong的权限验证
├── debug.js          //morgan
├── session.js        //Session
├── trace.js          //分布式服务追踪
├── validator.js      //Joi封装
└── view_cache.js     //基于时间的View缓存

Swagger生成原理

ES7 Files =(Babel)=>
ES5 Files =(acorn)=>
AST =(filter)=>
Annotations =(doctrine)=>
JsDocs =(convert)=>
Fragments + EvaEngine Exceptions + Sequelize Models =(Merge & Compile)=>
Swagger Specification JSON File

自定义错误处理

before run

engine.setDefaultErrorHandler((err, req, res, next)  => {});
engine.setUncaughtExceptionHandler((err) => {});

注册Service

class FooProvider extends ServiceProvider {
  get name() {
    return 'foo';
  }

  register() {
    DI.bindClass(this.name, FooClass);
  }
}

engine.registerServiceProviders([FooProvider])

如何联调

cd EvaNode
npm link
cd your_project
npm link evaengine

环境变量

  • NODE_ENV 区分开发生产测试环境
  • PORT Web服务端口
  • LOG_LEVEL Log级别
  • CLI_NAME CLI在log中的显示
  • MAX_REQUEST_DEBUG_BODY http client最大body长度
  • SEQUELIZE_REPLICATION_CONFIG_KEY 用于切换数据库连接配置

Devops

并发模型

  • 一个实例采用单进程单线程
  • 非容器环境下采用多实例监听不同端口+LB方式

异常设计

统一使用抛出异常作为错误处理的方式

async bindMobile(mobile, ticket) {
  assert.ok(mobile && ticket, 'input not correct');
  if (await Ticket.verifyTicket(mobile, ticket) === false) {
    throw new TicketNotMatchException('Ticket not match');
  }
}

i18n 支持

** 非内置功能

throw (new ResourceConflictedException())
  .i18n('User with mobile %s already exists', mobile);

I18N JSON File

{
  "User with mobile %s already exists": "已存在手机号为%s的用户"
}

- `StandardException extends Error` 500  //异常基类
  - `LogicException` 400  //逻辑异常 (预期异常,希望告知用户原因)
  - `RuntimeException` 500  //系统异常(预期外,记录并报警)

  - `LogicException` 400  //逻辑异常 (预期异常,希望告知用户原因)
     - `InvalidArgumentException` 400  //用户输入参数异常
        - `FormValidateException` 400   //表单验证异常(一般由validator中间件抛出)
        - `ModelInvalidateException` 400 //ORM校验异常
        - `HttpRequestLogicException` 400 //远程调用逻辑异常
        - `RestServiceLogicException` 400 //REST远程调用逻辑异常
    - `UnauthorizedException` 401 //权限异常(一般由auth中间件抛出)
    - `OperationNotPermittedException`  403 //行为被禁止
    - `ResourceNotFoundException`  404  //资源不存在
    - `OperationUnsupportedException` 405 //用户操作不支持
    - `ResourceConflictedException` 409 //资源冲突

  - `RuntimeException` 500  //系统异常(预期外,记录并报警)
    - `IOException` 500         //系统IO异常
      - `HttpRequestIOException` 500 //远程系统异常
        - `RestServiceIOException` 500 //远程REST服务异常
      - `DatabaseIOException`500 //数据库异常

TODO

  • TypeScript
  • Sequelize V4 upgrades
  • Better DI
  • Pluggable
  • Websockets support
  • Koa2 maybe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment