Home

egg-shell-decorators.png

v1.0.7 - 图2 v1.0.7 - 图3

Egg.js 路由装饰器,让你的开发更敏捷~

路由解析egg-shell-decorators(蛋壳) 最大的特点,使用 Decorator 装饰的路由,则会被自动解析成对应的路由:

  • 文件路径:app/controller/home.ts
    • @Get('/detail/:id')
    • @Post('/')
  • 解析路由:
    • [get] 全局前缀 + /home + /detail/:id
    • [post] 全局前缀 + /home + /

这里的 全局前缀 指的是你在 EggShell 里配置的 prefix,路由解析支持多层级解析噢~

示范代码均采用 TypeScript。(右边的菜单可以滚动的噢)

QuickStart

咱们先快速来一个例子:

  1. import { Controller } from 'egg';
  2. import { Get, Post, IgnoreJwtAll, TagsAll } from 'egg-shell-decorators';
  3. @TagsAll('用户')
  4. @IgnoreJwtAll
  5. export default class UserController extends Controller {
  6. @Get('/:id')
  7. public getUser({ params: { id } }) {
  8. return `getUser:${id}`;
  9. }
  10. @Post('/')
  11. public createUser({ body: { name, phone, age } }) {
  12. return { name, phone, age };
  13. }
  14. }

以下是 swagger-ui 的路由映射(具体请看Example > Swagger UI):

{
  "/:id": {
    "get": {
      "description": "获取用户",
      "parameters": [
        { "name": "id", "required": true, "in": "path", "type": "string", "description": "用户id" }
      ],
      "responses": {
        "用户id": {  "type": "object", "schema": { "$ref": "User" } }
      }
    }
  },
  "/": {
    "post": {
      "description": "创建用户",
      "parameters": [
        { "name": "body", "in": "body", "required": true, "schema": { "$ref": "User" } }
      ],
      "responses": {
        "200": {  "type": "object", "schema": { "$ref": "User" } }
      }
    }
  }
}

见证奇迹的一刻,Duang~
image.png

接着点击右边的【展开】按钮展开文档:
image.png

接着咱们输入参数,测试一下【获取用户】的接口:
image.png

可带劲来!接下来进入正文~

Installation

$ npm install egg-shell-decorators -S

如果不是采用 TypeScript 脚手架,则需执行以下脚本安装相关的 Babel 插件:

$ npm install babel-register babel-plugin-transform-decorators-legacy -D

Usage

// app/router.ts
import { Application } from 'egg';
import { EggShell } from 'egg-shell-decorators';

export default (app: Application) => {
  // option 详细配置参考下一节
  EggShell(app, { prefix: '/', quickStart: true });
};

如果不是采用 TypeScript 脚手架,则需在入口注册 Bable 插件使其支持 Decorator:

// app.js
'use strict';
require('babel-register')({
  plugins: [
    'transform-decorators-legacy',
  ],
});

Option

{
  "prefix": "string", // 全局前缀
  "quickStart": "boolean", // 开启QuickStart模式
  "before": "Function[]", // 全局前置中间件
  "after": "Function[]", // 全局后置中间件
  "swaggerOpt": "object" // 具体配置查看 (Example > Swagger Ui > swaggerOpt)
}

Member

Method代表装饰的是 Controller 里的函数,而Controller则是装饰控制器的。

Http相关

  • Method
    • Get (string)
      • 路由路径
    • Post (string)
      • 路由路径
    • Put (string)
      • 路由路径
    • Delete (string)
      • 路由路径
    • Patch (string)
      • 路由路径
    • Options (string)
      • 路由路径
    • Head (string)
      • 路由路径
    • 🔲 Header

中间件相关

  • Method
    • Before (Function[])
      • 中间件
    • After (Function[])
      • 中间件
  • Controller
    • BeforeAll (Function[])
      • 中间件
    • AfterAll (Function[])
      • 中间件

