2016-2018 @AlloVince
一个基于Node.js的快速构建微服务的开发引擎
- Not framework
- Not production
- 约定项目代码结构
- 约定API及代码规范
- 约定运维及发布规范
- 集成常用工具
约定优于配置
- 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
Webstorm导入项目下airbnb_code_style.xml文件
- 开发:
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.default.js
- config.production.js
- *config.local.production.js (git ignored)
- *Sprint Config Service
注册一个服务
DI.bindClass('redis', Redis);
获得一个服务实例
const redis = DI.get('redis');
Mock一个服务
DI.bindClass(Redis, MockRedis);
router.post('/register', wrapper(async(req, res) => {
const user = await (new models.User()).registerByMobile(req.body, req);
res.json(user);
}));
注册一个指令
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
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);
}
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);
});
- 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
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
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
/blog/posts:
get:
summary: List blog posts
tags:
- Blog
responses:
200:
description: response here
*/
npm run swagger
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;
...
}));
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的代收付订单状态
DI.get('mq').getProducer().produce(new Message({
command: `rc:audit_evaluation --id=${evaluation.id}`
}));
- EvaQueue.js
- EvaPermission.js
- EvaUser.js
- 源代码目录结构
- 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缓存
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) => {});
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
用于切换数据库连接配置
并发模型
- 一个实例采用单进程单线程
- 非容器环境下采用多实例监听不同端口+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');
}
}
** 非内置功能
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 //数据库异常
- TypeScript
- Sequelize V4 upgrades
- Better DI
- Pluggable
- Websockets support
- Koa2 maybe