|
| 1 | +# Middleware 中间件指南 |
| 2 | + |
| 3 | +## 常见错误 |
| 4 | + |
| 5 | +| 错误写法 | 正确写法 | 说明 | |
| 6 | +| ------------------------------------------ | ---------------------------------- | ----------------------------------------------- | |
| 7 | +| `import { Middleware } from '@eggjs/tegg'` | `import { Middleware } from 'egg'` | Middleware 从 `egg` 导入 | |
| 8 | +| `import { Advice } from 'egg'` | `import { Advice } from 'egg/aop'` | AOP 装饰器从 `egg/aop` 导入 | |
| 9 | +| `@Middleware(funcMw, AdviceClass)` | 分开写两个 `@Middleware` | 同一个 `@Middleware()` 中不能混用函数式和 AOP | |
| 10 | +| AOP Advice 中用实例属性存请求级状态 | 使用 `ctx.set()`/`ctx.get()` | Advice 默认 Singleton,实例属性会被并发请求共享 | |
| 11 | +| 把中间件文件放在 `app/middleware/` | 放在模块目录下 | 函数式中间件放在模块中,通过 import 引用 | |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 两种中间件模式 |
| 16 | + |
| 17 | +Egg 的 `@Middleware` 装饰器支持两种中间件写法,根据传入参数类型自动识别: |
| 18 | + |
| 19 | +- **AOP 写法(推荐)**:使用 `@Advice` 类,支持 `@Inject` 注入 Proto 依赖,拥有丰富的生命周期钩子 |
| 20 | +- **函数式写法(旧版兼容)**:标准 Koa 中间件函数,需要从 `ctx` 对象上手动获取依赖 |
| 21 | + |
| 22 | +新项目应优先使用 AOP 写法。函数式写法主要用于兼容旧的 egg 中间件或非常简单的场景。 |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## 实现中间件 |
| 27 | + |
| 28 | +### AOP 写法(推荐) |
| 29 | + |
| 30 | +使用 `@Advice()` 装饰器定义类,实现 `IAdvice` 的 `around` 方法,写法与 Koa 中间件一致(`next` 调用目标方法)。Advice 本身是 Proto,支持 `@Inject` 注入依赖。`around` 中可以修改入参(`ctx.args`)和返回值: |
| 31 | + |
| 32 | +```typescript |
| 33 | +// app/modules/foo/advice/LogAdvice.ts |
| 34 | +import { AccessLevel, Inject, Logger } from 'egg'; |
| 35 | +import { Advice, IAdvice, AdviceContext } from 'egg/aop'; |
| 36 | + |
| 37 | +// 跨模块使用时需设置 accessLevel: AccessLevel.PUBLIC |
| 38 | +@Advice({ accessLevel: AccessLevel.PUBLIC }) |
| 39 | +export class LogAdvice implements IAdvice { |
| 40 | + @Inject() |
| 41 | + logger: Logger; |
| 42 | + |
| 43 | + async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> { |
| 44 | + // 修改入参:ctx.args 对应控制器方法的参数列表 |
| 45 | + // ctx.args[0] = sanitize(ctx.args[0]); |
| 46 | + |
| 47 | + const start = Date.now(); |
| 48 | + const result = await next(); |
| 49 | + this.logger.info('%s cost %dms', ctx.method, Date.now() - start); |
| 50 | + |
| 51 | + // 修改返回值:直接返回新的值即可 |
| 52 | + return { success: true, data: result }; |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +### 函数式写法(旧版兼容) |
| 58 | + |
| 59 | +标准 Koa 中间件函数,签名为 `(ctx: Context, next: Next) => Promise<void>`。旧版 egg 写法,无法使用 `@Inject`,需要从 `ctx` 对象上手动获取依赖: |
| 60 | + |
| 61 | +```typescript |
| 62 | +// app/modules/foo/middleware/count.ts |
| 63 | +import type { Context, Next } from 'egg'; |
| 64 | + |
| 65 | +export async function countMw(ctx: Context, next: Next): Promise<void> { |
| 66 | + const start = Date.now(); |
| 67 | + await next(); |
| 68 | + ctx.set('X-Response-Time', `${Date.now() - start}ms`); |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## 应用中间件 |
| 75 | + |
| 76 | +通过 `@Middleware()` 装饰器将中间件应用到控制器,支持类级别和方法级别: |
| 77 | + |
| 78 | +```typescript |
| 79 | +// app/modules/foo/FooController.ts |
| 80 | +import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg'; |
| 81 | +import { LogAdvice } from '../common/advice/LogAdvice.ts'; |
| 82 | +import { countMw } from './middleware/count.ts'; |
| 83 | + |
| 84 | +@HTTPController({ path: '/api' }) |
| 85 | +@Middleware(LogAdvice) // 类级别:所有方法都会执行 |
| 86 | +export class FooController { |
| 87 | + @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' }) |
| 88 | + @Middleware(countMw) // 方法级别:仅此方法执行 |
| 89 | + async getProfile() { |
| 90 | + return { name: 'test' }; |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## 执行顺序 |
| 98 | + |
| 99 | +遵循洋葱模型,类级别先执行,方法级别后执行: |
| 100 | + |
| 101 | +```typescript |
| 102 | +@Middleware(globalMw) |
| 103 | +export class FooController { |
| 104 | + // 进:globalMw → methodMw → hello() |
| 105 | + // 出:hello() → methodMw → globalMw |
| 106 | + @Middleware(methodMw) |
| 107 | + async hello() {} |
| 108 | + |
| 109 | + // 多个 @Middleware 从下往上执行(靠近方法的先注册) |
| 110 | + // 进:globalMw → mw3 → mw2 → mw1 → multiple() |
| 111 | + // 出:multiple() → mw1 → mw2 → mw3 → globalMw |
| 112 | + @Middleware(mw1) |
| 113 | + @Middleware(mw2) |
| 114 | + @Middleware(mw3) |
| 115 | + async multiple() {} |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +**若混用函数式和 AOP 中间件,所有函数式中间件(无论类级别还是方法级别)会先于所有 AOP 中间件执行。** 即函数式和 AOP 分属两个独立的执行阶段,函数式阶段在前,AOP 阶段在后: |
| 120 | + |
| 121 | +```typescript |
| 122 | +@Middleware(countMw) // 函数式 - 类级别 |
| 123 | +@Middleware(LogAdvice) // AOP - 类级别 |
| 124 | +export class FooController { |
| 125 | + @Middleware(timeMw) // 函数式 - 方法级别 |
| 126 | + @Middleware(AuthAdvice) // AOP - 方法级别 |
| 127 | + async hello() {} |
| 128 | +} |
| 129 | + |
| 130 | +// 实际执行顺序: |
| 131 | +// countMw → timeMw → LogAdvice → AuthAdvice → hello() |
| 132 | +// (先所有函数式,再所有 AOP;各阶段内类级别先于方法级别) |
| 133 | +``` |
0 commit comments