Swagger Ui相关

  • Method
    • Summary (summary: string)
      • 对应 swagger-ui 路由的 summary
    • Description (description: string)
      • 对应 swagger-ui 路由的 description
    • Parameters (parameters: object[])
      • 对应 swagger-ui 路由的 parameters
    • Responses (responses: object)
      • 对应 swagger-ui 路由的 responses
    • Produces (produces: string[] || string)
      • 对应 swagger-ui 路由的 produces,当传入string类型时会自动处理为 [ string ]
    • Consumes (consumes: string[] || string)
      • 对应 swagger-ui 路由的 consumes,当传入string类型时会自动处理为 [ string ]
    • Tags (tags: string[] || string)
      • 对应 swagger-ui 路由的 tags,当传入string类型时会自动处理为 [ string ]
    • Hidden
      • 隐藏该接口的文档
    • TokenType (type: string)
      • 配置该接口的 token 类型(具体查看Example > Swagger Ui > Toekn
  • Controller
    • TagsAll (descriptions: string, name: string)
      • 对应 swagger-ui 路由的 tags,第一个参数为 tag 描述,第二个参数为 tag 的名称,第二个参数如果不填会自动把当前文件的 prefix 作为此参数(建议不填写)。
    • HiddenAll
      • 隐藏该控制器所有路由的接口文档
    • TokenTypeAll (type: string)
      • 配置该控制器所有路由的 token 类型(具体查看Example > Swagger Ui > Token

Jwt相关

  • Method
    • IgnoreJwt
      • 忽略该路由的 jwt 校验
  • Controller
    • IgnoreJwtAll
      • 忽略该控制器所有路由的 jwt 校验

其他

  • EggShell(app: Application, option: object) (非装饰器)
    • 核心对象
  • StatusError(message: string, status?: number) (非装饰器)
    • (非装饰器)message 为错误信息,status 为状态码(默认值为500)
  • Method
    • Message(message: string)
      • QuickStart 模式返回的信息
  • Controller
    • Prefix
      • 路由的前缀,用于取缔路由解析给的前缀(prefix)

Example

egg-shell-example 蛋壳示例代码,该仓库提供了一套简单的蛋壳示例代码,有疑惑的小伙伴可以下载取之。

Prefix

如果你不喜欢路由解析给你的路径,那么你可以自定义解析的路径:

// app/controller/user.ts
import { Controller } from 'egg';
import { Get, Message, Prefix } from 'egg-shell-decorators';

@Prefix('/super2god')
export default class UserController extends Controller {

  @Get('/detail/:id')
  @Message('so great !')
  public async get({ params: { id } }) {
    return await this.service.user.getById(id)
  }

}

这样解析出来的路由就是:全局前缀 + /super2god/detail/:id,而不是全局前缀 + /user/detail/:id

QuickStart

在 EggShell 里配置 quickStart 为 true 即可开启 QuickStart 模式,QuickStart 模式会自动处理响应体:

import { Controller } from 'egg';
import { Get, Message, Error, StatusError } from 'egg-shell-decorators';

export default class UserController extends Controller {

  /**
   status: 200
   {
     success: true,
     message: '棒棒哒',
     data: {
       id: '123',
       name: 'super2god'
     },
   }
   */
  @Get('/:id')
  @Message('棒棒哒')
  public async get({ params: { id } }) {
    return await this.service.user.getById(id)
  }

  /**
   status: 200
   {
     success: false,
     message: '故意的'
   }
   */
  @Post('/:id')
  public post() {
    throw Error('故意的')
  }

  /**
   status: 403
   {
     success: false,
     message: '权限不足'
   }
   */
  @Post('/:id')
  public post() {
    // StatusError 的第二个参数默认值为500
    throw StatusError('权限不足', 403)
  }

}

RESTful

让我们用蛋壳快速写一套 RESTful 风格的接口(QuickStart 模式):

注:body 为 request.body,方便结构。

import { Controller } from 'egg';
import { Get, Post, Put, Delete } from 'egg-shell-decorators';

export default class SubOrderController extends Controller {

  @Get('/:id')
  public get({ params: { id }, query: { keyword } }) {
    return `resuful get : ${id}, ${keyword}`;
  }

  @Post('/:id')
  public post({ params: { id }, body: { keyword } }) {
    return `resuful post : ${id}, ${keyword}`;
  }

  @Put('/:id')
  public put({ params: { id }, body: { keyword } }) {
    return `resuful put : ${id}, ${keyword}`;
  }

  @Delete('/:id')
  public delete({ params: { id }, body: { keyword } }) {
    return `resuful delete : ${id}, ${keyword}`;
  }

}

由于蛋壳内置把 ctx 对象传进 Controller 的函数里了,所以我们直接结构就可以获取到请求参数了,美滋滋~

当然,除了这四个常用的请求方法,蛋壳还提供了其他比较常用的请求方法,具体请看上面的Member > Http相关

Swagger Ui

本节篇幅较长,请细心阅读。如对 swagger-ui 不了解的同学可以先查看官方文档

Swagger UI可以用来生成可测试的API文档,很多开发语言都通过注解的方式进行配置,然后自动生成文档。蛋壳提供了一系列 Swagger Ui 相关的装饰器,让你能快速生成API文档~

其实最上面的QuickStart的例子就已经使用了 Swagger Ui 相关的部分装饰器了,但考虑到上面的例子不是很全面,所以这里再放出一个比较全面的示例:

import { Controller } from 'egg';
import { Post, IgnoreJwtAll, Description, TagsAll,
         Parameters, Responses } from 'egg-shell-decorators';

const USER_SCHEMA = {
  type: 'object',
  properties: {
    name: { type: 'string', description: '姓名' },
    phone: { type: 'string', description: '手机号码' },
    age: { type: 'integer', format: 'int32', description: '年龄' }
  }
};

@TagsAll('用户')
@IgnoreJwtAll
export default class SubOrderController extends Controller {

  @Post('/')
  @Description('创建用户')
  @Parameters([
    { name: 'body', in: 'body', required: true, schema: USER_SCHEMA }
  ])
  @Responses({
    200: { type: 'object', schema: USER_SCHEMA }
  })
  public listUser({ body: { name, phone, age } }) {
    return { name, phone, age };
  }

}

上面代码会生成以下的文档:
image.png

我的测试配置:

import { Application } from 'egg';
import { EggShell } from 'egg-shell-decorators';

export default (app: Application) => {
  EggShell(app, {
    prefix: '/api/v1',
    quickStart: true,
    swaggerOpt: {
      open: true,
      title: '示例',
      version: '1.0.0',
      host: '127.0.0.1',
      port: 7001,
      schemes: [ 'http' ],
      paths: {
        outPath: '../api-docs/public/json/main.json',
        definitionPath: './definitions',
        swaggerPath: './swagger',
      },
      tokenOpt: {
        default: 'manager',
        tokens: {
          manager: '123',
          user: '321',
        },
      }
    },
  });
};

根据这个例子,我们来讲解一下相关的配置:

swaggerOpt

{
  "open": "boolean", // 默认值为 false
  "title": "string", // 对应 swagger-ui 的 title
  "version": "string", // 对应 swagger-ui 的 version
  "host": "string", // 对应 swagger-ui 的 host
  "port": "number", // 如有填的话 host += ':' port
  "schemes": "string[]", // 默认值为 [ 'http' ]
  "paths": {
    "outPath": "string", // 输出 swagger-ui 文档json的文件路径
    "definitionPath": "string", // definitions 模型的路径(下面会有讲解)
    "swaggerPath": "string" // swagger-ui 路径映射(下面会有讲解)
  },
  "tokenOpt": { // token配置(下面有讲解)
    "token": "string", // jwt 的 token
    "tokens": // 多个 token
    "{
      tokenType1: token1,
      tokenType2: token2
    }",
    "defaultTokenType": "string" // 设置了 tokens 后用于设置默认的 token
  }
}

paths

outPath

也许有的小伙伴会好奇文档是怎么运行的,其实就是把一个静态web部署运行,这里我用的是Express,我已经开了一个 node-swagger-ui 的仓库,只需要把代码下载下来即可。

把下载下来的文件解压,然后文件夹更名为api-docs
image.png

上面是我的目录结构,这时我们再看看我上面的 swaggerOpt > paths 配置:

{
  "outPath": "../api-docs/public/json/main.json",
  "definitionPath": "./definitions",
  "swaggerPath": "./swagger"
},

当我们的 Egg 应用启动时,这时蛋壳会根据 outPath 把处理好的xx.json文件输出到指定的位置。(注:默认文件名为 main.json,如有修改记得在api-docs/public/index.html里修改url参数)
可以看到我的静态 web 项目是和我的项目(test-ts)放在同级的,所以基于我的项目路径,此时 outPath 就是 ../api-docs/public/json/main.json了。

definitionPath

上面的例子我用到 USER_SCHEMA 来共用 parameters 和 responses 的 schema,但有可能我们很多地方都想用到这个schema,那怎么办呢?No problem,因为 swagger-ui 已经考虑到这种情况了,它内部提供了 definitions 供我们放通用的模型。而蛋壳为了让项目更好的维护,会把 definitionPath 的所有对象都会被解析进 definitions 里

警告:definitionPath 路径下的文件只能是.json文件。

让我们把上面的例子修改一下:

// definitions/user.json
{
  "User": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "姓名"
      },
      "phone": {
        "type": "string",
        "description": "手机号码"
      },
      "age": {
        "type": "integer",
        "format": "int32",
        "description": "年龄"
      }
    }
  }
}
import { Controller } from 'egg';
import { Post, IgnoreJwtAll, Description, TagsAll,
         Parameters, Responses } from 'egg-shell-decorators';

