# Egg —— 从零开始搭建一个商用Egg.js框架

1,697 阅读1分钟

通过本文,你可以学习到:

  • egg中生成swagger-ui文档

创建项目

Egg.js官方文档传送门

  • 安装依赖:
    $ mkdir egg-example && cd egg-example
    $ npm init egg --type=simple //使用官方的脚手架创建项目(simple:标准模板)
    $ npm i
    
  • 启动项目
    $ npm run dev
    $ open http://localhost:7001
    

集成swagger-ui文档

  • 安装依赖

    npm install egg-swagger-doc-feat -S
    
  • config/plugin.js中声明插件

    /** @type Egg.EggPlugin */
    module.exports = {
      swaggerdoc: {
        enable: true,
        package: 'egg-swagger-doc-feat',
      },
    };
    
    
  • config/config.default.js文件中配置swaggerdoc插件

    'use strict';
    module.exports = appInfo => {
      const config = (exports = {});
      config.keys = appInfo.name + '_1622015186058_5727';
    
      // add your middleware config here
      config.middleware = [];
    
      // add your user config here
      const userConfig = {
        // 配置swagger-ui接口文档
        swaggerdoc: {
          dirScanner: './app/controller', //指定swaggerdoc从哪个目录开始检索,
          apiInfo: {
            title: 'xxx项目接口',
            description: 'xxx项目接口 swagger-ui for egg',
            version: '1.0.0',
          }, //接口文档主要信息、描述、版本号
          schemes: ['http', 'https'], //协议
          consumes: ['application/json'], //输出方式
          produces: ['application/json'],
          enableSecurity: false,
          //enableValidate:true,//是否开启参数校验(很遗憾虽然有这个api,但是功能没有实现)
          routerMap: true, //是否自动注册路由(很香)
          enable: true,
        },
      };
    
      return {
        ...config,
        ...userConfig,
      };
    };
    
  • 某一个接口编写jsdoc

    • 示例:在app/controller/home.js文件中
      const Controller = require ('egg').Controller;
      /**
       * @Controller 首页
       */
      class HomeController extends Controller {
        /**
         * @summary 首页
         * @description 获取首页数据
         * @router get /api/home/index
         * @request body indexRequest *body
         * @response 200 baseResponse 请求成功
         */
        async index () {
          const {ctx} = this;
          ctx.body = 'hi, egg';
        }
      }
      
      module.exports = HomeController;
      
  • 配置接口返回值的约束

    注意:除了contract的文件位置和名字固定之外,像新建的index.js、home.js都不需要对应,只需要module.exports就行,我这样写是为了文件对应模块。

    • 新建app/contract/index.js文件(contract文件名不要写错,这个是swagger默认读取model的文件夹)
      module.exports = {
       baseRequest: {},
       baseResponse: {//@response 200 baseResponse 操作结果,名字与相应结果对应
         code: {type: 'integer', required: true, example: 0},
         data: {
           type: 'string',
           example: '请求成功',
         },
         errorMessage: {type: 'string', example: '请求成功'},
       },
      };
      
      
    • 新建app/contract/home.js文件
      module.exports = {
        indexRequest: {
          test: {type: 'string', require: true, description: '测试'},
        },
      };
      
  • 访问http://127.0.0.1:7002/swagger-ui.html (我的项目是7002,一般默认是7001)

    image.png

集成异常捕获系统

对于一个成熟的系统来说,异常捕获系统是必要的,

可以从以下俩个方面来考虑:

  1. 有些错误在生产环境是不能抛给用户的,容易造成系统的安全问题
  2. 可以利用洋葱圈模型,做一个全局的异常捕获中间件

编写

  • 新建app/middleware/error_handler.js

    module.exports = (options, app) => {
      return async (ctx, next) => {
        try {
          await next ();
        } catch (err) {
          ctx.app.emit ('error', err, ctx);
    
          const status = err.status || 500;
          // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
          const error = status === 500 && ctx.app.config.env === 'prod'
            ? 'Internal Server Error'
            : err.message;
    
          // 从 error 对象上读出各个属性,设置到响应中
          ctx.body = {error};
          if (status === 422) {//422是参数验证类型的错误
            ctx.body.detail = err.errors;
          }
          ctx.status = status;
        }
      };
    };
    
  • config/config.default.js中挂载全局中间件

    'use strict';
    module.exports = appInfo => {
      const config = (exports = {});
      config.keys = appInfo.name + '_1622015186058_5727';
    
      // 挂载应用级的中间件
      config.middleware = ["errorHandler"];
    
      // 添加用户自定义配置
      const userConfig = {
        // 配置swagger-ui接口文档
        swaggerdoc: {
          dirScanner: './app/controller', //指定swaggerdoc从哪个目录开始检索,
          apiInfo: {
            title: 'xxx项目接口',
            description: 'xxx项目接口 swagger-ui for egg',
            version: '1.0.0',
          }, //接口文档主要信息、描述、版本号
          schemes: ['http', 'https'], //协议
          consumes: ['application/json'], //输出方式
          produces: ['application/json'],
          enableSecurity: false,
          routerMap: true, //是否自动注册路由(很香)
          enable: true,
        },
      };
    
      return {
        ...config,
        ...userConfig,
      };
    };
    

