Skip to main content

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;};
info

此配置方法为 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.tsimport { 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 中间件加载之前被执行

info

代码编写完后,请先执行一次 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();    };  }}