跳到主要内容
版本:4.0.0 🚧

Web 中间件

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

不同的上层 Web 框架中间件形式不同,Midway 标准的中间件基于 洋葱圈模型。而 Express 则是传统的队列模型。

Koa 和 EggJs 可以在 控制器前后都被执行,在 Express 中,中间件 只能在控制器之前 调用,将在 Express 章节单独介绍。

下面的代码,我们将以 @midwayjs/koa 举例。

编写中间件

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

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

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

Midway 使用 @Middleware 装饰器标识中间件,完整的中间件示例代码如下。

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {

resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
console.log(Date.now() - startTime);
// 返回给上一个中间件的结果
return result;
};
}

static getName(): string {
return 'report';
}
}

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

注意,Midway 对传统的洋葱模型做了一些微调,使得其可以获取到下一个中间件的返回值,同时,你也可以将这个中间件的结果,通过 return 方法返回给上一个中间件。

这里的静态 getName 方法,用来指定中间件的名字,方便排查问题。

使用中间件

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

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

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

他们之间的关系一般为:

image.png

路由中间件

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

import { Controller } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';

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

}

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

import { Controller, Get } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';

@Controller('/')
export class HomeController {

@Get('/', { middleware: [ ReportMiddleware ]})
async home() {
}
}

全局中间件

所谓的全局中间件,就是对所有的路由生效的 Web 中间件。

我们需要在应用启动前,加入当前框架的中间件列表中,useMiddleware 方法,可以把中间件加入到中间件列表中。

// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';

@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {

@App()
app: koa.Application;

async onReady() {
this.app.useMiddleware(ReportMiddleware);
}
}

你可以同时添加多个中间件。

async onReady() {
this.app.useMiddleware([ReportMiddleware1, ReportMiddleware2]);
}

忽略和匹配路由

在中间件执行时,我们可以添加路由忽略的逻辑。

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {

resolve() {
return async (ctx: Context, next: NextFunction) => {
// ...
};
}

ignore(ctx: Context): boolean {
// 下面的路由将忽略此中间件
return ctx.path === '/'
|| ctx.path === '/api/auth'
|| ctx.path === '/api/login';
}

static getName(): string {
return 'report';
}
}

同理,也可以添加匹配的路由,只有匹配到的路由才会执行该中间件。ignorematch 同时只有一个会生效。

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {

resolve() {
return async (ctx: Context, next: NextFunction) => {
// ...
};
}

match(ctx: Context): boolean {
// 下面的匹配到的路由会执行此中间件
if (ctx.path === '/api/index') {
return true;
}
}

static getName(): string {
return 'report';
}
}

除此之外,matchignore 还可以是普通字符串或者正则,以及他们的数组形式。

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
// 字符串
match = '/api/index';

// 正则
match = /^\/api/;

// 数组
match = ['/api/index', '/api/user', /^\/openapi/, ctx => {
if (ctx.path === '/api/index') {
return true;
}
}];
}

我们也可以在初始化阶段对属性进行修改,比如:

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {

// 某个中间件的配置
@Config('report')
reportConfig;

@Init()
async init() {
// 动态合并一些规则
if (this.reportConfig.match) {
this.match = ['/api/index', '/api/user'].concat(this.reportConfig.match);
} else if (this.reportConfig.ignore) {
this.match = [].concat(this.reportConfig.ignore);
}
}
}

复用中间件

中间件的本质是函数,函数可以传递不同的配置来复用中间件,但是在 class 场景下较难实现。Midway 提供了 createMiddleware 方法辅助 class 场景下创建不同的中间件函数。

可以在 useMiddleware 阶段使用 createMiddleare 复用。

// src/configuration.ts
import { App, Configuration, createMiddleare } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';

@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {

@App()
app: koa.Application;

async onReady() {
// 添加 ReportMiddleware 中间件
this.app.useMiddleware(ReportMiddleware);
// 添加一个不同参数的 ReportMiddleware
this.app.useMiddleware(createMiddleare(ReportMiddleware, {
text: 'abc'
}, 'anotherReportMiddleare'));
}
}

我们可以在中间件中获取到这个参数,从而执行不同的逻辑,。

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
initData = 'text1';

resolve(_, options?: {
text: string;
}) {
return async (ctx: Context, next: NextFunction) => {
this.ctx.setAttr('data', options?.text || this.initData);
return await next();
};
}
}

createMiddleare 方法定义如下,包含三个参数。

function createMiddleware(middlewareClass: new (...args) => IMiddleware, options, name?: string);
参数描述
middlewareClass中间件类
options传递的自定义参数
name可选,中间件名称

options 可以传递中间件的自定义函数,在逻辑中可以自行进行处理。

name 字段用于中间件的排序和展示,一般会选择一个和原中间件名不同的字符串。

createMiddleare 方法还可以在路由中间件使用。

import { Controller, Get, createMiddleware } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';

const anotherMiddleware = createMiddleware(ReportMiddleware, {
// ...
});

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

注意,装饰器会在框架启动前加载,这个时候 createMiddleware 的参数无法从框架配置中获取,一般为固定的对象值。

