Skip to content

Commit 8fa3c4c

Browse files
gxklclaude
andauthored
feat(skills): add Middleware and unittest skills (#5836)
## Summary - Add Middleware skill to `egg-controller`, covering AOP (recommended) and function-style (legacy) patterns - Add `egg-unittest` skill with 5 reference docs: HTTP test, Service/DI test, Mock patterns, BackgroundTask test, EventBus test - Update entry skill (`egg/SKILL.md`) routing for middleware and unittest queries - Add cross-references from `egg-controller` and `egg-core` skills to `egg-unittest` - Narrow `@eggjs/skills` package published files to skill directories only - Add 8 middleware evals and 24 unittest evals ## Test plan - [x] Middleware evals: with-skill 3.94/4 vs site-docs 3.06/4 (8 cases) - [x] Unittest evals: with-skill 4.0/4 vs site-docs 3.52/4 (22 cases) - [x] `npm pack --dry-run` verify only skill dirs included 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added a new unit-testing skill with end-to-end guides and examples for HTTP, Service/DI, mocks, background tasks, and EventBus testing. * Added controller middleware guidance covering function-style and AOP middleware, application patterns, execution order, and troubleshooting. * Expanded core/controller decision guides and quick-reference entries to include testing and middleware scenarios; added many example tests and common-mistake tables. * **Chores** * Narrowed published doc file inclusion and generalized workspace ignore rules. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1980cf1 commit 8fa3c4c

18 files changed

Lines changed: 1091 additions & 66 deletions

File tree

packages/skills/CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ packages/skills/eval/
117117
├── evals-egg-core.json # egg-core skill 评测用例
118118
├── evals-egg-controller.json # egg-controller skill 评测用例
119119
├── evals-routing.json # 入口路由评测用例
120-
├── .gitignore # 忽略 workspace/ 和 egg-workspace/
121-
└── egg-workspace/ # 评测输出(gitignored),由 /skill-creator 管理
120+
├── .gitignore # 忽略 *-workspace/ 目录
121+
└── <skill-name>-workspace/ # 评测输出(gitignored),由 /skill-creator 管理
122122
└── iteration-N/
123123
├── REPORT.md # 对比评分报告
124124
├── GRADING.md # with-skill 通过率报告
@@ -195,7 +195,7 @@ site-docs 环境:
195195

196196
**输出目录:**
197197

198-
评测结果保存到 `egg-workspace/iteration-N/` 目录下,具体目录结构由 `/skill-creator` 管理。
198+
评测结果保存到 `packages/skills/eval/<skill-name>-workspace/iteration-N/` 目录下(已在 `.gitignore` 中通过 `*-workspace/` 忽略),具体目录结构由 `/skill-creator` 管理。
199199

200200
**评测用例设计原则:**
201201

packages/skills/egg-controller/SKILL.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: egg-controller
3-
description: Use when creating API endpoints, implementing protocol handlers, exposing interfaces for specific clients, or adding parameter validation. Covers HTTP, MCP and Schedule controllers, and Ajv/TypeBox parameter validation for EGG framework applications.
3+
description: Use when creating API endpoints, implementing protocol handlers, exposing interfaces for specific clients, adding parameter validation, or applying middleware to controllers. Covers HTTP, MCP and Schedule controllers, Ajv/TypeBox parameter validation, and Middleware (function-style and AOP) for EGG framework applications.
44
allowed-tools: Read
55
---
66

@@ -22,6 +22,10 @@ allowed-tools: Read
2222
需要做参数校验?
2323
2424
4. 使用 Ajv + TypeBox 做入参校验,参考 `references/ajv-validate.md`
25+
26+
需要给控制器加中间件(日志、鉴权、耗时统计等横切逻辑)?
27+
28+
5. 使用 Middleware,支持函数式写法和 AOP 写法,参考 `references/middleware.md`
2529
```
2630

2731
---
@@ -53,17 +57,26 @@ allowed-tools: Read
5357
- **特点**:TypeBox 定义一次 Schema,同时获得校验和 TypeScript 类型
5458
- **详细文档**`references/ajv-validate.md`
5559

60+
### Middleware 中间件
61+
62+
- **导入**`import { Middleware } from 'egg'`(AOP 类从 `import { Advice } from 'egg/aop'`
63+
- **写法**:函数式(Koa 中间件函数)和 AOP(@Advice 类),通过 `@Middleware()` 应用
64+
- **特点**:支持类级别和方法级别,函数式和 AOP 不能在同一个 `@Middleware()` 中混用
65+
- **详细文档**`references/middleware.md`
66+
5667
---
5768

5869
## 常见问题排查
5970

60-
| 现象 | 原因 | 解决方案 |
61-
| ---------------------- | -------------------------- | ------------------------------------------------------------------ |
62-
| MCP 装饰器 import 报错 |`'egg'` 导入 | MCP 装饰器从 `'@eggjs/tegg'` 导入,zod 从 `'@eggjs/tegg/zod'` 导入 |
63-
| MCP Schema 报错 | 用了 `z.object()` 包装 | 直接用普通对象 `{ name: z.string() }` |
64-
| 定时任务不生效 | 放在 `app/schedule` 目录 | 放在模块目录中,避免和 egg 默认扫描冲突 |
65-
| Ajv 校验 import 报错 |`typebox``ajv` 导入 | 统一从 `'egg/ajv'` 导入 Type、Static、Ajv 等 |
66-
| type 推导不完整 |`type` 定义 |`interface Foo extends Static<typeof Schema> {}` 代替 |
71+
| 现象 | 原因 | 解决方案 |
72+
| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------ |
73+
| MCP 装饰器 import 报错 |`'egg'` 导入 | MCP 装饰器从 `'@eggjs/tegg'` 导入,zod 从 `'@eggjs/tegg/zod'` 导入 |
74+
| MCP Schema 报错 | 用了 `z.object()` 包装 | 直接用普通对象 `{ name: z.string() }` |
75+
| 定时任务不生效 | 放在 `app/schedule` 目录 | 放在模块目录中,避免和 egg 默认扫描冲突 |
76+
| Ajv 校验 import 报错 |`typebox``ajv` 导入 | 统一从 `'egg/ajv'` 导入 Type、Static、Ajv 等 |
77+
| type 推导不完整 |`type` 定义 |`interface Foo extends Static<typeof Schema> {}` 代替 |
78+
| Middleware 混用报错 | 函数和 Advice 类放同一个 `@Middleware()` | 分开写多个 `@Middleware()`,每个内部类型一致 |
79+
| AOP Middleware 不生效 | Advice 类没加 `@Advice()` 装饰器 | 必须同时有 `@Advice()` 装饰器才能被识别为 AOP |
6780

6881
---
6982

@@ -85,5 +98,8 @@ allowed-tools: Read
8598
- `references/mcp-controller.md` - MCP 接口开发
8699
- `references/schedule.md` - 定时任务
87100
- `references/ajv-validate.md` - Ajv 参数校验
101+
- `references/middleware.md` - Middleware 中间件(函数式 + AOP)
88102

89103
核心概念(`egg-core` skill):模块、依赖注入、对象生命周期
104+
105+
单元测试(`egg-unittest` skill):HTTP 接口测试、Service 测试、Mock
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
```

packages/skills/egg-core/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,5 @@ AOP 用于将日志、鉴权、缓存、事务等横切关注点从业务代码
244244
- 请求后异步任务(BackgroundTaskHelper),请参阅:`references/background-task.md`
245245
- 事件总线(EventBus),请参阅:`references/eventbus.md`
246246
- AOP 切面编程(Advice、Pointcut、Crosscut),请参阅:`references/aop.md`
247+
248+
单元测试(`egg-unittest` skill):Service 测试、BackgroundTask 测试、EventBus 测试

packages/skills/egg-core/references/background-task.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,9 @@ BackgroundTaskHelper 的作用是:
113113
4. 等待结束后才释放上下文
114114

115115
这就是为什么必须用 `backgroundTaskHelper.run()` 而不是 `setTimeout` — 后者绕过了框架的上下文生命周期管理,执行时上下文可能已被释放。
116+
117+
---
118+
119+
## 单元测试
120+
121+
BackgroundTaskHelper 的测试方法参考 `egg-unittest` skill 的 `references/background-task-test.md`

packages/skills/egg-core/references/eventbus.md

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -135,39 +135,4 @@ export class BatchService {
135135

136136
## 单元测试
137137

138-
使用 `app.getEventWaiter()` 等待事件被处理完成,避免使用 `sleep`
139-
140-
```typescript
141-
import { describe, it, expect } from 'vitest';
142-
import { mm, type MockApplication } from '@eggjs/mock';
143-
import { OrderService } from './path/to/OrderService.ts';
144-
145-
describe('order events', () => {
146-
let app: MockApplication;
147-
148-
it('should emit orderCreated event', async () => {
149-
await app.mockModuleContextScope(async (ctx) => {
150-
const orderService = await ctx.getEggObject(OrderService);
151-
const eventWaiter = await app.getEventWaiter();
152-
153-
// 准备等待事件
154-
const eventPromise = eventWaiter.await('orderCreated');
155-
156-
// 触发业务逻辑
157-
await orderService.createOrder('user1', []);
158-
159-
// 等待事件处理完成
160-
await eventPromise;
161-
162-
// 断言 handler 的执行结果
163-
});
164-
});
165-
});
166-
```
167-
168-
**EventWaiter API:**
169-
170-
- `eventWaiter.await('eventName')` — 等待指定事件触发
171-
- `eventWaiter.awaitFirst('event1', 'event2')` — 等待多个事件中最先触发的一个
172-
173-
**注意:** `eventWaiter.await()` 必须在 `emit()` 之前调用(先注册等待,再触发事件),否则可能错过事件。
138+
EventBus 的测试方法参考 `egg-unittest` skill 的 `references/eventbus-test.md`

0 commit comments

Comments
 (0)