@amriogit/injector 0.1.0 → 0.3.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.md +112 -0
- package/dist/injector-vue2.d.ts +94 -0
- package/dist/injector-vue2.js +110 -0
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -61,6 +61,37 @@ const userService = useInject(UserService)
|
|
|
61
61
|
useProvide(UserService, MockUserService)
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Vue 2 集成
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import Vue from 'vue'
|
|
68
|
+
import { ServicePlugin, mapInject } from '@amriogit/injector/vue2'
|
|
69
|
+
|
|
70
|
+
// 安装插件
|
|
71
|
+
Vue.use(ServicePlugin, {
|
|
72
|
+
setup(injector) {
|
|
73
|
+
injector.provide(API_BASE, '/api')
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// 使用 mapInject 在 computed 中批量声明依赖
|
|
78
|
+
export default {
|
|
79
|
+
computed: {
|
|
80
|
+
...mapInject({ UserService, ChannelService }),
|
|
81
|
+
},
|
|
82
|
+
created() {
|
|
83
|
+
this.userService.fetchUsers()
|
|
84
|
+
this.channelService.load()
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Vue 2 注意事项
|
|
90
|
+
|
|
91
|
+
1. **推荐 `mapInject`**:批量声明、自动 camelCase 转换、类型安全
|
|
92
|
+
2. **无额外依赖**:不依赖 Composition API,Vue 2.0+ 通用
|
|
93
|
+
3. **自动清理**:根组件销毁时自动调用 `injector.reset()`,触发所有 Service 的 `onDestroy` 钩子
|
|
94
|
+
|
|
64
95
|
## 特性
|
|
65
96
|
|
|
66
97
|
- **零依赖** — 纯 TypeScript,无运行时依赖
|
|
@@ -71,6 +102,87 @@ useProvide(UserService, MockUserService)
|
|
|
71
102
|
- **生命周期** — `onInit()` 钩子在依赖就绪后调用
|
|
72
103
|
- **SSR 兼容** — 无 DOM 依赖,InjectionToken 可序列化
|
|
73
104
|
|
|
105
|
+
## 生命周期
|
|
106
|
+
|
|
107
|
+
### onInit — 初始化
|
|
108
|
+
|
|
109
|
+
`onInit()` 在 Service 构造完成、所有 `$inject()` 依赖就绪后自动调用。不要在构造函数中做初始化逻辑。
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { BaseService } from '@amriogit/injector'
|
|
113
|
+
|
|
114
|
+
class TimerService extends BaseService {
|
|
115
|
+
private logger = this.$inject(LoggerService)
|
|
116
|
+
private intervalId?: ReturnType<typeof setInterval>
|
|
117
|
+
|
|
118
|
+
state = reactive({ ticks: 0 })
|
|
119
|
+
|
|
120
|
+
onInit() {
|
|
121
|
+
// 依赖已就绪,可以安全使用 this.logger
|
|
122
|
+
this.intervalId = setInterval(() => {
|
|
123
|
+
this.state.ticks++
|
|
124
|
+
this.logger.log(`tick ${this.state.ticks}`)
|
|
125
|
+
}, 1000)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### onDestroy — 清理
|
|
131
|
+
|
|
132
|
+
`onDestroy()` 在 Service 被销毁时调用,用于清理定时器、取消订阅、关闭连接等。
|
|
133
|
+
|
|
134
|
+
触发方式:
|
|
135
|
+
|
|
136
|
+
- **`injector.destroy(Token)`** — 销毁指定 Service 实例
|
|
137
|
+
- **`injector.reset()`** — 销毁所有 Service 实例
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { BaseService, Injector, InjectionToken } from '@amriogit/injector'
|
|
141
|
+
|
|
142
|
+
// WebSocket 连接示例
|
|
143
|
+
class ConnectionService extends BaseService {
|
|
144
|
+
private ws?: WebSocket
|
|
145
|
+
|
|
146
|
+
connect = (url: string) => {
|
|
147
|
+
this.ws = new WebSocket(url)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onDestroy() {
|
|
151
|
+
this.ws?.close()
|
|
152
|
+
this.ws = undefined
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 定时器清理示例
|
|
157
|
+
class PollingService extends BaseService {
|
|
158
|
+
private timer?: ReturnType<typeof setInterval>
|
|
159
|
+
|
|
160
|
+
onInit() {
|
|
161
|
+
this.timer = setInterval(() => this.poll(), 5000)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private poll = () => { /* ... */ }
|
|
165
|
+
|
|
166
|
+
onDestroy() {
|
|
167
|
+
clearInterval(this.timer)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const injector = new Injector()
|
|
172
|
+
|
|
173
|
+
const polling = injector.inject(PollingService)
|
|
174
|
+
|
|
175
|
+
// 需要替换实现时,先销毁旧实例
|
|
176
|
+
injector.destroy(PollingService)
|
|
177
|
+
// polling.onDestroy() 已调用,定时器已清理
|
|
178
|
+
// 下次 inject(PollingService) 返回新实例
|
|
179
|
+
|
|
180
|
+
// 或者一次性清理所有
|
|
181
|
+
injector.reset()
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`onDestroy` 与 `onInit` 配对使用,确保 Service 的资源生命周期可管理,防止内存泄漏。
|
|
185
|
+
|
|
74
186
|
## 测试
|
|
75
187
|
|
|
76
188
|
```bash
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue 2 集成层,用于对接共享 DI 核心。
|
|
3
|
+
* 与 injector.ts 分离,使服务端不打包 Vue。
|
|
4
|
+
*
|
|
5
|
+
* 通过 `Vue.use(ServicePlugin)` 安装后,所有组件实例
|
|
6
|
+
* 可使用 `this.$inject(Token)` 注入 Service。
|
|
7
|
+
*
|
|
8
|
+
* @module injector-vue2
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Vue 2 插件预期 `import Vue from 'vue'` 得到 Vue 2 构造函数。
|
|
12
|
+
* 当前项目 devDep 为 Vue 3(与 ./injector-vue.ts 共享),Vue 3 的 ESM
|
|
13
|
+
* 无 default export,因此需要 @ts-expect-error。
|
|
14
|
+
* 在消费方(Vue 2 项目)中,这行能正确解析。
|
|
15
|
+
*/
|
|
16
|
+
import VueConstructor from 'vue';
|
|
17
|
+
import { Injector, InjectionToken, type Token } from './injector';
|
|
18
|
+
type ClassToken<T> = {
|
|
19
|
+
new (...args: any[]): T;
|
|
20
|
+
};
|
|
21
|
+
type Resolved<T> = T extends InjectionToken<infer V> ? V : T extends ClassToken<infer V> ? V : never;
|
|
22
|
+
/** 将 PascalCase / SCREAMING_SNAKE 转为 camelCase */
|
|
23
|
+
type CamelCase<S extends string, FromSnake extends boolean = false> = S extends `${infer A}_${infer B}` ? `${Lowercase<A>}${Capitalize<CamelCase<B, true>>}` : FromSnake extends true ? Capitalize<Lowercase<S>> : Uncapitalize<S>;
|
|
24
|
+
/** mapInject 的返回类型 */
|
|
25
|
+
type MapInjectResult<T extends Record<string, Token<any>>> = {
|
|
26
|
+
[K in keyof T as CamelCase<K & string>]: Resolved<T[K]>;
|
|
27
|
+
};
|
|
28
|
+
/** 计算属性定义的类型(每个 key 对应一个 getter) */
|
|
29
|
+
type ComputedGetters<T> = {
|
|
30
|
+
[K in keyof T]: (this: any) => T[K];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Vue 2 插件。
|
|
34
|
+
*
|
|
35
|
+
* 安装后,所有组件实例可通过 `this.$inject(Token)` 访问 Service。
|
|
36
|
+
*
|
|
37
|
+
* 通过 mixin 在根组件销毁时自动清理所有 Service 实例的 onDestroy。
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import Vue from 'vue'
|
|
42
|
+
* import { ServicePlugin } from '@amriogit/injector/vue2'
|
|
43
|
+
*
|
|
44
|
+
* Vue.use(ServicePlugin, {
|
|
45
|
+
* setup(injector) {
|
|
46
|
+
* injector.provide(API_BASE, '/api')
|
|
47
|
+
* },
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* 在组件中使用(推荐 `mapInject`):
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { mapInject } from '@amriogit/injector/vue2'
|
|
54
|
+
*
|
|
55
|
+
* export default {
|
|
56
|
+
* computed: {
|
|
57
|
+
* ...mapInject({ UserService }),
|
|
58
|
+
* },
|
|
59
|
+
* created() {
|
|
60
|
+
* this.userService.fetchUsers()
|
|
61
|
+
* },
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare const ServicePlugin: {
|
|
66
|
+
install(VueInstance: typeof VueConstructor, options?: {
|
|
67
|
+
setup?: (injector: Injector) => void;
|
|
68
|
+
}): void;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* 批量声明 Service 注入,返回计算属性定义对象。
|
|
72
|
+
*
|
|
73
|
+
* 键名自动转换:`ChannelService` → `channelService`,`API_TOKEN` → `apiToken`。
|
|
74
|
+
* 配合 `...` 展开到 `computed` 中使用。
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* export default {
|
|
79
|
+
* computed: {
|
|
80
|
+
* ...mapInject({ ChannelService, UserService, API_TOKEN }),
|
|
81
|
+
* // 展开后等价于:
|
|
82
|
+
* // channelService() { return this.$inject(ChannelService) },
|
|
83
|
+
* // userService() { return this.$inject(UserService) },
|
|
84
|
+
* // apiToken() { return this.$inject(API_TOKEN) },
|
|
85
|
+
* },
|
|
86
|
+
* created() {
|
|
87
|
+
* this.channelService.fetchChannels()
|
|
88
|
+
* console.log(this.apiToken)
|
|
89
|
+
* },
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export declare function mapInject<T extends Record<string, Token<any>>>(tokens: T): ComputedGetters<MapInjectResult<T>>;
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue 2 集成层,用于对接共享 DI 核心。
|
|
3
|
+
* 与 injector.ts 分离,使服务端不打包 Vue。
|
|
4
|
+
*
|
|
5
|
+
* 通过 `Vue.use(ServicePlugin)` 安装后,所有组件实例
|
|
6
|
+
* 可使用 `this.$inject(Token)` 注入 Service。
|
|
7
|
+
*
|
|
8
|
+
* @module injector-vue2
|
|
9
|
+
*/
|
|
10
|
+
import { Injector } from './injector';
|
|
11
|
+
// ─── 工具函数 ──────────────────────────────────────────────
|
|
12
|
+
function toCamelCase(key) {
|
|
13
|
+
// SCREAMING_SNAKE_CASE → camelCase
|
|
14
|
+
if (key.includes('_')) {
|
|
15
|
+
return key
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.split('_')
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.map((w, i) => (i === 0 ? w : w[0].toUpperCase() + w.slice(1)))
|
|
20
|
+
.join('');
|
|
21
|
+
}
|
|
22
|
+
// PascalCase → camelCase
|
|
23
|
+
return key[0].toLowerCase() + key.slice(1);
|
|
24
|
+
}
|
|
25
|
+
// ─── Vue 2 Plugin ──────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Vue 2 插件。
|
|
28
|
+
*
|
|
29
|
+
* 安装后,所有组件实例可通过 `this.$inject(Token)` 访问 Service。
|
|
30
|
+
*
|
|
31
|
+
* 通过 mixin 在根组件销毁时自动清理所有 Service 实例的 onDestroy。
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import Vue from 'vue'
|
|
36
|
+
* import { ServicePlugin } from '@amriogit/injector/vue2'
|
|
37
|
+
*
|
|
38
|
+
* Vue.use(ServicePlugin, {
|
|
39
|
+
* setup(injector) {
|
|
40
|
+
* injector.provide(API_BASE, '/api')
|
|
41
|
+
* },
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* 在组件中使用(推荐 `mapInject`):
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { mapInject } from '@amriogit/injector/vue2'
|
|
48
|
+
*
|
|
49
|
+
* export default {
|
|
50
|
+
* computed: {
|
|
51
|
+
* ...mapInject({ UserService }),
|
|
52
|
+
* },
|
|
53
|
+
* created() {
|
|
54
|
+
* this.userService.fetchUsers()
|
|
55
|
+
* },
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export const ServicePlugin = {
|
|
60
|
+
install(VueInstance, options) {
|
|
61
|
+
const injector = new Injector();
|
|
62
|
+
options?.setup?.(injector);
|
|
63
|
+
// 全局可访问:this.$injector 和 this.$inject 在所有组件实例上可用
|
|
64
|
+
VueInstance.prototype.$injector = injector;
|
|
65
|
+
VueInstance.prototype.$inject = injector.inject.bind(injector);
|
|
66
|
+
// 全局 mixin:根组件销毁时自动清理所有 Service
|
|
67
|
+
VueInstance.mixin({
|
|
68
|
+
beforeDestroy() {
|
|
69
|
+
if (this.$root === this) {
|
|
70
|
+
injector.reset();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
// ─── mapInject ─────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* 批量声明 Service 注入,返回计算属性定义对象。
|
|
79
|
+
*
|
|
80
|
+
* 键名自动转换:`ChannelService` → `channelService`,`API_TOKEN` → `apiToken`。
|
|
81
|
+
* 配合 `...` 展开到 `computed` 中使用。
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* export default {
|
|
86
|
+
* computed: {
|
|
87
|
+
* ...mapInject({ ChannelService, UserService, API_TOKEN }),
|
|
88
|
+
* // 展开后等价于:
|
|
89
|
+
* // channelService() { return this.$inject(ChannelService) },
|
|
90
|
+
* // userService() { return this.$inject(UserService) },
|
|
91
|
+
* // apiToken() { return this.$inject(API_TOKEN) },
|
|
92
|
+
* },
|
|
93
|
+
* created() {
|
|
94
|
+
* this.channelService.fetchChannels()
|
|
95
|
+
* console.log(this.apiToken)
|
|
96
|
+
* },
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function mapInject(tokens) {
|
|
101
|
+
const result = {};
|
|
102
|
+
for (const key of Object.keys(tokens)) {
|
|
103
|
+
const camelKey = toCamelCase(key);
|
|
104
|
+
const token = tokens[key];
|
|
105
|
+
result[camelKey] = function () {
|
|
106
|
+
return this.$inject(token);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amriogit/injector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "轻量 DI 容器 — Injector, BaseService, InjectionToken。零外部依赖,支持 SSR,Vue 集成。",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"di",
|
|
8
|
+
"dependency-injection",
|
|
9
|
+
"injector",
|
|
10
|
+
"typescript",
|
|
11
|
+
"vue",
|
|
12
|
+
"ssr"
|
|
13
|
+
],
|
|
7
14
|
"sideEffects": false,
|
|
8
15
|
"repository": {
|
|
9
16
|
"type": "git",
|
|
@@ -17,14 +24,17 @@
|
|
|
17
24
|
"types": "./dist/injector.d.ts",
|
|
18
25
|
"exports": {
|
|
19
26
|
".": "./dist/injector.js",
|
|
20
|
-
"./vue": "./dist/injector-vue.js"
|
|
27
|
+
"./vue": "./dist/injector-vue.js",
|
|
28
|
+
"./vue2": "./dist/injector-vue2.js"
|
|
21
29
|
},
|
|
22
|
-
"files": [
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
23
33
|
"publishConfig": {
|
|
24
34
|
"access": "public"
|
|
25
35
|
},
|
|
26
36
|
"peerDependencies": {
|
|
27
|
-
"vue": "^3.4.0"
|
|
37
|
+
"vue": "^2.6.0 || ^3.4.0"
|
|
28
38
|
},
|
|
29
39
|
"peerDependenciesMeta": {
|
|
30
40
|
"vue": {
|
|
@@ -38,6 +48,7 @@
|
|
|
38
48
|
"prepublishOnly": "npm test && npm run build"
|
|
39
49
|
},
|
|
40
50
|
"devDependencies": {
|
|
51
|
+
"@types/vue": "^2.0.0",
|
|
41
52
|
"typescript": "^5.8.3",
|
|
42
53
|
"vitest": "^3.2.4",
|
|
43
54
|
"vue": "^3.5.34"
|