katagami 1.1.0 → 2.0.0

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/README.zh-CN.md DELETED
@@ -1,438 +0,0 @@
1
- [English](./README.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-TW.md) | [简体中文](./README.zh-CN.md) | [Español](./README.es.md) | [Deutsch](./README.de.md) | [Français](./README.fr.md)
2
-
3
- # Katagami
4
-
5
- 轻量级 TypeScript DI 容器,支持完整的类型推断。
6
-
7
- [![npm version](https://img.shields.io/npm/v/katagami)](https://www.npmjs.com/package/katagami)
8
- [![license](https://img.shields.io/npm/l/katagami)](https://github.com/hiroiku/katagami/blob/master/LICENSE)
9
- [![bundle size](https://img.shields.io/bundlephobia/minzip/katagami)](https://bundlephobia.com/package/katagami)
10
-
11
- > 名称源自日语「型紙」_(katagami)_——一种用于传统日本染色工艺的精密型版纸,将精确的图案转印到织物上。多张型版叠加组合出复杂的纹样,正如类型随着每次方法链调用而逐步积累。型版只需纸和刷子,无需精密的机械装置——同样地,Katagami 不依赖装饰器或元数据机制,开箱即用于任何构建工具。而就像型版能适应不同的织物与技法,Katagami 也能跨越 TypeScript 与 JavaScript、类令牌与 PropertyKey 令牌——以混合方式实现严格、可组合的 DI。
12
-
13
- ## 特性
14
-
15
- | 特性 | 说明 |
16
- | --------------- | ----------------------------------------------------------------------------- |
17
- | 完整的类型推断 | 类型随方法链积累;解析未注册的令牌会产生编译时错误 |
18
- | 三种生命周期 | Singleton、Transient 和 Scoped(支持子容器) |
19
- | 异步工厂 | 返回 Promise 的工厂会被类型系统自动追踪 |
20
- | 循环依赖检测 | 包含完整循环路径的清晰错误消息 |
21
- | Disposable 支持 | TC39 显式资源管理(`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
22
- | 捕获依赖防护 | Singleton/Transient 工厂无法访问 Scoped 令牌;在编译时捕获 |
23
- | 可选解析 | `tryResolve` 对未注册令牌返回 `undefined` 而非抛出异常 |
24
- | 混合令牌策略 | 类令牌提供严格的类型安全,PropertyKey 令牌提供灵活性 |
25
- | 接口类型映射 | 向 `createContainer<T>()` 传入接口,实现与注册顺序无关的注册 |
26
- | 零依赖 | 无需装饰器、无需 reflect-metadata、无需 polyfill |
27
-
28
- ## 安装
29
-
30
- ```bash
31
- npm install katagami
32
- ```
33
-
34
- ## 快速开始
35
-
36
- ```ts
37
- import { createContainer } from 'katagami';
38
-
39
- class Logger {
40
- log(msg: string) {
41
- console.log(msg);
42
- }
43
- }
44
-
45
- class UserService {
46
- constructor(private logger: Logger) {}
47
- greet(name: string) {
48
- this.logger.log(`Hello, ${name}`);
49
- }
50
- }
51
-
52
- const container = createContainer()
53
- .registerSingleton(Logger, () => new Logger())
54
- .registerSingleton(UserService, r => new UserService(r.resolve(Logger)));
55
-
56
- const userService = container.resolve(UserService);
57
- // ^? UserService(完全推断)
58
- userService.greet('world');
59
- ```
60
-
61
- ## 为什么选择 Katagami
62
-
63
- 大多数 TypeScript DI 容器依赖于装饰器、reflect-metadata 或基于字符串的令牌——每种方式都在工具兼容性、类型安全或包体积方面带来取舍。Katagami 采用了不同的方式。
64
-
65
- ### 无需装饰器,无需 reflect-metadata
66
-
67
- 基于装饰器的 DI 需要 `experimentalDecorators` 和 `emitDecoratorMetadata` 编译器选项。esbuild 和 Vite(默认配置)等现代构建工具不支持 `emitDecoratorMetadata`,且 TC39 标准装饰器提案也不包含自动类型元数据发射的等效功能。Katagami 不依赖这些——它可以开箱即用于任何构建工具。
68
-
69
- ### 基于类令牌的完整类型推断
70
-
71
- 基于字符串令牌的 DI 迫使你维护手动的令牌到类型映射。基于参数名的匹配在代码压缩后会失效。Katagami 直接使用类作为令牌,因此 `resolve` 会自动推断正确的返回类型——同步或 `Promise`——无需额外注解。
72
-
73
- ### 方法链类型积累
74
-
75
- 每次 `register` 调用都会积累类型。在工厂内部,解析器只接受链中该位置之前已注册的令牌。解析未注册的令牌会产生编译时错误,而非运行时意外。
76
-
77
- ### 混合令牌策略
78
-
79
- 类令牌通过方法链提供严格的、顺序依赖的类型安全。但有时你希望预先定义一组服务并以任意顺序注册。向 `createContainer<T>()` 传入接口并使用 PropertyKey 令牌——类型映射在创建时即已固定,注册顺序不再重要。
80
-
81
- ### 零依赖
82
-
83
- 无运行时依赖,无 polyfill。无需将 reflect-metadata(未压缩约 50 KB)添加到包中。
84
-
85
- ## 指南
86
-
87
- ### Singleton 与 Transient
88
-
89
- Singleton 在首次 `resolve` 时创建实例并缓存。Transient 每次都创建新实例。
90
-
91
- ```ts
92
- import { createContainer } from 'katagami';
93
-
94
- class Database {
95
- constructor(public id = Math.random()) {}
96
- }
97
-
98
- class RequestHandler {
99
- constructor(public id = Math.random()) {}
100
- }
101
-
102
- const container = createContainer()
103
- .registerSingleton(Database, () => new Database())
104
- .registerTransient(RequestHandler, () => new RequestHandler());
105
-
106
- // Singleton — 每次都是同一个实例
107
- container.resolve(Database) === container.resolve(Database); // true
108
-
109
- // Transient — 每次都是新实例
110
- container.resolve(RequestHandler) === container.resolve(RequestHandler); // false
111
- ```
112
-
113
- ### Scoped 生命周期与子容器
114
-
115
- Scoped 注册在作用域内表现得像 Singleton,但在每个新作用域中会产生新的实例。使用 `createScope()` 创建子容器。Scoped 令牌无法从根容器解析。
116
-
117
- ```ts
118
- import { createContainer } from 'katagami';
119
-
120
- class DbPool {
121
- constructor(public name = 'main') {}
122
- }
123
-
124
- class RequestContext {
125
- constructor(public id = Math.random()) {}
126
- }
127
-
128
- const root = createContainer()
129
- .registerSingleton(DbPool, () => new DbPool())
130
- .registerScoped(RequestContext, () => new RequestContext());
131
-
132
- // 为每个请求创建作用域
133
- const scope1 = root.createScope();
134
- const scope2 = root.createScope();
135
-
136
- // Scoped — 同一作用域内相同,不同作用域间不同
137
- scope1.resolve(RequestContext) === scope1.resolve(RequestContext); // true
138
- scope1.resolve(RequestContext) === scope2.resolve(RequestContext); // false
139
-
140
- // Singleton — 在所有作用域间共享
141
- scope1.resolve(DbPool) === scope2.resolve(DbPool); // true
142
- ```
143
-
144
- 作用域也可以嵌套。每个嵌套的作用域拥有自己的 Scoped 实例缓存,同时与父级共享 Singleton:
145
-
146
- ```ts
147
- const parentScope = root.createScope();
148
- const childScope = parentScope.createScope();
149
-
150
- // 每个嵌套作用域获得独立的 Scoped 实例
151
- parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // false
152
-
153
- // Singleton 仍然共享
154
- parentScope.resolve(DbPool) === childScope.resolve(DbPool); // true
155
- ```
156
-
157
- ### 异步工厂
158
-
159
- 返回 `Promise` 的工厂会被类型系统自动追踪。当你 `resolve` 异步令牌时,返回类型是 `Promise<V>` 而非 `V`:
160
-
161
- ```ts
162
- import { createContainer } from 'katagami';
163
-
164
- class Database {
165
- constructor(public connected: boolean) {}
166
- }
167
-
168
- class Logger {
169
- log(msg: string) {
170
- console.log(msg);
171
- }
172
- }
173
-
174
- const container = createContainer()
175
- .registerSingleton(Logger, () => new Logger())
176
- .registerSingleton(Database, async () => {
177
- await new Promise(r => setTimeout(r, 100)); // 模拟异步初始化
178
- return new Database(true);
179
- });
180
-
181
- const logger = container.resolve(Logger);
182
- // ^? Logger
183
-
184
- const db = await container.resolve(Database);
185
- // ^? Promise<Database>(await 后 → Database)
186
- db.connected; // true
187
- ```
188
-
189
- 异步工厂可以依赖同步和异步的注册:
190
-
191
- ```ts
192
- const container = createContainer()
193
- .registerSingleton(Logger, () => new Logger())
194
- .registerSingleton(Database, async r => {
195
- const logger = r.resolve(Logger); // 同步 → Logger
196
- logger.log('正在连接...');
197
- return new Database(true);
198
- });
199
- ```
200
-
201
- ### 循环依赖检测
202
-
203
- Katagami 会追踪当前正在解析的令牌。如果发现循环依赖,将抛出包含完整循环路径的 `ContainerError`:
204
-
205
- ```ts
206
- import { createContainer } from 'katagami';
207
-
208
- class ServiceA {
209
- constructor(public b: ServiceB) {}
210
- }
211
-
212
- class ServiceB {
213
- constructor(public a: ServiceA) {}
214
- }
215
-
216
- const container = createContainer()
217
- .registerSingleton(ServiceA, r => new ServiceA(r.resolve(ServiceB)))
218
- .registerSingleton(ServiceB, r => new ServiceB(r.resolve(ServiceA)));
219
-
220
- container.resolve(ServiceA);
221
- // ContainerError: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
222
- ```
223
-
224
- 间接循环也能被检测到:
225
-
226
- ```
227
- ContainerError: Circular dependency detected: ServiceX -> ServiceY -> ServiceZ -> ServiceX
228
- ```
229
-
230
- ### Disposable 支持
231
-
232
- `Container` 和 `Scope` 都实现了 `AsyncDisposable`。销毁时,托管实例按创建的逆序(LIFO)遍历,并自动调用其 `[Symbol.asyncDispose]()` 或 `[Symbol.dispose]()` 方法。
233
-
234
- ```ts
235
- import { createContainer } from 'katagami';
236
-
237
- class Connection {
238
- async [Symbol.asyncDispose]() {
239
- console.log('Connection closed');
240
- }
241
- }
242
-
243
- // 手动销毁
244
- const container = createContainer().registerSingleton(Connection, () => new Connection());
245
-
246
- container.resolve(Connection);
247
- await container[Symbol.asyncDispose]();
248
- // => "Connection closed"
249
- ```
250
-
251
- 使用 `await using` 时,作用域在块结束时自动销毁:
252
-
253
- ```ts
254
- const root = createContainer()
255
- .registerSingleton(DbPool, () => new DbPool())
256
- .registerScoped(Connection, () => new Connection());
257
-
258
- {
259
- await using scope = root.createScope();
260
- const conn = scope.resolve(Connection);
261
- // ... 使用 conn ...
262
- } // 此处作用域被销毁 — Connection 被清理,DbPool 不受影响
263
- ```
264
-
265
- 作用域销毁仅影响 Scoped 实例。Singleton 实例归根容器所有,在容器本身销毁时才会被销毁。
266
-
267
- ### 接口类型映射
268
-
269
- 当你向 `createContainer<T>()` 传入接口时,PropertyKey 令牌的类型来源于接口而非通过链式积累。这意味着你可以以任意顺序注册和解析令牌:
270
-
271
- ```ts
272
- import { createContainer } from 'katagami';
273
-
274
- class Logger {
275
- log(msg: string) {
276
- console.log(msg);
277
- }
278
- }
279
-
280
- interface Services {
281
- logger: Logger;
282
- greeting: string;
283
- }
284
-
285
- const container = createContainer<Services>()
286
- // 'greeting' 可以引用 'logger',即使 'logger' 是后注册的
287
- .registerSingleton('greeting', r => {
288
- r.resolve('logger').log('正在构建 greeting...');
289
- return 'Hello!';
290
- })
291
- .registerSingleton('logger', () => new Logger());
292
-
293
- const greeting = container.resolve('greeting');
294
- // ^? string
295
- ```
296
-
297
- ### 混合令牌策略
298
-
299
- 你可以混合使用两种方式——使用类令牌获得顺序依赖的类型安全,使用 PropertyKey 令牌获得顺序无关的灵活性:
300
-
301
- ```ts
302
- const container = createContainer<Services>()
303
- .registerSingleton(Logger, () => new Logger())
304
- .registerSingleton('logger', () => new Logger())
305
- .registerSingleton('greeting', r => {
306
- r.resolve(Logger).log('正在构建 greeting...');
307
- return 'Hello!';
308
- });
309
- ```
310
-
311
- ### 捕获依赖防护
312
-
313
- "捕获依赖"是指长生命周期的服务(Singleton 或 Transient)捕获了短生命周期的服务(Scoped),使其存活超出预期的作用域。Katagami 在编译时防止这种情况——Singleton 和 Transient 工厂只会收到限制为非 Scoped 令牌的解析器:
314
-
315
- ```ts
316
- import { createContainer } from 'katagami';
317
-
318
- class DbPool {}
319
- class RequestContext {}
320
-
321
- const container = createContainer()
322
- .registerScoped(RequestContext, () => new RequestContext())
323
- // @ts-expect-error — Singleton 工厂无法解析 Scoped 令牌
324
- .registerSingleton(DbPool, r => new DbPool(r.resolve(RequestContext)));
325
- ```
326
-
327
- 而 Scoped 工厂则可以解析 Scoped 和非 Scoped 令牌:
328
-
329
- ```ts
330
- const container = createContainer()
331
- .registerSingleton(DbPool, () => new DbPool())
332
- .registerScoped(RequestContext, r => {
333
- r.resolve(DbPool); // OK — Scoped 工厂可以解析 Singleton 令牌
334
- return new RequestContext();
335
- });
336
- ```
337
-
338
- ### 可选解析(tryResolve)
339
-
340
- 当需要处理可选依赖或想在不抛出错误的情况下检查令牌是否已注册时,使用 `tryResolve`。与 `resolve` 不同,它对未注册的令牌返回 `undefined` 而不是抛出 `ContainerError`:
341
-
342
- ```ts
343
- import { createContainer } from 'katagami';
344
-
345
- class Logger {
346
- log(msg: string) {
347
- console.log(msg);
348
- }
349
- }
350
-
351
- class Analytics {
352
- track(event: string) {
353
- console.log(`Track: ${event}`);
354
- }
355
- }
356
-
357
- const container = createContainer().registerSingleton(Logger, () => new Logger());
358
-
359
- // resolve 对未注册令牌抛出异常
360
- container.resolve(Analytics); // ContainerError: Token "Analytics" is not registered.
361
-
362
- // tryResolve 对未注册令牌返回 undefined
363
- const analytics = container.tryResolve(Analytics);
364
- // ^? Analytics | undefined
365
- if (analytics) {
366
- analytics.track('event');
367
- }
368
- ```
369
-
370
- `tryResolve` 对于工厂中的可选依赖特别有用。与 `resolve` 不同,它接受未注册的令牌而不会产生编译时错误:
371
-
372
- ```ts
373
- const container = createContainer()
374
- .registerSingleton(Logger, () => new Logger())
375
- .registerSingleton('UserService', r => {
376
- const logger = r.tryResolve(Logger); // 可选依赖
377
- const analytics = r.tryResolve(Analytics); // Analytics 未注册但不会产生编译错误
378
-
379
- return {
380
- greet(name: string) {
381
- logger?.log(`Hello, ${name}`);
382
- analytics?.track('user_greeted');
383
- },
384
- };
385
- });
386
- ```
387
-
388
- `tryResolve` 仍会对循环依赖和已销毁容器/作用域的操作抛出 `ContainerError` — 只有未注册的令牌才返回 `undefined`。
389
-
390
- ## API
391
-
392
- ### `createContainer<T, ScopedT>()`
393
-
394
- 创建新的 DI 容器。传入接口作为 `T` 以定义 PropertyKey 令牌的类型映射。传入 `ScopedT` 以定义 Scoped PropertyKey 令牌的独立类型映射(与 `T` 一样与注册顺序无关)。
395
-
396
- ### `container.registerSingleton(token, factory)`
397
-
398
- 将工厂注册为 Singleton。实例在首次 `resolve` 时创建并缓存。返回容器以支持方法链。
399
-
400
- ### `container.registerTransient(token, factory)`
401
-
402
- 将工厂注册为 Transient。每次 `resolve` 都会创建新实例。返回容器以支持方法链。
403
-
404
- ### `container.registerScoped(token, factory)`
405
-
406
- 将工厂注册为 Scoped。在作用域内,实例在首次 `resolve` 时创建并在该作用域内缓存。每个作用域维护自己的缓存。Scoped 令牌无法从根容器解析。返回容器以支持方法链。
407
-
408
- ### `container.resolve(token)`
409
-
410
- 解析并返回给定令牌的实例。如果令牌未注册或检测到循环依赖,则抛出 `ContainerError`。
411
-
412
- ### `container.tryResolve(token)` / `scope.tryResolve(token)`
413
-
414
- 尝试解析给定令牌的实例。如果令牌未注册,返回 `undefined` 而不是抛出异常。对于循环依赖或已销毁容器/作用域的操作仍会抛出 `ContainerError`。
415
-
416
- ### `container.createScope()`
417
-
418
- 创建新的 `Scope`(子容器)。作用域继承父级的所有注册。Singleton 实例与父级共享,Scoped 实例为作用域本地。
419
-
420
- ### `Scope`
421
-
422
- 由 `createScope()` 创建的作用域子容器。提供 `resolve(token)`、`tryResolve(token)`、`createScope()`(用于嵌套作用域)和 `[Symbol.asyncDispose]()`。
423
-
424
- ### `container[Symbol.asyncDispose]()` / `scope[Symbol.asyncDispose]()`
425
-
426
- 按创建的逆序(LIFO)销毁所有托管实例。调用每个实例的 `[Symbol.asyncDispose]()` 或 `[Symbol.dispose]()`。幂等——后续调用为空操作。销毁后,`resolve()` 和 `createScope()` 将抛出 `ContainerError`。
427
-
428
- ### `ContainerError`
429
-
430
- 用于容器故障的错误类,例如解析未注册的令牌、循环依赖或对已销毁容器/作用域的操作。
431
-
432
- ### `Resolver`
433
-
434
- 表示传递给工厂回调的解析器的类型导出。当你需要为接受解析器参数的函数添加类型时很有用。
435
-
436
- ## 许可证
437
-
438
- MIT