验证

  • 修改app/controller/home.js文件,主动抛出错误,验证我们的errorHandler中间件。

    const Controller = require ('egg').Controller;
    /**
     * @Controller 首页
     */
    class HomeController extends Controller {
      /**
       * @summary 首页
       * @description 获取首页数据
       * @router get /api/home/index
       * @request body indexRequest *niji
       * @response 200 baseResponse 请求成功
       */
      async index () {
        const {ctx} = this;
        aaaaaaaaaaaaaaaaaaaaaaa ();//这个方法不存在
        ctx.body = 'hi, egg';
      }
    }
    
    module.exports = HomeController;
    
  • 访问这个接口,成功!

    image.png

集成结构化返回数据

我们在写一个接口返回数据的时候,数据的结构必须一直,我们可以这样写:

ctx.body = {name:"xxx",age:34}

但是如果每个函数都这样去写的话,就会很烦,我们需要一个统一的方法,来帮我们做这件事情:

  • 新建app/extend/helper.js文件

    exports.success = ({ctx, res, code, message}) => {
      ctx.body = {
        code: code || 100,
        data: res,
        message: message || '请求成功',
      };
    
      ctx.status = 200;
    };
    
    
  • app/controller/home.js文件中,使用helperapi

    const Controller = require ('egg').Controller;
    /**
     * @Controller 首页
     */
    class HomeController extends Controller {
      /**
       * @summary 首页
       * @description 获取首页数据
       * @router get /api/home/index
       * @request body indexRequest *niji
       * @response 200 baseResponse 请求成功
       */
      async index () {
        const {ctx} = this;
        ctx.helper.success ({ctx, res: {value: 1}, message: '请求成功'});
      }
    }
    
    module.exports = HomeController;
    
  • 请求接口,查看返回数据

    image.png

集成参数校验功能

  • 安装依赖
    npm install egg-validate -S
    
  • config/plugin.js文件中配置
    'use strict';
    
    /** @type Egg.EggPlugin */
    module.exports = {
      // had enabled by egg
      // static: {
      //   enable: true,
      // }
      swaggerdoc: {
        enable: true,
        package: 'egg-swagger-doc-feat',
      },
      eggvalidaate: {
        enable: true,
        package: 'egg-validate',
      },
    };
    

第一种:egg-validate正常使用

一般我们正常使用egg-validate,就是用下面的这种方式,因为在你安装了egg-validate,应用启动的时候,就会给ctx这个上下文对象挂载validate方法

  • app/controller/home.js文件中

    const Controller = require ('egg').Controller;
    /**
     * @Controller 首页
     */
    class HomeController extends Controller {
      /**
       * @summary 首页
       * @description 获取首页数据
       * @router get /api/home/index
       * @request body indexRequest *niji
       * @response 200 baseResponse 请求成功
       */
      async index () {
        const {ctx} = this;
        ctx.validate ({
          id: {type: 'number', required: true},
          phoneNumber: {type: 'string', requried: true, format: /^1[34578]\d{9}$/},
        });
        ctx.helper.success ({ctx, res: {value: 1}, message: '请求成功'});
      }
    }
    module.exports = HomeController;
    
  • 请求接口,进行测试

    image.png

第二种:egg-validate结合egg-swagger-doc-feat的配置使用

我们正常使用egg-validate的时候,都需要给ctx.validate({})传递一个rule对象,这样会很烦。

现在我们提出这样一个设想,能不能把app/contract文件夹下面定义的rule规则,给ctx.validate({})传进去,(egg-validate的规则属性和egg-swagger-doc-feat的规则属性配置是一样的)这样我们共用一个规则,也方便我们后期维护。

通过debug发现,egg-swagger-doc-feat这个npm包,虽然没有实现参数校验的功能(文章上面在配置中提到了)但是,他会把app/contract下面定义的所有规则全部挂载到上下文对象ctx.rule这个属性上,是不是很香。

image.png

既然如此我们就可以这样编写我们的代码:

  • app/controller/home.js文件中

    const Controller = require ('egg').Controller;
    /**
     * @Controller 首页
     */
    class HomeController extends Controller {
      /**
       * @summary 首页
       * @description 获取首页数据
       * @router get /api/home/index
       * @request body indexRequest *niji
       * @response 200 baseResponse 请求成功
       */
      async index () {
        const {ctx} = this;
        ctx.validate (ctx.rule.indexRequest);
        ctx.helper.success ({ctx, res: {value: 1}, message: '请求成功'});
      }
    }
    
    module.exports = HomeController;
    
  • 请求接口,进行测试

    image.png 完美!!!