@TagsAll('用户')
@IgnoreJwtAll
export default class SubOrderController extends Controller {

  @Post('/')
  @Description('创建用户')
  @Parameters([
    { name: 'body', in: 'body', required: true, schema: { $ref: 'User' } }
  ])
  @Responses({ type: 'object', schema: { $ref: 'User' } })
  public listUser({ body: { name, phone, age } }) {
    return { name, phone, age };
  }

}

swaggerPath

也许有的小伙伴不想让控制器被太多装饰器装饰,没问题,路由映射帮你搞定~

只需要配置好 swaggerPath,接着保持和 controller 文件夹一样目录结构,但文件只能是.json

|—— controller
  |—— user.ts
|—— swagger
  |—— user.json
import { Controller } from 'egg';
import { Post, IgnoreJwtAll, TagsAll } from 'egg-shell-decorators';

@TagsAll('用户')
@IgnoreJwtAll
export default class UserController extends Controller {

  @Post('/')
  public listUser({ body: { name, phone, age } }) {
    return { name, phone, age };
  }

}
// swagger/user.json
{
  "/": {
    "post": {
      "description": "创建用户",
      "parameters": [
        { "name": "body", "in": "body", "required": true, "schema": { "$ref": "User" } }
      ],
      "responses": { "type": "object", "schema": { "$ref": "User" } }
    }
  }
}

