@eggjs/skills 0.0.0 → 4.1.2-beta.5
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/PLAN.md +396 -0
- package/egg/SKILL.md +230 -0
- package/egg-controller/SKILL.md +75 -0
- package/egg-controller/references/http-controller.md +467 -0
- package/egg-controller/references/mcp-controller.md +289 -0
- package/egg-controller/references/schedule.md +110 -0
- package/egg-core/SKILL.md +192 -0
- package/egg-core/references/dynamic-inject.md +91 -0
- package/egg-core/references/inject.md +258 -0
- package/egg-core/references/module.md +202 -0
- package/egg-core/references/proto.md +181 -0
- package/package.json +15 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# MCPController 开发指南
|
|
2
|
+
|
|
3
|
+
## 常见错误
|
|
4
|
+
|
|
5
|
+
生成 MCPController 代码时,**必须**注意以下易错点:
|
|
6
|
+
|
|
7
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
8
|
+
| -------------------------------- | ------------------------------------- | ---------------------------------------- |
|
|
9
|
+
| `from 'egg'` | `from '@eggjs/tegg'` | 所有 MCP 装饰器和类型来自 `@eggjs/tegg` |
|
|
10
|
+
| `import z from 'zod'` | `import { z } from '@eggjs/tegg/zod'` | 框架内置 zod,必须使用具名导入 |
|
|
11
|
+
| `z.object({ name: z.string() })` | `{ name: z.string() }` | Schema 使用普通对象,不要用 `z.object()` |
|
|
12
|
+
| `args: ToolArgs<MySchema>` | `args: ToolArgs<typeof MySchema>` | 类型参数必须用 `typeof` |
|
|
13
|
+
| `@MCPController` 不加括号 | `@MCPController()` | 装饰器必须带括号调用 |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 文件约定
|
|
18
|
+
|
|
19
|
+
### 文件位置与命名
|
|
20
|
+
|
|
21
|
+
MCPController 放在 module 的 `controller/` 目录下,命名规则为 `{Name}MCPController.ts`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
app/module-name/
|
|
25
|
+
├── controller/
|
|
26
|
+
│ ├── PackageMCPController.ts ← MCP 控制器
|
|
27
|
+
│ └── PackageHTTPController.ts ← 同模块可共存 HTTP 控制器
|
|
28
|
+
└── service/
|
|
29
|
+
└── PackageService.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 插件配置
|
|
33
|
+
|
|
34
|
+
在 `config/plugin.ts` 中启用:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
plugin.mcpProxy = true;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 路径配置
|
|
41
|
+
|
|
42
|
+
在 `config/config.default.ts` 中配置 MCP 路径(通常不需要修改,以下为默认值):
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { randomUUID } from 'node:crypto';
|
|
46
|
+
|
|
47
|
+
export default () => {
|
|
48
|
+
const config = {
|
|
49
|
+
mcp: {
|
|
50
|
+
sseInitPath: '/mcp/sse',
|
|
51
|
+
sseMessagePath: '/mcp/message',
|
|
52
|
+
streamPath: '/mcp/stream',
|
|
53
|
+
statelessStreamPath: '/mcp/stateless/stream',
|
|
54
|
+
sessionIdGenerator: randomUUID,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return config;
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
当使用 `@MCPController({ name: 'myServer' })` 声明命名服务时,路径自动变为:
|
|
62
|
+
|
|
63
|
+
- `/mcp/myServer/sse`
|
|
64
|
+
- `/mcp/myServer/message`
|
|
65
|
+
- `/mcp/myServer/stream`
|
|
66
|
+
- `/mcp/myServer/stateless/stream`
|
|
67
|
+
|
|
68
|
+
### AccessLevel
|
|
69
|
+
|
|
70
|
+
`@MCPController` 装饰器内部已默认设置 AccessLevel(PUBLIC),不需要再手动声明。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 场景决策树
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
用户需要什么?
|
|
78
|
+
|
|
79
|
+
├─ "让 AI 能查数据 / 执行操作"
|
|
80
|
+
│ └─ → @MCPTool + @Inject Service 处理业务
|
|
81
|
+
│
|
|
82
|
+
├─ "给 AI 一个提示词模板"
|
|
83
|
+
│ └─ → @MCPPrompt
|
|
84
|
+
│
|
|
85
|
+
├─ "让 AI 读取某类资源数据"
|
|
86
|
+
│ ├─ 资源地址固定 → @MCPResource({ uri: '...' })
|
|
87
|
+
│ └─ 资源地址动态 → @MCPResource({ template: [...] })
|
|
88
|
+
│
|
|
89
|
+
├─ "Tool 执行中要推送进度"
|
|
90
|
+
│ └─ → @MCPTool + @Extra() 获取 sendNotification(见下方 @Extra 章节)
|
|
91
|
+
│
|
|
92
|
+
└─ "Tool 中需要读取自定义请求头"
|
|
93
|
+
└─ → @MCPTool + @Extra() 获取 requestInfo.headers
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 端到端完整示例
|
|
99
|
+
|
|
100
|
+
以下展示一个完整的 MCP 功能从配置到测试的所有文件:
|
|
101
|
+
|
|
102
|
+
### 1. 插件配置 — `config/plugin.ts`
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
plugin.mcpProxy = true;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 2. 控制器 — `app/npm/controller/PackageMCPController.ts`
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import {
|
|
112
|
+
MCPController, MCPTool, MCPToolResponse,
|
|
113
|
+
MCPPrompt, MCPPromptResponse,
|
|
114
|
+
MCPResource, MCPResourceResponse,
|
|
115
|
+
ToolArgs, ToolArgsSchema,
|
|
116
|
+
PromptArgs, PromptArgsSchema,
|
|
117
|
+
Inject,
|
|
118
|
+
} from '@eggjs/tegg';
|
|
119
|
+
import { z } from '@eggjs/tegg/zod';
|
|
120
|
+
|
|
121
|
+
import { PackageService } from '../service/PackageService.ts';
|
|
122
|
+
|
|
123
|
+
const SearchSchema = {
|
|
124
|
+
name: z.string({ description: 'npm package name' }),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const SummarySchema = {
|
|
128
|
+
name: z.string(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
@MCPController()
|
|
132
|
+
export class PackageMCPController {
|
|
133
|
+
@Inject()
|
|
134
|
+
private readonly packageService: PackageService;
|
|
135
|
+
|
|
136
|
+
@MCPTool({ description: 'Search npm package info' })
|
|
137
|
+
async searchPackage(
|
|
138
|
+
@ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>,
|
|
139
|
+
): Promise<MCPToolResponse> {
|
|
140
|
+
const pkg = await this.packageService.findByName(args.name);
|
|
141
|
+
if (!pkg) {
|
|
142
|
+
return { content: [{ type: 'text', text: `Package ${args.name} not found` }] };
|
|
143
|
+
}
|
|
144
|
+
return { content: [{ type: 'text', text: JSON.stringify(pkg) }] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@MCPPrompt({ description: 'Generate package summary' })
|
|
148
|
+
async summarize(
|
|
149
|
+
@PromptArgsSchema(SummarySchema) args: PromptArgs<typeof SummarySchema>,
|
|
150
|
+
): Promise<MCPPromptResponse> {
|
|
151
|
+
return {
|
|
152
|
+
messages: [{
|
|
153
|
+
role: 'user',
|
|
154
|
+
content: {
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: `Summarize the npm package: ${args.name}`,
|
|
157
|
+
},
|
|
158
|
+
}],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@MCPResource({
|
|
163
|
+
template: ['npm://{name}/{?version}', { list: undefined }],
|
|
164
|
+
})
|
|
165
|
+
async getPackageReadme(uri: URL): Promise<MCPResourceResponse> {
|
|
166
|
+
const name = uri.hostname;
|
|
167
|
+
const readme = await this.packageService.getReadme(name);
|
|
168
|
+
return { contents: [{ uri: uri.toString(), text: readme }] };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 3. Service — `app/npm/service/PackageService.ts`
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { SingletonProto } from 'egg';
|
|
177
|
+
|
|
178
|
+
@SingletonProto()
|
|
179
|
+
export class PackageService {
|
|
180
|
+
async findByName(name: string) {
|
|
181
|
+
// 业务逻辑
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getReadme(name: string): Promise<string> {
|
|
185
|
+
// 业务逻辑
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 4. 单元测试 — `test/npm/controller/PackageMCPController.test.ts`
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import assert from 'node:assert';
|
|
194
|
+
import { app } from 'egg-mock/bootstrap';
|
|
195
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
196
|
+
|
|
197
|
+
describe('PackageMCPController', () => {
|
|
198
|
+
it('should search package via tool', async () => {
|
|
199
|
+
app.mockCsrf();
|
|
200
|
+
const client: Client = await app.mcpClient();
|
|
201
|
+
|
|
202
|
+
const tools = await client.listTools();
|
|
203
|
+
assert(tools.tools.some(t => t.name === 'searchPackage'));
|
|
204
|
+
|
|
205
|
+
const res = await client.callTool({
|
|
206
|
+
name: 'searchPackage',
|
|
207
|
+
arguments: { name: 'egg' },
|
|
208
|
+
});
|
|
209
|
+
assert(res.content[0].type === 'text');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should get prompt', async () => {
|
|
213
|
+
app.mockCsrf();
|
|
214
|
+
const client: Client = await app.mcpClient();
|
|
215
|
+
|
|
216
|
+
const res = await client.getPrompt({
|
|
217
|
+
name: 'summarize',
|
|
218
|
+
arguments: { name: 'egg' },
|
|
219
|
+
});
|
|
220
|
+
assert(res.messages.length > 0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should read resource', async () => {
|
|
224
|
+
app.mockCsrf();
|
|
225
|
+
const client: Client = await app.mcpClient();
|
|
226
|
+
|
|
227
|
+
const res = await client.readResource({
|
|
228
|
+
uri: 'npm://egg?version=4.0.0',
|
|
229
|
+
});
|
|
230
|
+
assert(res.contents.length > 0);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## @Extra() 的使用场景
|
|
238
|
+
|
|
239
|
+
`@Extra()` 装饰器注入 `ToolExtra` 对象,提供两个能力:
|
|
240
|
+
|
|
241
|
+
### 发送通知(长任务进度推送)
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
@MCPTool()
|
|
245
|
+
async longTask(
|
|
246
|
+
@ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,
|
|
247
|
+
@Extra() extra: ToolExtra,
|
|
248
|
+
): Promise<MCPToolResponse> {
|
|
249
|
+
const { sendNotification } = extra;
|
|
250
|
+
for (let i = 0; i < 10; i++) {
|
|
251
|
+
await sendNotification({
|
|
252
|
+
method: 'notifications/message',
|
|
253
|
+
params: { level: 'info', data: `Step ${i + 1}/10` },
|
|
254
|
+
});
|
|
255
|
+
// ... 执行步骤
|
|
256
|
+
}
|
|
257
|
+
return { content: [{ type: 'text', text: 'Done' }] };
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 读取自定义请求头
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
@MCPTool()
|
|
265
|
+
async myTool(
|
|
266
|
+
@ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,
|
|
267
|
+
@Extra() extra: ToolExtra,
|
|
268
|
+
): Promise<MCPToolResponse> {
|
|
269
|
+
const headers = extra.requestInfo?.headers;
|
|
270
|
+
// 处理自定义 header
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 装饰器参考
|
|
277
|
+
|
|
278
|
+
| 装饰器 | 用途 | 常用参数 | 返回类型 |
|
|
279
|
+
| --------------------- | ------------ | ------------------------------------------ | --------------------- |
|
|
280
|
+
| `@MCPController()` | 声明控制器 | `{ name?: string }` | - |
|
|
281
|
+
| `@MCPTool()` | 声明工具 | `{ name?: string, description?: string }` | `MCPToolResponse` |
|
|
282
|
+
| `@MCPPrompt()` | 声明提示词 | `{ name?: string, description?: string }` | `MCPPromptResponse` |
|
|
283
|
+
| `@MCPResource()` | 声明资源 | `{ uri: string }` 或 `{ template: [...] }` | `MCPResourceResponse` |
|
|
284
|
+
| `@ToolArgsSchema()` | Tool 参数 | Zod Schema 普通对象 | - |
|
|
285
|
+
| `@PromptArgsSchema()` | Prompt 参数 | Zod Schema 普通对象 | - |
|
|
286
|
+
| `@Extra()` | 额外上下文 | - | `ToolExtra` |
|
|
287
|
+
| `@Inject()` | 注入 Service | - | - |
|
|
288
|
+
|
|
289
|
+
**注意**:`@MCPController` 的 `version`、`timeout` 等参数通常不需要配置。
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# 定时任务开发指南
|
|
2
|
+
|
|
3
|
+
## 注意事项
|
|
4
|
+
|
|
5
|
+
- **不要将代码放在 `app/schedule` 目录下**,egg 默认会扫描该路径注册定时任务,会和装饰器方式冲突
|
|
6
|
+
- 定时任务类必须包含一个 `subscribe` 方法,框架调度时会调用该方法
|
|
7
|
+
- import 路径是 `egg/schedule`,不是 `egg`
|
|
8
|
+
|
|
9
|
+
## Step 1: 创建定时任务
|
|
10
|
+
|
|
11
|
+
使用 `@Schedule` 装饰器标识一个类为定时任务,支持 interval 和 cron 两种调度模式。
|
|
12
|
+
|
|
13
|
+
### interval 模式
|
|
14
|
+
|
|
15
|
+
按固定间隔执行。`interval` 支持毫秒数或 [ms](https://github.com/vercel/ms) 格式字符串(如 `'5s'`、`'1m'`)。
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// app/{moduleName}/schedule/Demo.ts
|
|
19
|
+
import { Inject, Logger } from 'egg';
|
|
20
|
+
import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';
|
|
21
|
+
|
|
22
|
+
@Schedule<IntervalParams>({
|
|
23
|
+
type: ScheduleType.WORKER,
|
|
24
|
+
scheduleData: {
|
|
25
|
+
interval: '5s',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
export class DemoScheduler {
|
|
29
|
+
@Inject()
|
|
30
|
+
private logger: Logger;
|
|
31
|
+
|
|
32
|
+
async subscribe() {
|
|
33
|
+
this.logger.info('schedule called');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### cron 模式
|
|
39
|
+
|
|
40
|
+
按 cron 表达式执行,格式参考 [cron-parser](https://github.com/harrisiirak/cron-parser):
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
* * * * * *
|
|
44
|
+
┬ ┬ ┬ ┬ ┬ ┬
|
|
45
|
+
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
|
|
46
|
+
│ │ │ │ └───── month (1 - 12)
|
|
47
|
+
│ │ │ └────────── day of month (1 - 31)
|
|
48
|
+
│ │ └─────────────── hour (0 - 23)
|
|
49
|
+
│ └──────────────────── minute (0 - 59)
|
|
50
|
+
└───────────────────────── second (0 - 59, optional)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// app/{moduleName}/schedule/CronDemo.ts
|
|
55
|
+
import { Inject, Logger } from 'egg';
|
|
56
|
+
import { CronParams, Schedule, ScheduleType } from 'egg/schedule';
|
|
57
|
+
|
|
58
|
+
@Schedule<CronParams>({
|
|
59
|
+
type: ScheduleType.WORKER,
|
|
60
|
+
scheduleData: {
|
|
61
|
+
cron: '0 0 3 * * *', // 每日 3 点执行
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
export class CronScheduler {
|
|
65
|
+
@Inject()
|
|
66
|
+
private logger: Logger;
|
|
67
|
+
|
|
68
|
+
async subscribe() {
|
|
69
|
+
this.logger.info('schedule called');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Step 2: 选择工作模式
|
|
75
|
+
|
|
76
|
+
| 模式 | 说明 | 使用场景 |
|
|
77
|
+
| --------------------- | ---------------------------------------- | ------------------------------------ |
|
|
78
|
+
| `ScheduleType.WORKER` | 每台机器只有一个 worker 执行(随机选择) | 大多数场景,如数据同步、缓存刷新 |
|
|
79
|
+
| `ScheduleType.ALL` | 每台机器的所有 worker 都执行 | 需要每个 worker 都更新本地状态的场景 |
|
|
80
|
+
|
|
81
|
+
## Step 3: 配置运行参数
|
|
82
|
+
|
|
83
|
+
`@Schedule` 装饰器支持第二个参数,控制定时任务的运行行为:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
@Schedule<IntervalParams>(
|
|
87
|
+
{
|
|
88
|
+
type: ScheduleType.WORKER,
|
|
89
|
+
scheduleData: {
|
|
90
|
+
interval: '1m',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
immediate: true, // 应用启动后立即执行一次
|
|
95
|
+
// disable: true, // 禁用该定时任务
|
|
96
|
+
env: ['devserver', 'test'], // 仅在指定环境下启动
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
export class MyScheduler {
|
|
100
|
+
async subscribe() {
|
|
101
|
+
// ...
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| 参数 | 类型 | 说明 |
|
|
107
|
+
| ----------- | -------- | ------------------------------- |
|
|
108
|
+
| `immediate` | boolean | 应用启动并 ready 后立即执行一次 |
|
|
109
|
+
| `disable` | boolean | 设为 true 时不启动该定时任务 |
|
|
110
|
+
| `env` | string[] | 仅在指定环境下启动 |
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: egg-core
|
|
3
|
+
description: 本技能用于处理 EGG 基础核心概念,包括模块架构、@SingletonProto、@ContextProto、@Inject 装饰器和动态注入。用于理解 EGG 的基础构建块、依赖注入、对象生命周期管理和运行时多实现动态选择。
|
|
4
|
+
allowed-tools: Read
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# egg 核心概念
|
|
8
|
+
|
|
9
|
+
## Step 1: 代码写在 module 中
|
|
10
|
+
|
|
11
|
+
### 什么是模块?
|
|
12
|
+
|
|
13
|
+
模块是 EGG 中基础的代码组织单元。只有模块内的代码会被框架扫描和加载。模块之间相互独立,但可以通过 `@Inject` 装饰器访问其他模块的对象。
|
|
14
|
+
|
|
15
|
+
### 定义 module
|
|
16
|
+
|
|
17
|
+
在目录中添加包含 `eggModule.name` 字段的 `package.json` 文件来声明该目录为模块:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"name": "foo",
|
|
22
|
+
"eggModule": {
|
|
23
|
+
"name": "foo"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**重要提示**:模块名称不能包含 `-` 或其他特殊字符;使用驼峰命名规则。
|
|
29
|
+
|
|
30
|
+
#### 正确示例
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
app/
|
|
34
|
+
└── userModule/ ✅ 驼峰命名
|
|
35
|
+
├── package.json
|
|
36
|
+
│ └── { "eggModule": { "name": "userModule" } }
|
|
37
|
+
└── service.ts ✅ 会被框架加载
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### 错误示例
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
app/
|
|
44
|
+
├── user-module/ ❌ 名称包含 `-`
|
|
45
|
+
│ └── package.json
|
|
46
|
+
│ └── { "eggModule": { "name": "user-module" } }
|
|
47
|
+
│
|
|
48
|
+
└── common/ ❌ 缺少 package.json(不是 module)
|
|
49
|
+
└── utils.ts ❌ 不会被框架加载
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### 模块配置
|
|
53
|
+
|
|
54
|
+
在模块根目录创建 `module.yml` 用于模块特定配置:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
foo: bar
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
通过 `@Inject()` 注入配置,使用 `moduleConfig`:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { SingletonProto, Inject } from 'egg';
|
|
64
|
+
|
|
65
|
+
interface ModuleConfig {
|
|
66
|
+
foo: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@SingletonProto()
|
|
70
|
+
export class ConfigService {
|
|
71
|
+
@Inject()
|
|
72
|
+
private readonly moduleConfig: ModuleConfig;
|
|
73
|
+
|
|
74
|
+
async hello(): Promise<string> {
|
|
75
|
+
return `hello ${this.moduleConfig.foo}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### 模块组织最佳实践
|
|
81
|
+
|
|
82
|
+
- 新应用:按功能在 `app/` 目录中组织
|
|
83
|
+
- 存量应用:保留老的 egg 代码在 `app/controller`/`app/service`,将新增的 module 代码放在 `app/module/`
|
|
84
|
+
- 可以在 `dependencies` 中导入 npm 包作为额外模块
|
|
85
|
+
|
|
86
|
+
## Step 2: 用 Proto 实现 Service
|
|
87
|
+
|
|
88
|
+
### SingletonProto
|
|
89
|
+
|
|
90
|
+
应用启动时立即创建,整个应用生命周期内只有一个实例,性能更好,应该作为默认选择。
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { SingletonProto } from 'egg';
|
|
94
|
+
|
|
95
|
+
@SingletonProto()
|
|
96
|
+
export class HelloService {
|
|
97
|
+
async hello(): Promise<string> {
|
|
98
|
+
return 'hello';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### ContextProto
|
|
104
|
+
|
|
105
|
+
请求到达时按需创建,每个请求一个实例,请求结束自动销毁。仅在需要隔离不同请求的上下文信息时使用。
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { ContextProto } from 'egg';
|
|
109
|
+
|
|
110
|
+
@ContextProto()
|
|
111
|
+
export class RequestContext {
|
|
112
|
+
userId: string;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**重要提示**:大多数服务应该使用 `SingletonProto` 以获得更好的性能。只有当请求上下文必须在服务之间共享以确保请求之间隔离时,才使用 `ContextProto`。
|
|
117
|
+
|
|
118
|
+
### AccessLevel
|
|
119
|
+
|
|
120
|
+
proto 对象默认 accessLevel 为 `AccessLevel.PRIVATE`,仅在当前 module 内使用。可以设置为 `AccessLevel.PUBLIC`,进行跨模块访问。
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { AccessLevel, ContextProto, SingletonProto } from 'egg';
|
|
124
|
+
|
|
125
|
+
@SingletonProto({ accessLevel: AccessLevel.PUBLIC })
|
|
126
|
+
export class SharedService {}
|
|
127
|
+
|
|
128
|
+
@ContextProto({ accessLevel: AccessLevel.PUBLIC })
|
|
129
|
+
export class SharedContextService {}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Step 3: 通过 Inject 使用 Service
|
|
133
|
+
|
|
134
|
+
### 基本用法
|
|
135
|
+
|
|
136
|
+
使用 `@Inject()` 注入其他 Proto 或 Egg 对象:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { Inject, Logger, SingletonProto } from 'egg';
|
|
140
|
+
import { FooService } from './FooService.ts';
|
|
141
|
+
|
|
142
|
+
@SingletonProto()
|
|
143
|
+
export class HelloService {
|
|
144
|
+
@Inject()
|
|
145
|
+
fooService: FooService; // 注入另一个 Proto
|
|
146
|
+
|
|
147
|
+
@Inject()
|
|
148
|
+
logger: Logger; // 注入 Egg 对象
|
|
149
|
+
|
|
150
|
+
async hello(): Promise<string> {
|
|
151
|
+
this.logger.info(`[HelloService] ${this.fooService.hello()}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 动态注入
|
|
157
|
+
|
|
158
|
+
当同一个抽象有多种实现,需要在运行时动态选择时,通过 `EggObjectFactory` 按类型获取实现,无需 if/else。详见 `references/dynamic-inject.md`。
|
|
159
|
+
|
|
160
|
+
### 重要约束
|
|
161
|
+
|
|
162
|
+
- **不能有循环依赖**:Proto 或模块之间都不能有循环依赖
|
|
163
|
+
- **不能有同名对象**:一个模块不能有相同名称和初始化类型的 Proto
|
|
164
|
+
- **按需注入**:不要直接注入 `app` 或 `ctx`,按需注入特定对象
|
|
165
|
+
|
|
166
|
+
## 快速决策指南
|
|
167
|
+
|
|
168
|
+
| 场景 | 使用装饰器 |
|
|
169
|
+
| -------------------------------- | ------------------------------------------------------ |
|
|
170
|
+
| 无状态服务 | `@SingletonProto()` |
|
|
171
|
+
| 跨服务共享的请求级状态 | `@ContextProto()` |
|
|
172
|
+
| 需要跨模块访问 | `@SingletonProto({ accessLevel: AccessLevel.PUBLIC })` |
|
|
173
|
+
| 注入依赖 | `@Inject()` |
|
|
174
|
+
| 使用自定义名称注入 | `@Inject({ name: 'customName' })` |
|
|
175
|
+
| 同一抽象多种实现,运行时动态选择 | `QualifierImplDecoratorUtil` + `EggObjectFactory` |
|
|
176
|
+
|
|
177
|
+
## 常见问题排查
|
|
178
|
+
|
|
179
|
+
| 现象 | 原因 | 解决方案 |
|
|
180
|
+
| ---------------------- | ------------------------------------------ | -------------------------------------------- |
|
|
181
|
+
| 模块没被加载 | `eggModule.name` 包含 `-` 等特殊字符 | 改为驼峰命名 |
|
|
182
|
+
| `EggPrototypeNotFound` | 跨模块注入但 accessLevel 为 PRIVATE | 改为 `AccessLevel.PUBLIC` |
|
|
183
|
+
| 注入对象不对 | 类型为 `interface`/`any`,回退到属性名匹配 | 改用 class 类型或 `@Inject({ name: 'xxx' })` |
|
|
184
|
+
| 混用注入方式报错 | 属性注入和构造函数注入不能混用 | 统一使用一种方式 |
|
|
185
|
+
| 可选依赖启动报错 | 缺少 optional 标记 | `@Inject({ optional: true })` |
|
|
186
|
+
|
|
187
|
+
## 参考资料
|
|
188
|
+
|
|
189
|
+
- 详细的 module 文档,请参阅:`references/module.md`
|
|
190
|
+
- Inject 装饰器使用,请参阅:`references/inject.md`
|
|
191
|
+
- SingletonProto 和 ContextProto 详情,请参阅:`references/proto.md`
|
|
192
|
+
- 动态注入(Qualifier 动态注入),请参阅:`references/dynamic-inject.md`
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# 动态注入开发指南
|
|
2
|
+
|
|
3
|
+
## 何时使用
|
|
4
|
+
|
|
5
|
+
- **用动态注入**:同一抽象有多种实现,运行时按参数选择(如多种支付方式、多种存储后端)
|
|
6
|
+
- **用普通 `@Inject()`**:依赖只有一个实现,编译时就能确定
|
|
7
|
+
|
|
8
|
+
## Step 1: 定义抽象类和类型枚举
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// AbstractHello.ts
|
|
12
|
+
export abstract class AbstractHello {
|
|
13
|
+
abstract hello(): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// HelloType.ts
|
|
17
|
+
export enum HelloType {
|
|
18
|
+
FOO = 'FOO',
|
|
19
|
+
BAR = 'BAR',
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
如果类型是无限扩展的,没有固定枚举,可以用 `Record<string, string>` 代替:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
type AnyEnum = Record<string, string>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Step 2: 创建自定义装饰器
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// decorator/Hello.ts
|
|
33
|
+
import { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';
|
|
34
|
+
import { HelloType } from '../HelloType.ts';
|
|
35
|
+
import { AbstractHello } from '../AbstractHello.ts';
|
|
36
|
+
|
|
37
|
+
export const HELLO_ATTRIBUTE = Symbol('HELLO_ATTRIBUTE');
|
|
38
|
+
|
|
39
|
+
export const Hello: ImplDecorator<AbstractHello, typeof HelloType> =
|
|
40
|
+
QualifierImplDecoratorUtil.generatorDecorator(AbstractHello, HELLO_ATTRIBUTE);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**注意事项:**
|
|
44
|
+
|
|
45
|
+
- **ATTRIBUTE(Symbol)不要重复**,重复会导致实现被覆盖
|
|
46
|
+
- **抽象类不要指定错**,否则可能导致实现被覆盖
|
|
47
|
+
|
|
48
|
+
## Step 3: 实现抽象类
|
|
49
|
+
|
|
50
|
+
每个实现加上 `@SingletonProto()`(或 `@ContextProto()`)和自定义装饰器:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// impl/FooHello.ts
|
|
54
|
+
import { SingletonProto } from 'egg';
|
|
55
|
+
import { Hello } from '../decorator/Hello.ts';
|
|
56
|
+
import { HelloType } from '../HelloType.ts';
|
|
57
|
+
import { AbstractHello } from '../AbstractHello.ts';
|
|
58
|
+
|
|
59
|
+
@SingletonProto()
|
|
60
|
+
@Hello(HelloType.FOO)
|
|
61
|
+
export class FooHello extends AbstractHello {
|
|
62
|
+
hello(): string {
|
|
63
|
+
return 'hello, foo';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Step 4: 动态获取实现
|
|
69
|
+
|
|
70
|
+
通过 `EggObjectFactory` 在运行时按类型获取对应实现:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// HelloService.ts
|
|
74
|
+
import { EggObjectFactory, SingletonProto, Inject } from 'egg';
|
|
75
|
+
import { HelloType } from './HelloType.ts';
|
|
76
|
+
import { AbstractHello } from './AbstractHello.ts';
|
|
77
|
+
|
|
78
|
+
@SingletonProto()
|
|
79
|
+
export class HelloService {
|
|
80
|
+
@Inject()
|
|
81
|
+
private eggObjectFactory: EggObjectFactory;
|
|
82
|
+
|
|
83
|
+
async hello(type: HelloType): Promise<string> {
|
|
84
|
+
const helloImpl = await this.eggObjectFactory.getEggObject(
|
|
85
|
+
AbstractHello,
|
|
86
|
+
type,
|
|
87
|
+
);
|
|
88
|
+
return helloImpl.hello();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|