跳到主要内容
版本:2.0.0

Web 中间件

Web 中间件是在控制器调用  之前  和 之后(部分) 调用的函数。 中间件函数可以访问请求和响应对象。

不同的上层 Web 框架中间件形式不同,EggJS 的中间件形式和 Koa 的中间件形式相同,都是基于洋葱圈模型。而 Express 则是传统的队列模型。

所以在 Express 中,中间件只能在控制器之前调用,而 Koa 和 EggJs 可以在控制器前后都被执行

由于 Web 中间件使用较为类同,下面的代码,我们将以 @midwayjs/web(Egg.js)框架举例。

编写 Web 中间件

一般情况下,我们会在 src/middleware  文件夹中编写 Web 中间件。

创建一个 src/middleware/report.ts 。我们在这个 Web 中间件中打印了控制器(Controller)执行的时间。

➜  my_midway_app tree
.
├── src
│ ├── controller
│ │ ├── user.ts
│ │ └── home.ts
│ ├── interface.ts
│ ├── middleware ## 中间件目录
│ │ └── report.ts
│ └── service
│ └── user.ts
├── test
├── package.json
└── tsconfig.json

代码如下。

import { Provide } from '@midwayjs/decorator';
import { IWebMiddleware, IMidwayWebNext } from '@midwayjs/web';
import { Context } from 'egg';

@Provide()
export class ReportMiddleware implements IWebMiddleware {
resolve() {
return async (ctx: Context, next: IMidwayWebNext) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
await next();
// 控制器之后执行的逻辑
console.log(Date.now() - startTime);
};
}
}

简单来说, await next() 则代表了下一个要执行的逻辑,这里一般代表控制器执行,在执行的前后,我们可以进行一些打印和赋值操作,这也是洋葱圈模型最大的优势。

注意,这里我们导出了一个 ReportMiddleware 类,这个中间件类的 key 为 reportMiddleware

使用 Web 中间件

Web 中间件在写完之后,需要应用到请求流程之中。

根据应用到的位置,分为两种:

  • 1、全局中间件,所有的路由都会执行的中间件,比如 cookie、session 等等
  • 2、路由中间件,单个/部分路由会执行的中间件,比如某个路由的前置校验,数据处理等等

他们之间的关系一般为:

路由中间件

在写完中间件之后,我们需要把它应用到各个控制器路由之上。 @Controller 装饰器的第二个参数,可以让我们方便的在某个路由分组之上添加中间件。

import { Controller, Provide } from '@midwayjs/decorator';
import { Context } from 'egg';

@Provide()
@Controller('/', { middleware: ['reportMiddleware'] })
export class HomeController {}

Midway 同时也在 @Get@Post 等路由装饰器上都提供了 middleware 参数,方便对单个路由做中间件拦截。

import { Controller, Get, Provide } from '@midwayjs/decorator';

@Provide()
@Controller('/')
export class HomeController {
@Get('/', { middleware: ['reportMiddleware'] })
async home() {}
}

这里 middleware 属性的参数则是依赖注入容器的 key,也就是 @Provide 的值,前面讲过,默认为类名的驼峰形式。

全局中间件

所谓的全局中间件,就是对所有的路由生效的 Web 中间件。传统的 Express/Koa 中间件都可以是全局中间件。

设置全局中间件需要拿到应用的实例,同时,需要在所有请求之前被加载。

在 EggJS 中,其提供了一个配置性的加载全局中间件的用法。在 src/config/config.default.ts 中配置 middleware 属性即可定义全局中间件,同样的,指定全局中间件的 key 即可。

// src/config/config.default.ts

export default (appInfo: EggAppInfo) => {
const config = {} as DefaultConfig;

// ...

config.middleware = ['reportMiddleware'];

return config;
};
信息

此配置方法为 EggJS 的特殊用法。

常见示例

接入三方中间件

社区有很多三方中间件,Midway 的 Class 写法可以比较方便的接入。本质上, resolve() 方法只需要返回一个符合当前中间件格式的方法即可。

