​eggjs实战

本文主要内容- 文档生成工具:基于插件的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-doc:

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 函数用来提供一些实用的 通用方法。

它的作用在于我们可以将一些常用的动作抽离在 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请求:

连接文档型数据库(mongodb)

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

// 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)}

service

先安装哈希依赖:

 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;

修改controller层

   /**    * @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})    }

结果显示请求成功。

练习:实现增删改查整套接口(熟悉API调用)

基本思路:就是在service层定义方法,在controller层调用方法。

更新数据(findByIdAndUpdate)
 // 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 })    }
删除
单个用户(findByIdAndRemove)
// 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 })    }
批量删除(removes(【id数组】))
// 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 })    }
查询
查询一个(indById(_id))
// 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
  • worker 准备就绪( 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;

本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309),作者:一li小麦

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • koa实践及其手撸

    Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石...

    一粒小麦
  • 前后端权限机制

    本项目使用vue全家桶,axios和cube-ui cube-ui文档地址:https://didi.github.io/cube-ui/#/zh-CN/doc...

    一粒小麦
  • React 组件化开发(二):最新组件api

    如果连这点觉悟都没有,那就不是一个合格的程序员。而雇主的本质是逐利,最忌讳的是重构,这个问题可以请高水平的工程师来得到缓解,但不可能彻底解决。

    一粒小麦
  • 【Koa】385- koa框架的快速入门与使用

    Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石...

    pingan8787
  • 治电EggJS开发规范

    函数/方法注释放置于函数/方法的上方,主要描述函数/方法功能以及参数类型,参数和返回值说明

    治电小白菜
  • 探究 canvas 绘图中撤销(undo)功能的实现方式

    最近在做网页版图片处理相关的项目,也算是初入了 canvas 的坑。项目需求中有一个给图片添加水印的功能。我们知道,在浏览器端实现图片添加水印功能,通常的做法就...

    逆葵
  • 引用类型

    在JavaScript中引用类型是一种数据结构,将数据和功能组织在一起,或者也称之为类,但是在ECMAScript并不真正具有类,所以我们都把引用类型称之为对象...

    wade
  • 为什么我的样式不起作用?

    大概看一下代码,是有一个Parent的父组件,蓝底白字。还有一个Child的子组件,红底黑字。 那么实际渲染出的样式是什么样子的呢。如下图:

    w候人兮猗
  • Clojure 学习入门(5)—— 关键字

    一、创建: Keyword: 关键字是一个内部字符串; 两个同样的关键字指向同一个对象; 通常被用来作为map的key。 

    阳光岛主
  • c语言智能指针 附完整示例代码

    资源获取即初始化 (Resource Acquisition Is Initialization, RAII),RAII是一种资源管理机制,资源的有效期与持有资...

    cpuimage

扫码关注云+社区

领取腾讯云代金券