函数中间件

Midway 依旧支持函数中间件的形式,并且可以使用 useMiddleware 来加入到中间件列表。

// src/middleware/another.middleware.ts
export async function fnMiddleware(ctx, next) {
// ...
await next();
// ...
}


// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
import { fnMiddleware } from './middleware/another.middleware';

@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {

@App()
app: koa.Application;

async onReady() {
// add middleware
this.app.useMiddleware([ReportMiddleware, fnMiddleware]);
}
}


这样的话,社区很多 koa 三方中间件都可以比较方便的接入。

使用社区中间件

我们以 koa-static 举例。

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

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

那么, require('koa-static')(root, opts) 这个,其实就是返回的中间件方法,我们直接导出,并且调用 useMiddleware 即可。

async onReady() {
// add middleware
this.app.useMiddleware(require('koa-static')(root, opts));
}

如果中间件支持在路由上引入,比如:

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

我们也可以将中间件看成普通函数,放在装饰器参数中。

const staticMiddleware = require('koa-static')(root, opts);

// ...
class HomeController {
@Get('/controller', {middleware: [staticMiddleware]})
async getMethod() {
// ...
}
}

也可以作为作为路由方法体使用。

const staticMiddleware = require('koa-static')(root, opts);

// ...
class HomeController {
@Get('/controller')
async getMethod(ctx, next) {
// ...
return staticMiddleware(ctx, next);
}
}
提示

三方中间件写法有很多种,上面只是列出最基本的使用方式。

获取中间件名

每个中间件应当有一个名字,默认情况下,类中间件的名字将依照下面的规则获取:

  • 1、当 getName() 静态方法存在时,以其返回值作为名字
  • 2、如果不存在 getName() 静态方法,将使用类名作为中间件名

一个好认的中间件名在手动排序或者调试代码时有很大的作用。

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {

// ...

static getName(): string {
return 'report'; // 中间件名
}
}

函数中间件也是类似,定义的方法名就是中间件的名字,比如下面的 fnMiddleware

export async function fnMiddleware(ctx, next) {
// ...
await next();
// ...
}

假如三方中间件导出了一个匿名的中间件函数,那么你可以使用 _name 来添加一个名字。

const fn = async (ctx, next) => {
// ...
await next();
// ...
};

fn._name = 'fnMiddleware';

我们可以使用 getMiddleware().getNames() 来获取当前中间件列表中的所有中间件名。

// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';
import { fnMiddleware } from './middleware/another.middleware';

@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {

@App()
app: koa.Application;

async onReady() {
// add middleware
this.app.useMiddleware([ReportMiddleware, fnMiddleware]);

// output
console.log(this.app.getMiddleware().getNames());
// => report, fnMiddleware
}
}



中间件顺序

有时候,我们需要在组件或者应用中修改中间件的顺序。

Midway 提供了 insert 系列的 API,方便用户快速调整中间件。

我们需要先使用 getMiddleware() 方法获取中间件列表,然后对其进行操作。

// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { ReportMiddleware } from './middleware/user.middleware';

@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {

@App()
app: koa.Application;

async onReady() {
// 把中间件添加到最前面
this.app.getMiddleware().insertFirst(ReportMiddleware);
// 把中间件添加到最后面,等价于 useMiddleware
this.app.getMiddleware().insertLast(ReportMiddleware);

// 把中间件添加到名为 session 的中间件之后
this.app.getMiddleware().insertAfter(ReportMiddleware, 'session');
// 把中间件添加到名为 session 的中间件之前
this.app.getMiddleware().insertBefore(ReportMiddleware, 'session');
}
}

常见示例

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

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

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

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

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {

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

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

}

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

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {

resolve() {
return async (ctx: Context, next: NextFunction) => {
const userService = await ctx.requestContext.getAsync<UserService>(UserService);
// TODO userService.xxxx
await next();
};
}

}

统一返回数据结构

比如在 /api 返回的所有数据都是用统一的结构,减少 Controller 中的重复代码。

我们可以增加一个类似下面的中间件代码。

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class FormatMiddleware implements IMiddleware<Context, NextFunction> {

resolve() {
return async (ctx: Context, next: NextFunction) => {
const result = await next();
return {
code: 0,
msg: 'OK',
data: result,
}
};
}

match(ctx) {
return ctx.path.indexOf('/api') !== -1;
}
}

上面的仅是正确逻辑返回的代码,如需错误的返回包裹,可以使用 过滤器

关于中间件返回 null 的情况

在 koa/egg 下,如果中间件中返回 null 值,会使得状态码变为 204,如果需要返回其他状态码(如 200),需要在中间件中显式额外赋值状态码。

import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';

@Middleware()
export class FormatMiddleware implements IMiddleware<Context, NextFunction> {

resolve() {
return async (ctx: Context, next: NextFunction) => {
const result = await next();
if (result === null) {
ctx.status = 200;
}
return {
code: 0,
msg: 'OK',
data: result,
}
};
}

match(ctx) {
return ctx.path.indexOf('/api') !== -1;
}
}