中间件的作用

灵活应用中间件机制,可以有效扩展架构的功能。中间件主要的作用有:拦截重整注入

  1. 拦截:比如通过中间件判断用户权限,如果没有权限则中止后续执行
  2. 重整:比如通过中间件对前端发来的数据进行验证,并转化为期望的类型
  3. 注入:比如通过中间件向ctx注入对象,以便为后续代码提供必要的基础功能

拦截

在执行API路由之前,对请求参数进行判断,如果不符合预期,就拒绝

比如中间件test,判断当前运行环境,如果不是测试环境就抛出异常:

egg-born-backend/app/middleware/test.js

module.exports = () => {
  return async function test(ctx, next) {
    if (!ctx.app.meta.isTest) ctx.throw(403);
    // next
    await next();
  };
};

重整

在执行API路由之前,对请求参数进行调整,使其符合预期的格式(如果不符合预期也可以拒绝)

比如中间件validate,验证前端发送的表单数据,并转换为预期的数据类型:

a-validation/backend/src/config/middleware/validate.js

module.exports = (options2, app) => {
  const moduleInfo = app.meta.mockUtil.parseInfoFromPackage(__dirname);
  return async function validate(ctx, next, options) {
    // must exists
    const validator = options.validator;
    if (!validator) ctx.throw.module(moduleInfo.relativeName, 1001);
    // params
    const module = options.module || ctx.module.info.relativeName;
    const schema = options.schema || (ctx.meta._validator && ctx.meta._validator.schema);
    const data = ctx.request.body[options.data || 'data'];
    // if error throw 422
    await ctx.meta.validation.validate({
      module,
      validator,
      schema,
      data,
    });
    // next
    await next();
  };
};

注入

在执行API路由之前,向ctx对象注入对象实例,扩展功能

比如中间件cachemem,向ctx注入了基于内存的cache对象:

a-cache/backend/src/config/middleware/cachemem.js

const memFn = require('./adapter/mem.js');
const CACHE = Symbol('CTX#__CACHE');

module.exports = () => {
  return async function cachemem(ctx, next) {
    ctx.cache = ctx.cache || {};
    Object.defineProperty(ctx.cache, 'mem', {
      get() {
        if (ctx.cache[CACHE] === undefined) {
          ctx.cache[CACHE] = new (memFn(ctx))();
        }
        return ctx.cache[CACHE];
      },
    });

    // next
    await next();
  };
};

中间件的类型

EggBornJS主要有三个来源的中间件:

  1. EggJS中间件
  2. EggBornJS系统中间件
  3. EggBornJS模块中间件

EggJS中间件

在EggBornJS中可以直接使用EggJS生态的中间件

请参考Egg中间件

EggBornJS系统中间件

为满足常见场景的应用,EggBornJS内置了三个系统中间件:innertesttransaction

1. inner

后端API路由可通过ctx.performAction调用其他后端API路由,这称之为内部访问

后端API路由指定中间件inner就可以限定此路由只能被内部访问

{ method: 'post', path: 'version/update', controller: version, middlewares: 'inner' }

中间件inner也可看作是API授权策略体系之一

2. test

后端API路由指定中间件test,可以限定此路由只能在测试环境中使用

{ method: 'post', path: 'version/test', controller: version, middlewares: 'test' }

中间件test也可看作是API授权策略体系之一

3. transaction

后端API路由指定中间件transaction,可以指定此API路由开启数据库事务

{ method: 'post', path: 'test/echo', controller: test, middlewares: 'transaction'}

EggBornJS模块中间件

EggBornJS允许在模块中开发中间件,此中间件可以供本模块和其他模块使用

模块中间件的开发

在这里,我们通过一个虚拟的需求,用中间件实现拦截重整注入三种功能:

  1. 前端发送两个参数:a、b,后端计算二者之和,并返回前端
  2. 拦截:中间件判断参数是否为空,如果为空则中止后续执行
  3. 重整:将参数类型强制转换为Integer类型
  4. 注入:注入一个方法,以便在后续代码中调用

声明中间件

src/module/test-party/backend/src/config/config.js

