Web 中间件
Web 中间件是在控制器调用 之前 和 **之后(部分)**调用的函数。 中间件函数可以访问请求和响应对象。
不同的上层 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、路由中间件,单个/部分路由会执行的中间件,比如某个路由的前置校验,数据处理等等
他们之间的关系一般为:
路由中间件
在写完中间件之后,我们需要把它应用到各个控制器路由之上。 @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';
}
}
同理,也可以添加匹配的路由,只有匹配到的路由才会执行该中间件。ignore
和 match
同时只有一个会生效。
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';
}
}
除此之外,match
和 ignore
还可以是普通字符串或者正则,以及他们的数组形式。
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'));
}
}