我们以 koa-static 举例。

koa-static 文档中,是这样写的。

const Koa = require('koa');
const app = new Koa();
app.use(require('koa-static')(root, opts));

那么, require('koa-static')(root, opts) 这个,其实就是返回的中间件方法,我们只需要将这段代码加入到 resolve 方法中,

类写法示例如下。

import * as koaStatic from 'koa-static';
import { join } from 'path';

@Provide()
export class ReportMiddleware implements IWebMiddleware {
resolve() {
return koaStatic(join(__dirname, '../public'));
}
}

中间件中获取请求作用域实例

由于 Web 中间件在生命周期的特殊性,会在应用请求前就被加载(绑定)到路由上,所以无法和请求关联。中间件类的作用域 固定为单例(Singleton)。

由于 中间件实例为单例,所以中间件中注入的实例和请求不绑定,无法获取到 ctx,无法使用 @Inject() 注入请求作用域的实例,只能获取 Singleton 的实例。

比如,下面的代码是错误的。

import { Provide } from '@midwayjs/decorator';
import { IWebMiddleware, IMidwayWebNext } from '@midwayjs/web';
import { Context } from 'egg';

@Provide()
export class ReportMiddleware implements IWebMiddleware {
@Inject()
userService; // 这里注入的实例和上下文不绑定,无法获取到 ctx

resolve() {
return async (ctx: Context, next: IMidwayWebNext) => {
// TODO
await next();
};
}
}

如果要获取请求作用域的实例,可以使用从请求作用域容器 ctx.requestContext  中获取,如下面的方法。

import { Provide } from '@midwayjs/decorator';
import { IWebMiddleware, IMidwayWebNext } from '@midwayjs/web';
import { Context } from 'egg';

@Provide()
export class ReportMiddleware implements IWebMiddleware {
resolve() {
return async (ctx: Context, next: IMidwayWebNext) => {
const userService = await ctx.requestContext.getAsync<UserService>('userService');
// TODO userService.xxxx
await next();
};
}
}

传统函数式中间件

如果要继续使用 EggJS 传统的函数式写法,请参考 EggJS 章节

特殊的全局中间件用法

Midway 提供了一个生命周期的口子,方便业务在非常早的时候可以做一些自定义处理。我们需要手动创建一个生命周期文件,位于 src/configuration.ts

➜  my_midway_app tree
.
├── src
│ ├── configuration.ts ## 全局生命周期配置文件
│ ├── controller
│ │ ├── user.ts
│ │ └── home.ts
│ ├── interface.ts
│ ├── middleware
│ │ └── report.ts
│ └── service
│ └── user.ts
├── test
├── package.json
└── tsconfig.json

内容如下:

// src/configuration.ts
import { Configuration, App } from '@midwayjs/decorator';
import { ILifeCycle } from '@midwayjs/core';
import { Application } from 'egg';

@Configuration()
export class ContainerLifeCycle implements ILifeCycle {
@App()
app: Application;

async onReady() {
this.app.use(await this.app.generateMiddleware('reportMiddleware'));
}
}

Midway 在各个 Web 框架的 app 上提供了一个 generateMiddleware 方法,用于快速创建 Class 形式的中间件,然后使用框架原有的 use 方法即可加载为全局中间件。

在 onReady 中加载的中间件,框架会保证在 egg 中间件加载之前被执行

信息

代码编写完后,请先执行一次 npm run dev ,egg 需要在第一次运行后才会生成定义。

全局路由前缀

全局的路由前缀可以由反向代理工具来做,比如 nginx,也可以由中间件代码来完成,下面的中间件简单演示了如何为所有路由增加 api 前缀。

import { Provide } from '@midwayjs/decorator';

@Provide()
export class PrefixMiddleware {
resolve() {
return async (ctx, next) => {
ctx.path = ctx.path.replace(/^\/api/, '') || '/';
await next();
};
}
}