映射文件也很容易写,就是路径对象下面包含着请求方法,so easy~

definitions + 路由映射,让控制器保持干净,降低耦合性。

token

有些同学可能会用到 jwt,那当我们测试接口的时候就需要加上 Authorization 请求头,为了减少没必要的重复代码,同学们只需要在swaggerOpt > tokenOpt里配置了相应的信息,蛋壳就会帮你自动加上加上请求头:

因为我们公司有一套系统的后台服务是服务两个端的,也就是说会存在不同的 token,如果是这样,那你只需要配置上 tokens,把 token去掉,然后再配置defaultTokenType(如果不填写默认为 tokens[0]),就可以让API文档兼容多 token。

举个栗子,比如说我公司的系统主要是服务A系统,那么我设置 defaultTokenType 为 A,而因为有部分接口是服务B系统的,而同时 token 的信息不能共用,那么我就通过 @TokenType 或者 @TokenTypeAll 设置其 tokenType为B即可。

注:tokenType 的设置也可以在路由映射里进行噢~

Jwt

Jwt是目前比较流行的身份认证机制,所以蛋壳提供了相关的 Decorator。如果你使用了 egg-jwt,那默认所以路由都需要进行身份校验,而有时我们想让部分路由不用校验,那么你只需那么做:

import { Controller } from 'egg';
import { Get, IgnoreJwt } from 'egg-shell-decorators';

export default class HomeController extends Controller {

  @IgnoreJwt
  @Get('/')
  public async index() {
    return 'hi, egg';
  }

}

是不是很简单呢,如果你想对整个 Controller 都进行校验忽略,那也很简单:

import { Controller } from 'egg';
import { Get, Post, IgnoreJwtAll } from 'egg-shell-decorators';

@IgnoreJwtAll
export default class HomeController extends Controller {

  @Get('/')
  public async get() {
    return 'get';
  }

  @Post('/')
  public async post() {
    return 'post';
  }

}

MiddleWare

蛋壳提供了四个中间件相关的 Decorator,配合上配置里的全局中间件,让你使用中间件更简单:

// app/router.ts
import { Application } from 'egg';
import { EggShell } from 'egg-shell-decorators';

const Before1 = require('egg-shell-decorators/test/middlewares/before-1');
const Before2 = require('egg-shell-decorators/test/middlewares/before-2');
const After1 = require('egg-shell-decorators/test/middlewares/after-1');
const After2 = require('egg-shell-decorators/test/middlewares/after-2');

export default (app: Application) => {
  EggShell(app, {
    prefix: '/',
    quickStart: true,
    before: [ Before1, Before2 ],
    after: [ After1, After2 ],
  });
};
import { Controller } from 'egg';
import { Get, IgnoreJwtAll, Before, After, BeforeAll, AfterAll } from 'egg-shell-decorators';

const Before3 = require('egg-shell-decorators/test/middlewares/before-3');
const Before4 = require('egg-shell-decorators/test/middlewares/before-4');
const Before5 = require('egg-shell-decorators/test/middlewares/before-5');
const Before6 = require('egg-shell-decorators/test/middlewares/before-6');

const After3 = require('egg-shell-decorators/test/middlewares/after-3');
const After4 = require('egg-shell-decorators/test/middlewares/after-4');
const After5 = require('egg-shell-decorators/test/middlewares/after-5');
const After6 = require('egg-shell-decorators/test/middlewares/after-6');

@BeforeAll([ Before3, Before4 ])
@AfterAll([ After3, After4 ])
@IgnoreJwtAll
export default class HomeController extends Controller {

  /**
   before middleware => 1
   before middleware => 2
   before middleware => 3
   before middleware => 4
   before middleware => 5
   before middleware => 6
   主业务...
   after middleware => 1
   after middleware => 2
   after middleware => 3
   after middleware => 4
   after middleware => 5
   after middleware => 6
   */
  @Before([ Before5, Before6 ])
  @After([ After5, After6 ])
  @Get('/')
  public async index() {
    return 'hi, egg';
  }

}

技术交♂流

有啥技术问题可以加我微信~

image.png