本文主要内容- 文档生成工具:基于插件的swagger-doc接口定义- 统一异常处理- 基于扩展的helper响应统一处理- validate接口格式检查- 三层结构
推荐直接使用脚手架,只需几条简单指令,即可快速生成项目:
$ mkdir egg-example && cd egg-example$ npm init egg --type=simple$ npm i
启动项目:
$ npm run dev$ open http://localhost:7001
Swagger – The World's Most Popular Framework for APIs.
swagger会扫描配置的API文档格式自动生成一份json数据,而swagger官方也提供了ui来做通常的展示,当然也支持自定义ui的。不过对后端开发者来说,能用就可以了,官方就可以了。
swagger-doc可以做两件事。
npm install egg-swagger-doc-feat -s
在插件中注册:
// config/plugin.js'use strict';
/** @type Egg.EggPlugin */module.exports = { // had enabled by egg // static: { // enable: true, // }
// config/plugin swaggerdoc: { enable: true, package: 'egg-swagger-doc-feat', }};
在config下的config.default.js添加如下配置
config.swaggerdoc = { dirScanner: './app/controller', apiInfo: { title: 'djtao接口', description: 'djtao接口 swagger-ui for egg', version: '1.0.0', }, schemes: ['http', 'https'], consumes: ['application/json'], produces: ['application/json'], enableSecurity: false, // enableValidate: true, routerMap: true, enable: true, }
在controller下新建user.js,写一个创建用户接口
// app/controller/user.jsconst Controller = require('egg').Controller/*** @Controller 用户管理*/class UserController extends Controller { constructor(ctx) { super(ctx) }
/** 注意这段描述 * @summary 创建用户 * @description 创建用户,记录用户账户/密码/类型 * @router post /api/user * @request body createUserRequest *body * @response 200 baseResponse 创建成功 */ async create() { const { ctx } = this ctx.body = 'user ctrl' }}module.exports = UserController
注意,描述里用到了createUserRequest对象 在app下新建一个contract目录,新建index.js(定义标准接口的请求和返回格式)
// app/contract/index.jsmodule.exports = { baseRequest: { id: { type: 'string', description: 'id 唯一键', required: true, example: '1' }, }, baseResponse: { code: { type: 'integer', required: true, example: 0 }, data: { type: 'string', example: '请求成功' }, errorMessage: { type: 'string', example: '请求成功' }, },};
再在contract下新建user.js(定义添加用户接口的字段)
// app/contract/index.jsmodule.exports = { createUserRequest:{ mobile:{type:'string',required:true,descption:'手机号码',example:'13800138000',format:/^1[34578]\d{9}$/ }, password:{type:'string',required:true,descption:'密码',example:'123456'}, realName:{type:'string',required:true,descption:'姓名',example:'djtao'}, }};
配置完成后,启动项目访问http://localhost:7001/swagger-ui.html,就可以看到文档了。包括类型,还带上了测试用例。
全部接口文档包括:http://localhost:7001/swagger-doc
有了它,可以很方便地在注释创建接口,不需要再创建路由了!
前一篇文章讲了手撸degg,如何实现捕获异常的中间件,现在看看在egg中怎么做(方法和思维几乎一样):
// /middleware/error_handler.js'use strict'module.exports = (option, app) => { return async function (ctx, next) { try { await next() } catch (err) { // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 app.emit('error', err, this) const status = err.status || 500 // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 const error = status === 500 && app.config.env === 'prod' ? 'Internal Server Error' : err.message // 从 error 对象上读出各个属性,设置到响应中 ctx.body = { code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开 始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 error: error } if (status === 422) { ctx.body.detail = err.errors } ctx.status = 200 } }}
写好之后再config中注册
// config.default.jsconfig.middleware = ['errorHandler']
我们测试一下:
在del接口添加一个错误的a.a.a=1,运行:
异常处理的好处在于:统一数据返回格式。这是对前端最友好的报错。
Helper 函数用来提供一些实用的 通用方法。
它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可以让我们更容易 编写测试用例。
框架内置了一些常用的 Helper 函数。
你可以通过 ctx.helper 调用
我们也可以编写自定义扩展的 Helper 。
应用开发中,我们可能经常要自定义一些 helper 方法,例如上面例子中的 formatUser
,我们可以通过框架扩展的形式来自定义 helper 方法。
在这里我们定义了两个方法。
// extend/helper.jsconst moment = require('moment') // 格式化时间exports.formatTime = time => moment(time).format('YYYY-MM-DD HH:mm:ss')// 处理成功响应exports.success = ({ ctx, res = null, msg = '请求成功' })=> { ctx.body = { code: 0,data: res,msg} ctx.status = 200}
调用时可以直接 ctx.sucess({ctx,res})
/** * @summary 获取当前时间 * @description 删除* @router get /api/user/now * @response 200 baseResponse 创建成功 */ async now() { const { ctx } = this let now = ctx.helper.formatTime(new Date()) // a.a.a=1 ctx.helper.success({ ctx, res: now }) }
安装egg-validate校验插件:
npm i egg-validate -s
注册插件;
// plugin.js validate: { enable: true, package: 'egg-validate', },
对注册用户进行校验:
/** * @summary 创建用户 * @description 创建用户,记录用户账户/密码/类型 * @router post /api/user * @request body createUserRequest *body * @response 200 baseResponse 创建成功 */ async create() { const { ctx, service } = this // 校验参数 ctx.validate(ctx.rule.createUserRequest) ctx.body = 'user ctrl' }
现在模拟一下post请求:
npm install egg-mongoose -s
在插件里补上这么一句:
// plugin.jsmongoose : { enable: true, package: 'egg-mongoose',},
// config.default.js
// 连接到mongodb config.mongoose = { url: 'mongodb://127.0.0.1:27017/egg_x', options: { // useMongoClient: true, autoReconnect: true, reconnectTries: Number.MAX_VALUE, bufferMaxEntries: 0, }, }
接下来就是定义MVC框架了。
首先一定要明白:model层定义数据表结构,service定义业务方法,controller负责业务处理
// model/user.jsmodule.exports = app => { const mongoose = app.mongoose const UserSchema = new mongoose.Schema({ mobile: { type: String, unique: true, required: true }, password: { type: String, required: true }, realName: { type: String, required: true }, avatar: { type: String, default:'https://1.gravatar.com/avatar/a3e54af3cb6e157e496ae430aed4f4a3?s=96&d=mm'}, extra: { type: mongoose.Schema.Types.Mixed }, createdAt: { type: Date, default: Date.now }}) return mongoose.model('User', UserSchema)}
先安装哈希依赖:
npm install egg-bcrypt -s
在插件中定义:
bcrypt : { enable: true, package: 'egg-bcrypt' }
创建server层,直接调用mongoose的 create
API。
// server/user.jsconst Service =require('egg').Service;
class UserService extends Service{
/** * 创建用户 * @param {*} payload */ async create(payload){ const {ctx}=this; payload.password=await ctx.genHash(payload.password); return ctx.model.User.create(payload) }
}
module.exports=UserService;
/** * @summary 创建用户 * @description 创建用户,记录用户账户/密码/类型 * @router post /api/user * @request body createUserRequest *body * @response 200 baseResponse 创建成功 */ async create() { const { ctx, service } = this // 校验参数 ctx.validate(ctx.rule.createUserRequest) // 组装参数 const payload = ctx.request.body || {} // 调用 Service 进行业务处理 const res = await service.user.create(payload) // 设置响应内容和响应状态码 ctx.helper.success({ctx, res}) }
结果显示请求成功。
基本思路:就是在service层定义方法,在controller层调用方法。
// service/user.js /** * 更新用户信息 * @param {*} id * @param {*} values */ async findByIdAndUpdate(id, values) { return this.ctx.model.User.findByIdAndUpdate(id, values) }
// controller/user.js /** * @summary 修改用户 * @description 获取用户信息 * @router put /api/user/ * @response 200 baseResponse 创建成功 * @ignore */ async update() { const { ctx, service } = this // 校验参数 ctx.validate(ctx.rule.createUserRequest) // 组装参数 const { id } = ctx.params const payload = ctx.request.body || {} // 调用 Service 进行业务处理 await service.user.update(id, payload) // 设置响应内容和响应状态码 ctx.helper.success({ ctx }) }
// service/user.js /** * 删除用户 * @param {*} _id */ async destroy(_id) { const { ctx, service } = this const user = await ctx.service.user.find(_id) if (!user) { ctx.throw(404, 'user not found') } return ctx.model.User.findByIdAndRemove(_id) }
// controller/user.js /** * @summary 删除单个用户 * @description 删除单个用户 * @router delete /api/user/{id} * @request path string *id eg:1 用户ID * @response 200 baseResponse 创建成功 */ async destroy() { const { ctx, service } = this // 校验参数 const { id } = ctx.params // 调用 Service 进行业务处理 await service.user.destroy(id) // 设置响应内容和响应状态码 ctx.helper.success({ ctx }) }
// service/user.js /** * 删除多个用户 * @param {*} payload */ async removes(payload) { return this.ctx.model.User.remove({ _id: { $in: payload } }) }
// controller/user.js /** * @summary 删除所选用户 * @description 获取用户信息 * @router delete /api/user/{id} * @request path string *id * @response 200 baseResponse 创建成功 */ async removes() { const { ctx, service } = this // 组装参数 // const payload = ctx.queries.id const { id } = ctx.request.body const payload = id.split(',') || [] // 调用 Service 进行业务处理 const result = await service.user.removes(payload) // 设置响应内容和响应状态码 ctx.helper.success({ ctx }) }
// service/user.js /** * 查看单个用户 */ async show(_id) { const user = await this.ctx.service.user.find(_id) if (!user) { this.ctx.throw(404, 'user not found') } return this.ctx.model.User.findById(_id).populate('role') }
// controller/user.js /** * @summary 获取单个用户 * @description 获取用户信息 * @router get /api/user/{id} * @request url baseRequest * @response 200 baseResponse 创建成功 */ async show() { const { ctx, service } = this // 组装参数 const { id } = ctx.params // 调用 Service 进行业务处理 const res = await service.user.show(id) // 设置响应内容和响应状态码 ctx.helper.success({ ctx, res }) }
列表筛查的条件就比较多了。
// service/user.js/** * 查看用户列表 * @param {*} payload */ async index(payload) { const { currentPage, pageSize, search } = payload let res = [] let count = 0 let skip = ((Number(currentPage)) - 1) * Number(pageSize || 10)
if (search) { res = await this.ctx.model.User.find({ mobile: { $regex: search } }).populate('role').skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() count = res.length } else { res = await this.ctx.model.User.find({}).populate('role').skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() count = await this.ctx.model.User.count({}).exec() }
// 整理数据源 -> Ant Design Pro let data = res.map((e, i) => { const jsonObject = Object.assign({}, e._doc) jsonObject.key = i jsonObject.password = 'Are you ok?' jsonObject.createdAt = this.ctx.helper.formatTime(e.createdAt) return jsonObject })
return { count: count, list: data, pageSize: Number(pageSize), currentPage: Number(currentPage) } }
// controller/user.js /** * @summary 获取所有用户(分页/模糊) * @description 获取用户信息 * @router get /api/user * @request query integer *currentPage eg:1 当前页 * @request query integer *pageSize eg:10 单页数量 * @request query string search eg: 搜索字符串 * @request query boolean isPaging eg:true 是否需要翻页 * @response 200 baseResponse 创建成功 */ async index() { const { ctx, service } = this // 组装参数 const payload = ctx.query // 调用 Service 进行业务处理 const res = await service.user.index(payload) // 设置响应内容和响应状态码 ctx.helper.success({ ctx, res }) }
https://eggjs.org/zh-cn/basics/app-start.html
我们常常需要在应用启动期间进行一些初始化工作,等初始化完成后应用才可以启动成功,并开始对外提供服务。 框架提供了统一的入口文件(
app.js
)进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。 框架提供了这些 生命周期函数供开发人员处理:
configWillLoad
)configDidLoad
)didLoad
)willReady
)didReady
)serverDidReady
)beforeClose
)这七个全局相关的api,必须在根目录下app.js进行定义。
//app.js/*** 全局定义* @param app*/// app.jsclass AppBootHook { constructor(app) { this.app = app; }
configWillLoad() { // 此时 config 文件已经被读取并合并,但是还并未生效 // 这是应用层修改配置的最后时机 // 注意:此函数只支持同步调用
// 例如:参数中的密码是加密的,在此处进行解密 // this.app.config.mysql.password = decrypt(this.app.config.mysql.password); // 例如:插入一个中间件到框架的 coreMiddleware 之间 // const statusIdx = this.app.config.coreMiddleware.indexOf('status'); // this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit'); }
async didLoad() { // 所有的配置已经加载完毕 // 可以用来加载应用自定义的文件,启动自定义的服务
// 例如:创建自定义应用的示例 // this.app.queue = new Queue(this.app.config.queue); // await this.app.queue.init();
// 例如:加载自定义的目录 // this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', { // fieldClass: 'tasksClasses', // }); }
async willReady() { // 所有的插件都已启动完毕,但是应用整体还未 ready // 可以做一些数据初始化等操作,这些操作成功才会启动应用
// 例如:从数据库加载数据到内存缓存 // this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL); }
async didReady() { // 应用已经启动完毕 console.log('========Init Data=========') // 创建匿名上下文 const ctx = await this.app.createAnonymousContext(); await ctx.model.User.remove(); //清理 // 新增 await ctx.service.user.create({ mobile: '13800138000', password: '123456', realName: 'dangjingtao', }) }
async serverDidReady() { // http / https server 已启动,开始接受外部请求 // 此时可以从 app.server 拿到 server 的实例
// this.app.server.on('timeout', socket => { // handle socket timeout // }); }}
module.exports = AppBootHook;