@eggjs/skills 0.0.0 → 4.1.2-beta.4

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.
@@ -0,0 +1,258 @@
1
+ # Inject 开发指南
2
+
3
+ ## 基本用法
4
+
5
+ 使用 `@Inject()` 注入其他 Proto 对象或 Egg 内置对象:
6
+
7
+ ```typescript
8
+ import { SingletonProto, Inject } from 'egg';
9
+
10
+ @SingletonProto()
11
+ export class OrderService {
12
+ @Inject()
13
+ userService: UserService; // 按类型自动匹配
14
+
15
+ @Inject({ name: 'customName' })
16
+ config: any; // 按名称匹配
17
+
18
+ @Inject('shorthand')
19
+ other: any; // 字符串简写,等同于 { name: 'shorthand' }
20
+ }
21
+ ```
22
+
23
+ ## 名称解析规则
24
+
25
+ 框架按以下优先级确定注入目标:
26
+
27
+ 1. **显式指定 name** — `@Inject({ name: 'xxx' })` 或 `@Inject('xxx')`
28
+ 2. **类型元数据自动关联** — 当属性类型是使用了 `@SingletonProto()` 或 `@ContextProto()` 装饰器的 class 时,框架自动从类型元数据匹配对应的 Proto 对象。此时属性名可以任意取值,不影响注入结果
29
+ 3. **属性名兜底** — 当类型为 `interface`、`any`、`unknown` 等无法获取运行时元信息的类型时,使用属性名作为注入名称
30
+
31
+ ```typescript
32
+ @SingletonProto()
33
+ export class MyService {
34
+ @Inject()
35
+ fooService: FooService; // 优先级 2:从 FooService 类型自动关联
36
+
37
+ @Inject()
38
+ whatever: FooService; // 优先级 2:同样生效,属性名不影响匹配
39
+
40
+ @Inject({ name: 'bar' })
41
+ baz: FooService; // 优先级 1:显式 name → "bar"
42
+
43
+ @Inject()
44
+ something: any; // 优先级 3:类型无元数据,回退到属性名 → "something"
45
+ }
46
+ ```
47
+
48
+ ## 可选注入
49
+
50
+ 默认情况下,注入目标不存在会在启动时报错。使用 `optional` 可以跳过不存在的依赖:
51
+
52
+ ```typescript
53
+ import { SingletonProto, Inject, InjectOptional } from 'egg';
54
+
55
+ @SingletonProto()
56
+ export class MyService {
57
+ @Inject({ optional: true })
58
+ maybeService?: SomeService; // 不存在时为 undefined
59
+
60
+ @InjectOptional()
61
+ anotherOptional?: OtherService; // 简写方式,效果相同
62
+ }
63
+ ```
64
+
65
+ ## 构造函数注入
66
+
67
+ 除了属性注入,也支持构造函数参数注入:
68
+
69
+ ```typescript
70
+ import { SingletonProto, Inject } from 'egg';
71
+
72
+ @SingletonProto()
73
+ export class MyService {
74
+ constructor(
75
+ @Inject() readonly fooService: FooService,
76
+ @Inject({ optional: true }) readonly barService?: BarService,
77
+ ) {}
78
+ }
79
+ ```
80
+
81
+ **注意:构造函数注入和属性注入不能混用。** 一个 class 只能选择其中一种方式,否则框架会报错。
82
+
83
+ ## 注入 Egg 内置对象
84
+
85
+ 框架会自动遍历 `Application` 和 `Context` 对象的所有属性,均可通过 `@Inject()` 注入。
86
+
87
+ #### 注入配置
88
+
89
+ ```typescript
90
+ import { Inject, SingletonProto, EggAppConfig } from 'egg';
91
+
92
+ @SingletonProto()
93
+ class Foo {
94
+ @Inject()
95
+ config: EggAppConfig;
96
+
97
+ bar(): void {
98
+ console.log('current env is %s', this.config.env);
99
+ }
100
+ }
101
+ ```
102
+
103
+ #### 注入 Logger
104
+
105
+ 专为 logger 做了优化,可以直接注入 custom logger:
106
+
107
+ ```typescript
108
+ import { Inject, SingletonProto, Logger } from 'egg';
109
+
110
+ @SingletonProto()
111
+ class FooService {
112
+ @Inject()
113
+ logger: Logger; // 注入 ${appname}-web.log
114
+
115
+ @Inject()
116
+ coreLogger: Logger; // 注入 egg-web.log
117
+
118
+ @Inject()
119
+ fooLogger: Logger; // 注入 customLogger 中配置的 fooLogger
120
+ }
121
+ ```
122
+
123
+ #### 注入 Service
124
+
125
+ > 强烈建议把 Egg Service 的代码通过 Proto 重新封装再注入。对于已有的 Service,可以通过以下方式引入:
126
+
127
+ ```typescript
128
+ import { Service, Inject, SingletonProto } from 'egg';
129
+
130
+ @SingletonProto()
131
+ class FooService {
132
+ @Inject()
133
+ service: Service; // 注入整个 ctx.service
134
+
135
+ get xxxService() {
136
+ return this.service.xxxService;
137
+ }
138
+ }
139
+ ```
140
+
141
+ #### 注入 HttpClient
142
+
143
+ ```typescript
144
+ import { Inject, SingletonProto, HttpClient } from 'egg';
145
+
146
+ @SingletonProto()
147
+ class Foo {
148
+ @Inject()
149
+ httpClient: HttpClient;
150
+
151
+ async bar(): Promise<void> {
152
+ await this.httpClient.request('https://example.com');
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## 注入模块配置
158
+
159
+ 在模块根目录创建 `module.yml`,通过 `moduleConfig` 名称注入,框架自动注入当前模块的配置:
160
+
161
+ ```yaml
162
+ # module.yml
163
+ apiEndpoint: https://api.example.com
164
+ retryCount: 3
165
+ ```
166
+
167
+ ```typescript
168
+ import { SingletonProto, Inject } from 'egg';
169
+
170
+ interface ModuleConfig {
171
+ apiEndpoint: string;
172
+ retryCount: number;
173
+ }
174
+
175
+ @SingletonProto()
176
+ export class ApiService {
177
+ @Inject()
178
+ moduleConfig: ModuleConfig;
179
+
180
+ async call(): Promise<void> {
181
+ // this.moduleConfig.apiEndpoint → "https://api.example.com"
182
+ }
183
+ }
184
+ ```
185
+
186
+ 如需注入其他模块的配置,使用 `@ConfigSourceQualifier` 指定模块名:
187
+
188
+ ```typescript
189
+ import { SingletonProto, Inject, ConfigSourceQualifier } from 'egg';
190
+
191
+ @SingletonProto()
192
+ export class MyService {
193
+ @Inject()
194
+ @ConfigSourceQualifier('otherModule')
195
+ moduleConfig: OtherModuleConfig; // 注入 otherModule 的配置
196
+ }
197
+ ```
198
+
199
+ ## Qualifier 限定符
200
+
201
+ 当同名对象存在多个实现时,使用限定符消歧义:
202
+
203
+ ### @InitTypeQualifier
204
+
205
+ 指定注入 Singleton 还是 Context 实例:
206
+
207
+ ```typescript
208
+ import { SingletonProto, Inject, InitTypeQualifier, ObjectInitType } from 'egg';
209
+
210
+ @SingletonProto()
211
+ export class MyService {
212
+ @Inject()
213
+ @InitTypeQualifier(ObjectInitType.CONTEXT)
214
+ barService: BarService; // 强制注入 ContextProto 版本
215
+ }
216
+ ```
217
+
218
+ > 大多数情况下不需要手动指定,框架会根据类型元数据自动推导。
219
+
220
+ ### @EggQualifier
221
+
222
+ 当 `app` 和 `ctx` 上存在同名属性时,框架默认优先匹配 `ctx`。使用 `@EggQualifier` 显式指定来源:
223
+
224
+ ```typescript
225
+ import { SingletonProto, Inject, EggQualifier, EggType } from 'egg';
226
+
227
+ @SingletonProto()
228
+ export class MyService {
229
+ @Inject()
230
+ @EggQualifier(EggType.APP)
231
+ someProp: any; // 强制从 app 注入
232
+
233
+ @Inject()
234
+ @EggQualifier(EggType.CONTEXT)
235
+ someProp2: any; // 强制从 ctx 注入
236
+ }
237
+ ```
238
+
239
+ ### @ModuleQualifier
240
+
241
+ 指定从哪个模块注入:
242
+
243
+ ```typescript
244
+ import { SingletonProto, Inject, ModuleQualifier } from 'egg';
245
+
246
+ @SingletonProto()
247
+ export class MyService {
248
+ @Inject()
249
+ @ModuleQualifier('userModule')
250
+ userService: UserService; // 明确从 userModule 注入
251
+ }
252
+ ```
253
+
254
+ ## 重要约束
255
+
256
+ - **不能有循环依赖**:Proto 之间或模块之间都不能形成循环引用
257
+ - **不能有同名对象**:同一模块内不能存在相同名称和相同初始化类型的 Proto
258
+ - **按需注入**:不要直接注入 `app` 或 `ctx`,按需注入具体的属性(如 `logger`、`config`)
@@ -0,0 +1,202 @@
1
+ # module 开发指南
2
+
3
+ ## 创建 module
4
+
5
+ 在 `app` 目录中,创建 module 目录,并在该目录中添加 `package.json`。
6
+
7
+ ```json
8
+ {
9
+ "name": "moduleName",
10
+ "eggModule": {
11
+ "name": "moduleName"
12
+ }
13
+ }
14
+ ```
15
+
16
+ **重要提示**:
17
+
18
+ - 模块名称不能包含 `-` 或其他特殊字符;使用驼峰命名规则。
19
+ - module 的 `package.json` 文件中,仅包含 `name` 以及 `eggModule.name` 字段,不应该有其他额外内容。
20
+
21
+ ## module 发现机制
22
+
23
+ 框架支持两种模式:自动扫描(默认)和手动声明。两者互斥,当 `config/module.json` 存在时,自动扫描完全禁用。
24
+
25
+ ### 模式一:自动扫描(默认)
26
+
27
+ 当 `config/module.json` **不存在**时,框架通过以下两种方式发现模块:
28
+
29
+ **1. 目录扫描**
30
+
31
+ 从项目根目录开始扫描,查找含有 `eggModule.name` 的 `package.json`。
32
+
33
+ - 默认扫描深度:10 层
34
+ - 自动排除:隐藏目录(`.` 开头)、`node_modules/`、`coverage/`
35
+
36
+ 可通过应用配置调整扫描深度和排除路径:
37
+
38
+ ```typescript
39
+ // config/config.default.ts
40
+ export default {
41
+ tegg: {
42
+ readModuleOptions: {
43
+ deep: 5, // 默认 10
44
+ extraFilePattern: ['!**/dist'], // 额外排除 dist 目录
45
+ },
46
+ },
47
+ };
48
+ ```
49
+
50
+ `extraFilePattern` 使用 [globby](https://github.com/sindresorhus/globby) 语法,以 `!` 开头表示排除。
51
+
52
+ ```
53
+ app/
54
+ ├── fooModule/ ✅ 被扫描到
55
+ │ └── package.json { "eggModule": { "name": "fooModule" } }
56
+ ├── barModule/ ✅ 被扫描到
57
+ │ └── package.json { "eggModule": { "name": "barModule" } }
58
+ ├── .hidden/ ❌ 隐藏目录,自动排除
59
+ │ └── package.json
60
+ └── common/ ❌ 无 package.json,不会加载
61
+ └── utils.ts
62
+ ```
63
+
64
+ **2. npm 包扫描**
65
+
66
+ 框架会遍历项目 `package.json` 中 `dependencies` 的每个包(不含 `devDependencies`),检查其 `package.json` 是否含有 `eggModule.name`,如果有则自动作为模块加载。
67
+
68
+ ### 模式二:手动声明
69
+
70
+ 创建 `config/module.json` 后,自动扫描**完全禁用**,仅加载文件中声明的模块:
71
+
72
+ ```json
73
+ [
74
+ { "path": "../app/module-a" }, // 相对于 config 目录的路径
75
+ { "package": "@eggjs/common-module" } // npm 包名
76
+ ]
77
+ ```
78
+
79
+ ## module 配置
80
+
81
+ 在模块根目录创建 `module.yml` 存放模块专属配置:
82
+
83
+ ```
84
+ app/
85
+ └── userModule/
86
+ ├── package.json
87
+ ├── module.yml # 基础配置
88
+ ├── module.unittest.yml # 环境特定配置(可选)
89
+ └── UserService.ts
90
+ ```
91
+
92
+ ### 配置文件格式
93
+
94
+ 支持 YAML 和 JSON 两种格式,优先加载 YAML:
95
+
96
+ ```yaml
97
+ # module.yml
98
+ features:
99
+ dynamic:
100
+ foo: bar
101
+ ```
102
+
103
+ ### 环境配置合并
104
+
105
+ 框架会按 `module.yml` → `module.{env}.yml` 的顺序深度合并:
106
+
107
+ ```yaml
108
+ # module.yml
109
+ features:
110
+ dynamic:
111
+ foo: bar
112
+
113
+ # module.unittest.yml
114
+ features:
115
+ dynamic:
116
+ testMode: true
117
+ ```
118
+
119
+ unittest 环境下合并结果:
120
+
121
+ ```json
122
+ {
123
+ "features": {
124
+ "dynamic": {
125
+ "foo": "bar",
126
+ "testMode": true
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### 注入配置
133
+
134
+ 通过 `@Inject()` 注入 `moduleConfig`,框架自动注入当前模块的配置:
135
+
136
+ ```typescript
137
+ import { SingletonProto, Inject } from 'egg';
138
+
139
+ interface ModuleConfig {
140
+ features: {
141
+ dynamic: {
142
+ foo: string;
143
+ };
144
+ };
145
+ }
146
+
147
+ @SingletonProto()
148
+ export class UserService {
149
+ @Inject()
150
+ moduleConfig: ModuleConfig;
151
+
152
+ async getFeatureFlag(): Promise<string> {
153
+ return this.moduleConfig.features.dynamic.foo; // 'bar'
154
+ }
155
+ }
156
+ ```
157
+
158
+ ## module 目录组织
159
+
160
+ ### 新应用
161
+
162
+ 直接在 `app/` 目录下平铺 module。可按功能划分,也可按业务划分:
163
+
164
+ **按功能划分:**
165
+
166
+ ```
167
+ app/
168
+ ├── authModule/
169
+ │ └── package.json
170
+ ├── logModule/
171
+ │ └── package.json
172
+ └── storageModule/
173
+ └── package.json
174
+ ```
175
+
176
+ **按业务划分:**
177
+
178
+ ```
179
+ app/
180
+ ├── userModule/
181
+ │ └── package.json
182
+ ├── orderModule/
183
+ │ └── package.json
184
+ └── paymentModule/
185
+ └── package.json
186
+ ```
187
+
188
+ ### 存量应用(包含老的 egg 写法)
189
+
190
+ 保留原有 `app/controller`、`app/service` 等目录不动,将新增的 module 统一放在 `app/module/` 下:
191
+
192
+ ```
193
+ app/
194
+ ├── controller/ # 老的 egg 代码,保持不变
195
+ ├── service/
196
+ ├── module/ # 新增 module 统一放这里
197
+ │ ├── userModule/
198
+ │ │ └── package.json
199
+ │ └── orderModule/
200
+ │ └── package.json
201
+ └── router.ts
202
+ ```
@@ -0,0 +1,181 @@
1
+ # Proto 开发指南
2
+
3
+ ## SingletonProto vs ContextProto
4
+
5
+ | | SingletonProto | ContextProto |
6
+ | -------- | ------------------ | ------------------ |
7
+ | 创建时机 | 应用启动时立即创建 | 请求到达时按需创建 |
8
+ | 实例数量 | 整个应用 1 个 | 每个请求 1 个 |
9
+ | 销毁时机 | 应用关闭 | 请求结束 |
10
+
11
+ **决策逻辑:默认使用 `@SingletonProto()`**,性能更好。只有当需要在多个服务间共享请求级隔离状态时,才使用 `@ContextProto()`。
12
+
13
+ ```typescript
14
+ import { SingletonProto, ContextProto } from 'egg';
15
+
16
+ // ✅ 默认选择:无状态服务用 SingletonProto
17
+ @SingletonProto()
18
+ export class UserService {
19
+ async findUser(id: string): Promise<User> {
20
+ // 无需请求级状态,适合 Singleton
21
+ }
22
+ }
23
+
24
+ // 仅在需要请求级隔离时使用 ContextProto
25
+ @ContextProto()
26
+ export class RequestContext {
27
+ traceId: string;
28
+ userId: string;
29
+ // 每个请求独立的状态
30
+ }
31
+ ```
32
+
33
+ ## 装饰器参数
34
+
35
+ `@SingletonProto()` 和 `@ContextProto()` 接受相同的可选参数:
36
+
37
+ | 参数 | 类型 | 默认值 | 说明 |
38
+ | ------------- | ----------- | -------------- | ------------------------------------ |
39
+ | name | string | 类名首字母小写 | 实例名称 |
40
+ | accessLevel | AccessLevel | PRIVATE | 跨模块可见性 |
41
+ | protoImplType | string | 'DEFAULT' | 实现类型标记(高级用法,一般不需要) |
42
+
43
+ 名称推导规则:类名首字母自动转小写,如 `MyService` → `myService`。
44
+
45
+ ```typescript
46
+ import { SingletonProto, AccessLevel } from 'egg';
47
+
48
+ // 使用默认参数:name 为 "helloService",accessLevel 为 PRIVATE
49
+ @SingletonProto()
50
+ export class HelloService {}
51
+
52
+ // 自定义参数
53
+ @SingletonProto({
54
+ name: 'customName',
55
+ accessLevel: AccessLevel.PUBLIC,
56
+ })
57
+ export class HelloService {}
58
+ ```
59
+
60
+ ## 注入规则
61
+
62
+ **SingletonProto 可以注入 ContextProto**,框架会自动处理生命周期差异。
63
+
64
+ 实现机制:框架不会直接注入 ContextProto 实例,而是通过 lazy getter/Proxy,在每次访问时从当前请求上下文动态解析,确保每个请求拿到各自的实例。
65
+
66
+ ```typescript
67
+ import { SingletonProto, ContextProto, Inject } from 'egg';
68
+
69
+ @ContextProto()
70
+ export class RequestContext {
71
+ userId: string;
72
+ }
73
+
74
+ @SingletonProto()
75
+ export class AppService {
76
+ @Inject()
77
+ requestContext: RequestContext; // ✅ 每次访问自动解析当前请求的实例
78
+ }
79
+ ```
80
+
81
+ ContextProto 也可以注入 SingletonProto:
82
+
83
+ ```typescript
84
+ @ContextProto()
85
+ export class RequestHandler {
86
+ @Inject()
87
+ appService: AppService; // ✅ 直接注入
88
+ }
89
+ ```
90
+
91
+ ## AccessLevel 跨模块访问
92
+
93
+ - `AccessLevel.PRIVATE`(默认):仅同模块内可注入
94
+ - `AccessLevel.PUBLIC`:跨模块可注入
95
+
96
+ 模块边界由 `package.json` 中的 `eggModule.name` 决定。
97
+
98
+ ```typescript
99
+ // userModule 中
100
+ @SingletonProto({ accessLevel: AccessLevel.PUBLIC })
101
+ export class UserService {}
102
+
103
+ // orderModule 中
104
+ @SingletonProto()
105
+ export class OrderService {
106
+ @Inject()
107
+ userService: UserService; // ✅ UserService 是 PUBLIC,跨模块可注入
108
+ }
109
+ ```
110
+
111
+ ```typescript
112
+ // userModule 中
113
+ @SingletonProto() // 默认 PRIVATE
114
+ export class UserHelper {}
115
+
116
+ // orderModule 中
117
+ @SingletonProto()
118
+ export class OrderService {
119
+ @Inject()
120
+ userHelper: UserHelper; // ❌ 报错:UserHelper 是 PRIVATE,不能跨模块注入
121
+ }
122
+ ```
123
+
124
+ ## 生命周期
125
+
126
+ Proto 对象支持以下生命周期钩子,按执行顺序排列:
127
+
128
+ | 阶段 | 装饰器 | 说明 |
129
+ | ---- | --------------------------- | -------------------------------- |
130
+ | 1 | constructor | 构造函数 |
131
+ | 2 | `@LifecyclePostConstruct()` | 构造完成后 |
132
+ | 3 | `@LifecyclePreInject()` | 注入前 |
133
+ | 4 | — | 依赖注入 |
134
+ | 5 | `@LifecyclePostInject()` | 注入完成后 |
135
+ | 6 | `@LifecycleInit()` | 异步初始化(对象就绪前最后一步) |
136
+ | 7 | — | **对象就绪,可被使用** |
137
+ | 8 | `@LifecyclePreDestroy()` | 销毁前 |
138
+ | 9 | `@LifecycleDestroy()` | 销毁,用于清理资源 |
139
+
140
+ 常用的是 `@LifecycleInit()` 和 `@LifecycleDestroy()`。
141
+
142
+ **示例 1:SingletonProto 异步初始化和资源清理**
143
+
144
+ ```typescript
145
+ import { SingletonProto, LifecycleInit, LifecycleDestroy } from 'egg';
146
+
147
+ @SingletonProto()
148
+ export class DatabaseService {
149
+ private connection: Connection;
150
+
151
+ @LifecycleInit()
152
+ async init(): Promise<void> {
153
+ this.connection = await createConnection();
154
+ }
155
+
156
+ @LifecycleDestroy()
157
+ async destroy(): Promise<void> {
158
+ await this.connection.close();
159
+ }
160
+ }
161
+ ```
162
+
163
+ **示例 2:ContextProto 在注入完成后初始化状态**
164
+
165
+ ```typescript
166
+ import { ContextProto, Inject, LifecyclePostInject } from 'egg';
167
+
168
+ @ContextProto()
169
+ export class RequestTracer {
170
+ @Inject()
171
+ traceService: TraceService;
172
+
173
+ traceId: string;
174
+
175
+ @LifecyclePostInject()
176
+ async postInject(): Promise<void> {
177
+ // 依赖注入完成后,利用注入的服务初始化状态
178
+ this.traceId = await this.traceService.generateTraceId();
179
+ }
180
+ }
181
+ ```
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@eggjs/skills",
3
- "version": "0.0.0",
3
+ "version": "4.1.2-beta.4",
4
4
  "description": "agent skills for egg",
5
+ "keywords": [
6
+ "egg",
7
+ "skill"
8
+ ],
9
+ "homepage": "https://github.com/eggjs/egg/tree/next/packages/skills",
10
+ "bugs": {
11
+ "url": "https://github.com/eggjs/egg/issues"
12
+ },
5
13
  "license": "MIT",
6
14
  "author": "eggjs",
7
15
  "repository": {
@@ -9,7 +17,13 @@
9
17
  "url": "git+https://github.com/eggjs/egg.git",
10
18
  "directory": "packages/skills"
11
19
  },
20
+ "files": [
21
+ "**/*.md"
22
+ ],
12
23
  "publishConfig": {
13
24
  "access": "public"
25
+ },
26
+ "engines": {
27
+ "node": ">=22.18.0"
14
28
  }
15
29
  }