// middlewares
config.middlewares = {
  testInterception: {
    global: false,
    dependencies: 'instance',
  },
  testRestructuring: {
    global: false,
    dependencies: 'instance',
  },
  testInjection: {
    global: false,
    dependencies: 'instance',
  },
};
名称 说明
global 是否为全局中间件,全局中间价会自动加载,局部中间件需要手动指定
dependencies 标明此中间件依赖哪些中间件,从而在那些中间件后面加载。一般要依赖于instance,因为instance提供了多实例的基础逻辑

定义中间件

src/module/test-party/backend/src/config/middleware/interception.js

module.exports = () => {
  return async function interception(ctx, next) {
    const { a, b } = ctx.request.body;
    if (a === undefined || b === undefined) return ctx.throw(1002); // 1002: 'Incomplete Parameters'
    // next
    await next();
  };
};

src/module/test-party/backend/src/config/middleware/restructuring.js

module.exports = () => {
  return async function restructuring(ctx, next) {
    const { a, b } = ctx.request.body;
    ctx.request.body.a = parseInt(a);
    ctx.request.body.b = parseInt(b);
    // next
    await next();
  };
};

src/module/test-party/backend/src/config/middleware/injection.js

module.exports = () => {
  return async function injection(ctx, next) {
    ctx.meta.__plus = (a, b) => {
      return a + b;
    };
    // next
    await next();
  };
};

引用中间件

src/module/test-party/backend/src/config/middlewares.js

const interception = require('./middleware/interception.js');
const restructuring = require('./middleware/restructuring.js');
const injection = require('./middleware/injection.js');

module.exports = {
  testInterception: interception,
  testRestructuring: restructuring,
  testInjection: injection,
};

使用中间件

因为testInterceptiontestRestructuringtestInjection是局部中间件,因此需要手动在API路由上指定

src/module/test-party/backend/src/routes.js

// test/feat/middleware
{ method: 'post', path: 'test/feat/middleware/interception', controller: testFeatMiddleware, middlewares: 'test,testInterception' },
{ method: 'post', path: 'test/feat/middleware/restructuring', controller: testFeatMiddleware, middlewares: 'test,testInterception,testRestructuring' },
{ method: 'post', path: 'test/feat/middleware/injection', controller: testFeatMiddleware, middlewares: 'test,testInterception,testRestructuring,testInjection' },

后端控制器方法

src/module/test-party/backend/src/controller/test/feat/middleware.js

module.exports = app => {

  class TestController extends app.Controller {

    async interception() {
      const { a, b } = this.ctx.request.body;
      const c = parseInt(a) + parseInt(b);
      this.ctx.success(c);
    }

    async restructuring() {
      const { a, b } = this.ctx.request.body;
      const c = a + b;
      this.ctx.success(c);
    }

    async injection() {
      const { a, b } = this.ctx.request.body;
      const c = this.ctx.meta.__plus(a, b);
      this.ctx.success(c);
    }

  }

  return TestController;
};

中间件的使用

全局中间件

全局中间件自动加载,不需在后端路由中指定

局部中间件

只需设置后端API路由参数middlewares,如:

{ method: 'post', path: 'version/update', controller: version, middlewares: 'inner' }

中间件参数

可通过后端API路由参数meta指定中间件的参数,如:

src/module/test-party/backend/src/routes.js

const testKitchensinkGuide = require('./controller/kitchen-sink/guide.js');

module.exports = app => {
  const routes = [
    { 
      method: 'post', 
      path: 'kitchen-sink/guide/echo9', 
      controller: testKitchensinkGuide,
      action: 'echo9',
      middlewares: 'test,transaction'
      meta: {
        right: {
          type: 'function',
          name: 'kitchenSink',
        },
      },
    },
  ];
  return routes;
};
名称 说明
meta.right 全局中间件right的参数,通过配置type和name,从而验证当前用户是否具有访问此API的权限

禁用中间件

可通过两种方式禁用中间件:

1. config参数配置

通过属性ignore指定哪些API路由禁用此中间件

a-instance/backend/src/config/config.js

// middlewares
config.middlewares = {
  instance: {
    global: true,
    dependencies: 'cachemem',
    ignore: /(\/version\/(update))/,
  },
};

2. 后端API路由

直接在后端API路由中通过属性enable禁用中间件

a-base/backend/src/routes.js

{ method: 'post', path: 'auth/echo', controller: auth,
  meta: {
    auth: { 
      enable: false
    }
  } 
}
名称 说明
auth.enable 禁用全局中间件auth

当某中间件被禁用后,依赖于此中间件的其他中间件也自动被禁用