@eggjs/skills 4.1.2-beta.5 → 4.1.2-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/egg/SKILL.md +93 -19
- package/egg-controller/SKILL.md +36 -6
- package/egg-controller/references/ajv-validate.md +146 -0
- package/egg-controller/references/middleware.md +133 -0
- package/egg-core/SKILL.md +70 -14
- package/egg-core/references/aop.md +219 -0
- package/egg-core/references/background-task.md +121 -0
- package/egg-core/references/eventbus.md +138 -0
- package/egg-unittest/SKILL.md +152 -0
- package/egg-unittest/references/background-task-test.md +55 -0
- package/egg-unittest/references/eventbus-test.md +47 -0
- package/egg-unittest/references/http-test.md +160 -0
- package/egg-unittest/references/mock.md +114 -0
- package/egg-unittest/references/service-test.md +75 -0
- package/package.json +6 -3
- package/PLAN.md +0 -396
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/egg/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: egg
|
|
3
|
-
description: 本技能用于处理 EGG
|
|
3
|
+
description: 本技能用于处理 EGG 框架。它提供基于用户意图在核心概念、控制器和单元测试之间做选择的决策指导。作为所有 EGG 相关问题的入口点使用。覆盖模块架构、依赖注入、后台任务、EventBus 事件总线、AOP 切面编程、HTTP/MCP/Schedule 控制器、Ajv 参数校验、单元测试等。
|
|
4
4
|
allowed-tools: Read
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -10,8 +10,9 @@ allowed-tools: Read
|
|
|
10
10
|
|
|
11
11
|
本技能帮助根据用户意图和任务类型确定使用哪个专用的 EGG 技能。EGG 文档组织为两个主要领域:
|
|
12
12
|
|
|
13
|
-
1. **核心概念**(`egg-core` skill
|
|
13
|
+
1. **核心概念**(`egg-core` skill):模块架构、依赖注入、对象生命周期、EventBus 事件总线、AOP 切面编程
|
|
14
14
|
2. **控制器**(`egg-controller` skill):用于 API 端点的各种协议特定控制器
|
|
15
|
+
3. **单元测试**(`egg-unittest` skill):HTTP 接口测试、Service/DI 对象测试、Mock 模拟、BackgroundTask 和 EventBus 测试
|
|
15
16
|
|
|
16
17
|
## 技能选择逻辑
|
|
17
18
|
|
|
@@ -26,6 +27,9 @@ allowed-tools: Read
|
|
|
26
27
|
- 模块之间的访问控制(`AccessLevel`)
|
|
27
28
|
- 模块配置(`module.yml`、`package.json`)
|
|
28
29
|
- 使用限定符解决命名冲突
|
|
30
|
+
- 请求返回后执行异步任务(BackgroundTaskHelper)
|
|
31
|
+
- 事件驱动架构(EventBus、@Event)
|
|
32
|
+
- AOP 切面编程(@Advice、@Pointcut、@Crosscut)
|
|
29
33
|
|
|
30
34
|
**触发关键词:**
|
|
31
35
|
|
|
@@ -34,6 +38,9 @@ allowed-tools: Read
|
|
|
34
38
|
- context、request context、@ContextProto
|
|
35
39
|
- inject、injection、dependency injection、@Inject
|
|
36
40
|
- prototype、lifecycle、实例化
|
|
41
|
+
- background task、异步任务、后台任务、BackgroundTaskHelper
|
|
42
|
+
- eventbus、event bus、事件总线、事件驱动、@Event、emit、发布订阅、解耦
|
|
43
|
+
- aop、切面、aspect、advice、pointcut、crosscut、拦截器、横切关注点
|
|
37
44
|
- access level、private、public、@ModuleQualifier
|
|
38
45
|
- configuration、module config
|
|
39
46
|
|
|
@@ -44,6 +51,9 @@ allowed-tools: Read
|
|
|
44
51
|
- "如何注入服务?"
|
|
45
52
|
- "如何访问其他模块的对象?"
|
|
46
53
|
- "EGG 中的 AccessLevel 是什么?"
|
|
54
|
+
- "如何用 EventBus 解耦异步任务?"
|
|
55
|
+
- "EventBus 和 BackgroundTaskHelper 有什么区别?"
|
|
56
|
+
- "如何用 AOP 给所有 Service 加日志?"
|
|
47
57
|
|
|
48
58
|
### 使用 `egg-controller` skill 当用户询问:
|
|
49
59
|
|
|
@@ -54,6 +64,8 @@ allowed-tools: Read
|
|
|
54
64
|
- 连接到外部系统或客户端
|
|
55
65
|
- 处理传入的请求/响应
|
|
56
66
|
- 控制器级别的装饰器和模式
|
|
67
|
+
- 参数校验(Ajv + TypeBox)
|
|
68
|
+
- 控制器中间件(Middleware,函数式或 AOP 写法)
|
|
57
69
|
|
|
58
70
|
**触发关键词:**
|
|
59
71
|
|
|
@@ -62,12 +74,47 @@ allowed-tools: Read
|
|
|
62
74
|
- MCP、LLM、AI、tool
|
|
63
75
|
- schedule、timer、cron、scheduled、定时
|
|
64
76
|
- SSE、streaming、server-sent events
|
|
77
|
+
- validate、校验、参数校验、ajv、typebox、schema
|
|
78
|
+
- middleware、中间件、拦截器、洋葱模型、@Middleware
|
|
65
79
|
|
|
66
80
|
**示例查询:**
|
|
67
81
|
|
|
68
82
|
- "如何创建 HTTP controller?"
|
|
69
83
|
- "如何实现 MCP 接口?"
|
|
70
84
|
- "怎么实现定时任务?"
|
|
85
|
+
- "帮我给接口加上参数校验"
|
|
86
|
+
- "如何给控制器加中间件?"
|
|
87
|
+
- "怎么写一个鉴权中间件?"
|
|
88
|
+
|
|
89
|
+
### 使用 `egg-unittest` skill 当用户询问:
|
|
90
|
+
|
|
91
|
+
**用户询问关于:**
|
|
92
|
+
|
|
93
|
+
- 编写单元测试或集成测试
|
|
94
|
+
- 使用 @eggjs/mock 进行测试
|
|
95
|
+
- 测试 HTTP 接口(app.httpRequest)
|
|
96
|
+
- 测试 Service/DI 对象
|
|
97
|
+
- Mock 数据或依赖
|
|
98
|
+
- 测试 BackgroundTaskHelper 或 EventBus
|
|
99
|
+
|
|
100
|
+
**触发关键词:**
|
|
101
|
+
|
|
102
|
+
- test、测试、单测、单元测试、unittest、unit test
|
|
103
|
+
- mock、mm、@eggjs/mock、mockCsrf、mockHttpclient、mockSession、mockContext
|
|
104
|
+
- httpRequest、supertest
|
|
105
|
+
- getEggObject、mockModuleContextScope
|
|
106
|
+
- eventWaiter、BackgroundTaskHelper 测试
|
|
107
|
+
- vitest、describe、it、beforeAll
|
|
108
|
+
|
|
109
|
+
**示例查询:**
|
|
110
|
+
|
|
111
|
+
- "如何写单元测试?"
|
|
112
|
+
- "如何测试 HTTP 接口?"
|
|
113
|
+
- "如何 mock 一个 Service?"
|
|
114
|
+
- "POST 请求测试报 CSRF 错误"
|
|
115
|
+
- "如何测试 ContextProto 的 Service?"
|
|
116
|
+
- "怎么测试后台任务是否执行完成?"
|
|
117
|
+
- "怎么测试 EventBus 事件是否被正确处理?"
|
|
71
118
|
|
|
72
119
|
---
|
|
73
120
|
|
|
@@ -106,14 +153,21 @@ allowed-tools: Read
|
|
|
106
153
|
|
|
107
154
|
### 步骤 3:协议/用例特定指示器
|
|
108
155
|
|
|
109
|
-
| 协议/用例
|
|
110
|
-
|
|
|
111
|
-
| HTTP API
|
|
112
|
-
| MCP
|
|
113
|
-
| Scheduled Tasks
|
|
114
|
-
|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
156
|
+
| 协议/用例 | 主要技能 | 次要技能 |
|
|
157
|
+
| ----------------------- | ---------------- | -------- |
|
|
158
|
+
| HTTP API | `egg-controller` | - |
|
|
159
|
+
| MCP | `egg-controller` | - |
|
|
160
|
+
| Scheduled Tasks | `egg-controller` | - |
|
|
161
|
+
| Parameter validation | `egg-controller` | - |
|
|
162
|
+
| Controller Middleware | `egg-controller` | - |
|
|
163
|
+
| Background tasks | `egg-core` | - |
|
|
164
|
+
| Event-driven / EventBus | `egg-core` | - |
|
|
165
|
+
| AOP / 切面编程 | `egg-core` | - |
|
|
166
|
+
| Cross-module injection | `egg-core` | - |
|
|
167
|
+
| Module structure | `egg-core` | - |
|
|
168
|
+
| Object lifecycle | `egg-core` | - |
|
|
169
|
+
| Unit Testing | `egg-unittest` | - |
|
|
170
|
+
| Mock / Test helpers | `egg-unittest` | - |
|
|
117
171
|
|
|
118
172
|
## 冲突解决规则
|
|
119
173
|
|
|
@@ -147,15 +201,22 @@ allowed-tools: Read
|
|
|
147
201
|
|
|
148
202
|
## 快速参考表
|
|
149
203
|
|
|
150
|
-
| 用户意图 | 关键词
|
|
151
|
-
| -------------------- |
|
|
152
|
-
| Module architecture | module、workspace、organization
|
|
153
|
-
| Object lifecycle | singleton、context、lifecycle
|
|
154
|
-
| Dependency injection | inject、@Inject、dependency
|
|
155
|
-
| Access control | private、public、cross-module
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
204
|
+
| 用户意图 | 关键词 | 使用技能 |
|
|
205
|
+
| -------------------- | --------------------------------------- | ---------------------- |
|
|
206
|
+
| Module architecture | module、workspace、organization | `egg-core` skill |
|
|
207
|
+
| Object lifecycle | singleton、context、lifecycle | `egg-core` skill |
|
|
208
|
+
| Dependency injection | inject、@Inject、dependency | `egg-core` skill |
|
|
209
|
+
| Access control | private、public、cross-module | `egg-core` skill |
|
|
210
|
+
| Background tasks | background task、异步任务、后台任务 | `egg-core` skill |
|
|
211
|
+
| Event-driven | eventbus、事件总线、@Event、emit | `egg-core` skill |
|
|
212
|
+
| AOP 切面编程 | aop、切面、advice、pointcut、crosscut | `egg-core` skill |
|
|
213
|
+
| HTTP endpoints | HTTP、API、REST | `egg-controller` skill |
|
|
214
|
+
| LLM/AI integration | MCP、tool、prompt | `egg-controller` skill |
|
|
215
|
+
| Scheduling | schedule、cron、timer | `egg-controller` skill |
|
|
216
|
+
| Param validation | validate、校验、ajv、typebox、schema | `egg-controller` skill |
|
|
217
|
+
| Middleware 中间件 | middleware、中间件、拦截器、@Middleware | `egg-controller` skill |
|
|
218
|
+
| Unit testing | test、mock、unittest、单测 | `egg-unittest` skill |
|
|
219
|
+
| Mock dependencies | mock、mm、mockCsrf、mockHttpclient | `egg-unittest` skill |
|
|
159
220
|
|
|
160
221
|
---
|
|
161
222
|
|
|
@@ -206,6 +267,18 @@ allowed-tools: Read
|
|
|
206
267
|
2. 简要解释 @Inject 如何工作(核心概念摘要)
|
|
207
268
|
3. 注意:"包含限定符的详细 @Inject 使用,请使用 `egg-core` skill"
|
|
208
269
|
|
|
270
|
+
### 示例 5:测试相关
|
|
271
|
+
|
|
272
|
+
**用户**:"帮我写个 UserController 的单元测试"
|
|
273
|
+
|
|
274
|
+
**分析**:问题关于编写测试代码
|
|
275
|
+
**决策**:使用 `egg-unittest` skill
|
|
276
|
+
|
|
277
|
+
**用户**:"POST 请求测试报 403 错误"
|
|
278
|
+
|
|
279
|
+
**分析**:测试中遇到 CSRF 问题
|
|
280
|
+
**决策**:使用 `egg-unittest` skill
|
|
281
|
+
|
|
209
282
|
## 路由最佳实践
|
|
210
283
|
|
|
211
284
|
1. **优先考虑显式控制器提及**:如果用户命名特定控制器(HTTP),即使涉及核心概念也使用控制器技能
|
|
@@ -226,5 +299,6 @@ allowed-tools: Read
|
|
|
226
299
|
|
|
227
300
|
- 为框架内部概念路由到 `egg-core` skill
|
|
228
301
|
- 为协议特定实现路由到 `egg-controller` skill
|
|
302
|
+
- 为测试相关问题路由到 `egg-unittest` skill
|
|
229
303
|
- 当意图模糊时提供决策指导
|
|
230
304
|
- 当存在有用的上下文时交叉引用技能
|
package/egg-controller/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: egg-controller
|
|
3
|
-
description: Use when creating API endpoints, implementing protocol handlers,
|
|
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.
|
|
4
4
|
allowed-tools: Read
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -18,6 +18,14 @@ allowed-tools: Read
|
|
|
18
18
|
2. 定时任务,可以使用 Schedule,参考 `references/schedule.md`
|
|
19
19
|
|
|
20
20
|
3. MCP 接口,可以使用 MCPController,参考 `references/mcp-controller.md`
|
|
21
|
+
|
|
22
|
+
需要做参数校验?
|
|
23
|
+
|
|
24
|
+
4. 使用 Ajv + TypeBox 做入参校验,参考 `references/ajv-validate.md`
|
|
25
|
+
|
|
26
|
+
需要给控制器加中间件(日志、鉴权、耗时统计等横切逻辑)?
|
|
27
|
+
|
|
28
|
+
5. 使用 Middleware,支持函数式写法和 AOP 写法,参考 `references/middleware.md`
|
|
21
29
|
```
|
|
22
30
|
|
|
23
31
|
---
|
|
@@ -42,15 +50,33 @@ allowed-tools: Read
|
|
|
42
50
|
- **模式**:Worker/All
|
|
43
51
|
- **详细文档**:`references/schedule.md`
|
|
44
52
|
|
|
53
|
+
### Ajv 参数校验
|
|
54
|
+
|
|
55
|
+
- **导入**:`import { Ajv, Type, Static } from 'egg/ajv'`
|
|
56
|
+
- **方式**:`@Inject() ajv: Ajv`,调用 `ajv.validate(schema, data)`
|
|
57
|
+
- **特点**:TypeBox 定义一次 Schema,同时获得校验和 TypeScript 类型
|
|
58
|
+
- **详细文档**:`references/ajv-validate.md`
|
|
59
|
+
|
|
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
|
+
|
|
45
67
|
---
|
|
46
68
|
|
|
47
69
|
## 常见问题排查
|
|
48
70
|
|
|
49
|
-
| 现象 | 原因
|
|
50
|
-
| ---------------------- |
|
|
51
|
-
| MCP 装饰器 import 报错 | 从 `'egg'` 导入
|
|
52
|
-
| MCP Schema 报错 | 用了 `z.object()` 包装
|
|
53
|
-
| 定时任务不生效 | 放在 `app/schedule` 目录
|
|
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 |
|
|
54
80
|
|
|
55
81
|
---
|
|
56
82
|
|
|
@@ -71,5 +97,9 @@ allowed-tools: Read
|
|
|
71
97
|
- `references/http-controller.md` - HTTP 接口完整指南
|
|
72
98
|
- `references/mcp-controller.md` - MCP 接口开发
|
|
73
99
|
- `references/schedule.md` - 定时任务
|
|
100
|
+
- `references/ajv-validate.md` - Ajv 参数校验
|
|
101
|
+
- `references/middleware.md` - Middleware 中间件(函数式 + AOP)
|
|
74
102
|
|
|
75
103
|
核心概念(`egg-core` skill):模块、依赖注入、对象生命周期
|
|
104
|
+
|
|
105
|
+
单元测试(`egg-unittest` skill):HTTP 接口测试、Service 测试、Mock
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Ajv 参数校验指南
|
|
2
|
+
|
|
3
|
+
## 常见错误
|
|
4
|
+
|
|
5
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
6
|
+
| -------------------------------- | --------------------------------- | ---------------------------------------------- |
|
|
7
|
+
| `import { Type } from 'typebox'` | `import { Type } from 'egg/ajv'` | 必须从 `egg/ajv` 导入,内部已封装 typebox |
|
|
8
|
+
| `import { Ajv } from 'ajv'` | `import { Ajv } from 'egg/ajv'` | Ajv 实例通过 `egg/ajv` 导出 |
|
|
9
|
+
| `new Ajv()` 手动创建实例 | `@Inject() ajv: Ajv` 注入全局单例 | 框架已配置好 formats 和 keywords,不要自行创建 |
|
|
10
|
+
| 在 Service 中做参数校验 | 在 Controller 中做参数校验 | 入参校验应在 Controller 层完成 |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 核心概念
|
|
15
|
+
|
|
16
|
+
Egg 通过 `@eggjs/typebox-validate` 插件集成 Ajv(JSON Schema 校验库)和 TypeBox(TypeScript-first 的 JSON Schema 构建器)。**定义一次 Schema,同时获得参数校验和 TypeScript 类型推导。**
|
|
17
|
+
|
|
18
|
+
### 导入路径
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// 所有 Ajv/TypeBox 相关导入统一从 egg/ajv
|
|
22
|
+
import { Ajv, Type, Static, TransformEnum } from 'egg/ajv';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 使用方式
|
|
28
|
+
|
|
29
|
+
通过 `@Inject()` 注入全局 Ajv 单例,在 Controller 方法中调用 `ajv.validate()` 进行校验。
|
|
30
|
+
|
|
31
|
+
### 完整示例
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// app/userModule/UserController.ts
|
|
35
|
+
import {
|
|
36
|
+
HTTPController,
|
|
37
|
+
HTTPMethod,
|
|
38
|
+
HTTPMethodEnum,
|
|
39
|
+
HTTPBody,
|
|
40
|
+
Inject,
|
|
41
|
+
} from 'egg';
|
|
42
|
+
import { Ajv, Type, Static, TransformEnum } from 'egg/ajv';
|
|
43
|
+
|
|
44
|
+
// 1. 定义 Schema
|
|
45
|
+
const CreateUserSchema = Type.Object({
|
|
46
|
+
name: Type.String({
|
|
47
|
+
transform: [TransformEnum.trim],
|
|
48
|
+
minLength: 1,
|
|
49
|
+
maxLength: 50,
|
|
50
|
+
}),
|
|
51
|
+
email: Type.String({ format: 'email' }),
|
|
52
|
+
age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 2. 从 Schema 推导类型
|
|
56
|
+
type CreateUserParams = Static<typeof CreateUserSchema>;
|
|
57
|
+
|
|
58
|
+
// 3. 在 Controller 中注入 Ajv 并校验
|
|
59
|
+
@HTTPController()
|
|
60
|
+
export class UserController {
|
|
61
|
+
@Inject()
|
|
62
|
+
private readonly ajv: Ajv;
|
|
63
|
+
|
|
64
|
+
@HTTPMethod({
|
|
65
|
+
method: HTTPMethodEnum.POST,
|
|
66
|
+
path: '/api/users',
|
|
67
|
+
})
|
|
68
|
+
async create(@HTTPBody() body: CreateUserParams) {
|
|
69
|
+
// 校验失败自动抛出 AjvInvalidParamError
|
|
70
|
+
this.ajv.validate(CreateUserSchema, body);
|
|
71
|
+
|
|
72
|
+
// 校验通过,body 已经有完整类型提示
|
|
73
|
+
return { name: body.name, email: body.email };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Schema 定义
|
|
81
|
+
|
|
82
|
+
### 常用类型
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Type } from 'egg/ajv';
|
|
86
|
+
|
|
87
|
+
Type.String() // string
|
|
88
|
+
Type.Number() // number
|
|
89
|
+
Type.Integer() // 整数
|
|
90
|
+
Type.Boolean() // boolean
|
|
91
|
+
Type.Optional(Type.String()) // string | undefined
|
|
92
|
+
Type.Array(Type.String()) // string[]
|
|
93
|
+
Type.Object({ name: Type.String() }) // { name: string }
|
|
94
|
+
Type.Union([Type.String(), Type.Number()]) // string | number
|
|
95
|
+
Type.Literal('admin') // 'admin'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
完整的 TypeBox JSON Schema 类型定义参考:https://github.com/sinclairzx81/typebox#json-types
|
|
99
|
+
|
|
100
|
+
### 内置 format 校验
|
|
101
|
+
|
|
102
|
+
框架通过 `ajv-formats` 预注册了以下格式:
|
|
103
|
+
|
|
104
|
+
| format | 说明 | 示例 |
|
|
105
|
+
| ----------- | ---------- | -------------------------------------- |
|
|
106
|
+
| `email` | 邮箱 | `user@example.com` |
|
|
107
|
+
| `uri` | URI | `https://example.com` |
|
|
108
|
+
| `uuid` | UUID | `550e8400-e29b-41d4-a716-446655440000` |
|
|
109
|
+
| `date` | 日期 | `2024-01-01` |
|
|
110
|
+
| `date-time` | 日期时间 | `2024-01-01T00:00:00Z` |
|
|
111
|
+
| `time` | 时间 | `12:00:00` |
|
|
112
|
+
| `ipv4` | IPv4 | `192.168.1.1` |
|
|
113
|
+
| `ipv6` | IPv6 | `::1` |
|
|
114
|
+
| `hostname` | 主机名 | `example.com` |
|
|
115
|
+
| `regex` | 正则表达式 | `^\\d+$` |
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
Type.String({ format: 'email' })
|
|
119
|
+
Type.String({ format: 'uuid' })
|
|
120
|
+
Type.String({ format: 'date-time' })
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### transform 预处理
|
|
124
|
+
|
|
125
|
+
通过 `ajv-keywords` 的 `transform` 关键字,在校验前对字符串做预处理:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { TransformEnum } from 'egg/ajv';
|
|
129
|
+
|
|
130
|
+
Type.String({
|
|
131
|
+
transform: [TransformEnum.trim], // 去除首尾空格
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
Type.String({
|
|
135
|
+
transform: [TransformEnum.trim, TransformEnum.toLowerCase], // 去空格 + 转小写
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 最佳实践
|
|
142
|
+
|
|
143
|
+
- **Schema 与 Controller 同文件** — Schema 定义放在使用它的 Controller 文件中,保持就近原则
|
|
144
|
+
- **校验在 Controller 层** — 不要在 Service 层做入参校验,Service 信任上层传入的数据
|
|
145
|
+
- **善用 Optional** — 非必填字段用 `Type.Optional()` 包装,避免前端遗漏字段导致校验失败
|
|
146
|
+
- **善用 transform** — 对用户输入做 trim 预处理,减少脏数据
|
|
@@ -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
|
+
```
|