@taicode/common-web 1.1.0 → 1.1.2
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/output/cache-api/cache-api.d.ts +13 -0
- package/output/cache-api/cache-api.d.ts.map +1 -0
- package/output/cache-api/cache-api.js +114 -0
- package/output/cache-api/cache-api.test.d.ts +2 -0
- package/output/cache-api/cache-api.test.d.ts.map +1 -0
- package/output/cache-api/cache-api.test.js +348 -0
- package/output/cache-api/index.d.ts +2 -0
- package/output/cache-api/index.d.ts.map +1 -0
- package/output/cache-api/index.js +1 -0
- package/output/helpers/cache-api/cache-api.d.ts +13 -0
- package/output/helpers/cache-api/cache-api.d.ts.map +1 -0
- package/output/helpers/cache-api/cache-api.js +114 -0
- package/output/helpers/cache-api/cache-api.test.d.ts +2 -0
- package/output/helpers/cache-api/cache-api.test.d.ts.map +1 -0
- package/output/helpers/cache-api/cache-api.test.js +348 -0
- package/output/helpers/cache-api/index.d.ts +2 -0
- package/output/helpers/cache-api/index.d.ts.map +1 -0
- package/output/helpers/cache-api/index.js +1 -0
- package/output/helpers/side-cache/side-cache.d.ts +5 -2
- package/output/helpers/side-cache/side-cache.d.ts.map +1 -1
- package/output/helpers/side-cache/side-cache.js +41 -10
- package/output/helpers/side-cache/side-cache.test.js +166 -76
- package/output/service/index.d.ts +2 -0
- package/output/service/index.d.ts.map +1 -0
- package/output/service/index.js +1 -0
- package/output/service/service.d.ts +114 -0
- package/output/service/service.d.ts.map +1 -0
- package/output/service/service.js +189 -0
- package/output/service/service.test.d.ts +2 -0
- package/output/service/service.test.d.ts.map +1 -0
- package/output/service/service.test.jsx +367 -0
- package/output/side-cache/index.d.ts +2 -0
- package/output/side-cache/index.d.ts.map +1 -0
- package/output/side-cache/index.js +1 -0
- package/output/side-cache/side-cache.d.ts +10 -0
- package/output/side-cache/side-cache.d.ts.map +1 -0
- package/output/side-cache/side-cache.js +137 -0
- package/output/side-cache/side-cache.test.d.ts +2 -0
- package/output/side-cache/side-cache.test.d.ts.map +1 -0
- package/output/side-cache/side-cache.test.js +179 -0
- package/output/use-observer/index.d.ts +2 -0
- package/output/use-observer/index.d.ts.map +1 -0
- package/output/use-observer/index.js +1 -0
- package/output/use-observer/use-observer.d.ts +3 -0
- package/output/use-observer/use-observer.d.ts.map +1 -0
- package/output/use-observer/use-observer.js +16 -0
- package/output/use-observer/use-observer.test.d.ts +2 -0
- package/output/use-observer/use-observer.test.d.ts.map +1 -0
- package/output/use-observer/use-observer.test.jsx +134 -0
- package/package.json +2 -1
|
@@ -1,89 +1,179 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { SideCache } from './side-cache';
|
|
3
3
|
describe('SideCache', () => {
|
|
4
|
-
let
|
|
4
|
+
let cache;
|
|
5
5
|
beforeEach(() => {
|
|
6
|
-
|
|
6
|
+
cache = new SideCache();
|
|
7
7
|
});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
describe('初始状态', () => {
|
|
9
|
+
it('应该初始化为空状态', () => {
|
|
10
|
+
expect(cache.empty).toBe(true);
|
|
11
|
+
expect(cache.value).toBe(undefined);
|
|
12
|
+
});
|
|
11
13
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
describe('同步缓存', () => {
|
|
15
|
+
it('应该能够缓存同步函数的结果', () => {
|
|
16
|
+
const mockFunc = vi.fn(() => 'test-result');
|
|
17
|
+
const key = 'test-key';
|
|
18
|
+
const result = cache.handle(key, mockFunc);
|
|
19
|
+
expect(result).toBe('test-result');
|
|
20
|
+
expect(mockFunc).toHaveBeenCalledTimes(1);
|
|
21
|
+
expect(cache.empty).toBe(false);
|
|
22
|
+
expect(cache.value).toBe('test-result');
|
|
23
|
+
});
|
|
24
|
+
it('应该对相同 key 返回缓存的结果', () => {
|
|
25
|
+
const mockFunc = vi.fn(() => 'cached-result');
|
|
26
|
+
const key = 'same-key';
|
|
27
|
+
// 第一次调用
|
|
28
|
+
const result1 = cache.handle(key, mockFunc);
|
|
29
|
+
expect(result1).toBe('cached-result');
|
|
30
|
+
expect(mockFunc).toHaveBeenCalledTimes(1);
|
|
31
|
+
// 第二次调用相同 key,应该直接返回缓存
|
|
32
|
+
const result2 = cache.handle(key, mockFunc);
|
|
33
|
+
expect(result2).toBe('cached-result');
|
|
34
|
+
expect(mockFunc).toHaveBeenCalledTimes(2); // 函数仍会被调用,但会更新缓存
|
|
35
|
+
expect(cache.value).toBe('cached-result');
|
|
36
|
+
});
|
|
37
|
+
it('应该对不同 key 分别缓存', () => {
|
|
38
|
+
const mockFunc1 = vi.fn(() => 'result1');
|
|
39
|
+
const mockFunc2 = vi.fn(() => 'result2');
|
|
40
|
+
cache.handle('key1', mockFunc1);
|
|
41
|
+
expect(cache.value).toBe('result1');
|
|
42
|
+
cache.handle('key2', mockFunc2);
|
|
43
|
+
expect(cache.value).toBe('result2');
|
|
44
|
+
// 切换回 key1
|
|
45
|
+
cache.handle('key1', mockFunc1);
|
|
46
|
+
expect(cache.value).toBe('result1');
|
|
47
|
+
});
|
|
19
48
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
describe('异步缓存', () => {
|
|
50
|
+
it('应该能够缓存异步函数的结果', async () => {
|
|
51
|
+
const mockAsyncFunc = vi.fn(async () => 'async-result');
|
|
52
|
+
const key = 'async-key';
|
|
53
|
+
const promise = cache.handle(key, mockAsyncFunc);
|
|
54
|
+
expect(promise).toBeInstanceOf(Promise);
|
|
55
|
+
const result = await promise;
|
|
56
|
+
expect(result).toBe('async-result');
|
|
57
|
+
expect(mockAsyncFunc).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(cache.empty).toBe(false);
|
|
59
|
+
expect(cache.value).toBe('async-result');
|
|
60
|
+
});
|
|
61
|
+
it('应该在异步函数执行期间立即设置 currentKey', async () => {
|
|
62
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
63
|
+
const mockAsyncFunc = vi.fn(async () => {
|
|
64
|
+
await delay(10);
|
|
65
|
+
return 'delayed-result';
|
|
66
|
+
});
|
|
67
|
+
const key = 'delayed-key';
|
|
68
|
+
// 在异步函数开始执行时,currentKey 应该已经设置
|
|
69
|
+
const promise = cache.handle(key, mockAsyncFunc);
|
|
70
|
+
// 此时异步函数还在执行,但 currentKey 已设置
|
|
71
|
+
// 如果之前有缓存,value 应该能立即获取到
|
|
72
|
+
const result = await promise;
|
|
73
|
+
expect(result).toBe('delayed-result');
|
|
74
|
+
expect(cache.value).toBe('delayed-result');
|
|
75
|
+
});
|
|
76
|
+
it('应该处理异步函数的错误', async () => {
|
|
77
|
+
const error = new Error('Async error');
|
|
78
|
+
const mockAsyncFunc = vi.fn(async () => {
|
|
79
|
+
throw error;
|
|
80
|
+
});
|
|
81
|
+
const key = 'error-key';
|
|
82
|
+
await expect(cache.handle(key, mockAsyncFunc)).rejects.toThrow('Async error');
|
|
83
|
+
expect(mockAsyncFunc).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
29
85
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
describe('key 序列化', () => {
|
|
87
|
+
it('应该正确处理不同类型的 key', () => {
|
|
88
|
+
const mockFunc = vi.fn(() => 'result');
|
|
89
|
+
// 字符串 key
|
|
90
|
+
cache.handle('string-key', mockFunc);
|
|
91
|
+
expect(cache.value).toBe('result');
|
|
92
|
+
// 数字 key
|
|
93
|
+
cache.handle(123, mockFunc);
|
|
94
|
+
expect(cache.value).toBe('result');
|
|
95
|
+
// 对象 key
|
|
96
|
+
cache.handle({ id: 1, name: 'test' }, mockFunc);
|
|
97
|
+
expect(cache.value).toBe('result');
|
|
98
|
+
// 数组 key
|
|
99
|
+
cache.handle([1, 2, 3], mockFunc);
|
|
100
|
+
expect(cache.value).toBe('result');
|
|
101
|
+
});
|
|
102
|
+
it('相同内容的对象 key 应该被视为相同', () => {
|
|
103
|
+
const mockFunc = vi.fn(() => 'object-result');
|
|
104
|
+
const key1 = { id: 1, name: 'test' };
|
|
105
|
+
const key2 = { id: 1, name: 'test' };
|
|
106
|
+
cache.handle(key1, mockFunc);
|
|
107
|
+
const result1 = cache.value;
|
|
108
|
+
cache.handle(key2, mockFunc);
|
|
109
|
+
const result2 = cache.value;
|
|
110
|
+
expect(result1).toBe(result2);
|
|
111
|
+
expect(result1).toBe('object-result');
|
|
112
|
+
});
|
|
36
113
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
114
|
+
describe('缓存时间戳', () => {
|
|
115
|
+
it('应该为缓存项添加创建时间', () => {
|
|
116
|
+
const mockFunc = vi.fn(() => 'timestamped-result');
|
|
117
|
+
const key = 'timestamp-key';
|
|
118
|
+
const beforeTime = new Date().toISOString();
|
|
119
|
+
cache.handle(key, mockFunc);
|
|
120
|
+
const afterTime = new Date().toISOString();
|
|
121
|
+
// 访问私有属性进行测试(仅用于测试目的)
|
|
122
|
+
const cacheData = cache.cache;
|
|
123
|
+
const cachedItem = cacheData[JSON.stringify(key)];
|
|
124
|
+
expect(cachedItem).toBeDefined();
|
|
125
|
+
expect(cachedItem.data).toBe('timestamped-result');
|
|
126
|
+
expect(cachedItem.createTime).toBeDefined();
|
|
127
|
+
expect(typeof cachedItem.createTime).toBe('string');
|
|
128
|
+
// 验证时间戳在合理范围内
|
|
129
|
+
expect(cachedItem.createTime >= beforeTime).toBe(true);
|
|
130
|
+
expect(cachedItem.createTime <= afterTime).toBe(true);
|
|
131
|
+
});
|
|
50
132
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
133
|
+
describe('边界情况', () => {
|
|
134
|
+
it('应该处理 null 和 undefined key', () => {
|
|
135
|
+
const mockFunc1 = vi.fn(() => 'null-key-result');
|
|
136
|
+
const mockFunc2 = vi.fn(() => 'undefined-key-result');
|
|
137
|
+
cache.handle(null, mockFunc1);
|
|
138
|
+
expect(cache.value).toBe('null-key-result');
|
|
139
|
+
cache.handle(undefined, mockFunc2);
|
|
140
|
+
expect(cache.value).toBe('undefined-key-result');
|
|
141
|
+
});
|
|
142
|
+
it('应该处理返回 null 或 undefined 的函数', () => {
|
|
143
|
+
const nullFunc = vi.fn(() => null);
|
|
144
|
+
const undefinedFunc = vi.fn(() => undefined);
|
|
145
|
+
cache.handle('null-key', nullFunc);
|
|
146
|
+
expect(cache.value).toBe(null);
|
|
147
|
+
expect(cache.empty).toBe(false);
|
|
148
|
+
cache.handle('undefined-key', undefinedFunc);
|
|
149
|
+
expect(cache.value).toBe(undefined);
|
|
150
|
+
expect(cache.empty).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
it('应该处理返回 Promise.resolve(null) 的异步函数', async () => {
|
|
153
|
+
const nullAsyncFunc = vi.fn(async () => null);
|
|
154
|
+
const result = await cache.handle('async-null', nullAsyncFunc);
|
|
155
|
+
expect(result).toBe(null);
|
|
156
|
+
expect(cache.value).toBe(null);
|
|
157
|
+
expect(cache.empty).toBe(false);
|
|
158
|
+
});
|
|
71
159
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
160
|
+
describe('类型检查', () => {
|
|
161
|
+
it('应该正确识别 Promise-like 对象', async () => {
|
|
162
|
+
let thenCallback;
|
|
163
|
+
const promiseLike = {
|
|
164
|
+
then: vi.fn((callback) => {
|
|
165
|
+
thenCallback = callback;
|
|
166
|
+
return promiseLike;
|
|
167
|
+
})
|
|
168
|
+
};
|
|
169
|
+
const mockFunc = vi.fn(() => promiseLike);
|
|
170
|
+
const result = cache.handle('promise-like', mockFunc);
|
|
171
|
+
expect(result).toBe(promiseLike);
|
|
172
|
+
expect(promiseLike.then).toHaveBeenCalled();
|
|
173
|
+
// 模拟 Promise 完成
|
|
174
|
+
if (thenCallback) {
|
|
175
|
+
thenCallback('resolved-value');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
88
178
|
});
|
|
89
179
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/service/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useService, ServiceProvider } from './service';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React 服务层集成工具
|
|
3
|
+
*
|
|
4
|
+
* 本模块提供了在 React 应用中集成依赖注入(DI)和状态管理(Mobx)的工具函数和组件。
|
|
5
|
+
* 主要功能包括:
|
|
6
|
+
* - 通过 useService Hook 使用全局注册的服务,并自动响应状态变化
|
|
7
|
+
* - 通过 useLocalService Hook 创建局部服务实例,避免全局状态污染
|
|
8
|
+
* - 通过 ServiceProvider 组件提供服务容器上下文
|
|
9
|
+
* - 自动初始化服务实例
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // 1. 定义服务
|
|
14
|
+
* class UserService extends Service {
|
|
15
|
+
* @observable users: User[] = []
|
|
16
|
+
* async init() { return true }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // 2. 在根组件提供服务
|
|
20
|
+
* function App() {
|
|
21
|
+
* return (
|
|
22
|
+
* <ServiceProvider services={[UserService]}>
|
|
23
|
+
* <UserList />
|
|
24
|
+
* </ServiceProvider>
|
|
25
|
+
* )
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // 3. 在组件中使用服务
|
|
29
|
+
* function UserList() {
|
|
30
|
+
* const users = useService(UserService, s => s.users)
|
|
31
|
+
* return <div>{users.map(user => <div key={user.id}>{user.name}</div>)}</div>
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import { Token, Container, Provider } from '@needle-di/core';
|
|
36
|
+
import React from 'react';
|
|
37
|
+
/**
|
|
38
|
+
* 使用全局注册的服务 Hook
|
|
39
|
+
*
|
|
40
|
+
* 该 Hook 从 DI 容器中获取指定的服务实例,并通过 selector 函数选择需要的数据。
|
|
41
|
+
* 当选择的数据发生变化时,组件会自动重新渲染。
|
|
42
|
+
*
|
|
43
|
+
* @template T 服务类型
|
|
44
|
+
* @template U 选择器返回的数据类型
|
|
45
|
+
* @param target 服务的 Token,用于从容器中获取服务实例
|
|
46
|
+
* @param selector 选择器函数,从服务实例中选择需要的数据
|
|
47
|
+
* @returns 选择器函数的返回值
|
|
48
|
+
*
|
|
49
|
+
* @throws {Error} 当组件不在 ServiceProvider 内部时抛出错误
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* // 获取用户列表
|
|
54
|
+
* const users = useService(UserService, service => service.users)
|
|
55
|
+
*
|
|
56
|
+
* // 获取当前用户信息
|
|
57
|
+
* const currentUser = useService(UserService, service => service.currentUser)
|
|
58
|
+
*
|
|
59
|
+
* // 获取用户数量
|
|
60
|
+
* const userCount = useService(UserService, service => service.users.length)
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function useService<T, U>(target: Token<T>, selector: (v: T) => U): U;
|
|
64
|
+
/**
|
|
65
|
+
* ServiceProvider 组件的属性接口
|
|
66
|
+
*/
|
|
67
|
+
interface ServiceProviderProps {
|
|
68
|
+
/** 要注册到容器中的服务提供者数组 */
|
|
69
|
+
services: Provider<unknown>[];
|
|
70
|
+
/** 子组件 */
|
|
71
|
+
children: React.ReactNode;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 服务提供者组件
|
|
75
|
+
*
|
|
76
|
+
* 该组件为其子组件树提供依赖注入容器上下文。
|
|
77
|
+
* 它会自动复用父级容器(如果存在),并创建子容器来管理新的服务实例。
|
|
78
|
+
*
|
|
79
|
+
* 特性:
|
|
80
|
+
* - 自动复用父级容器,避免重复创建
|
|
81
|
+
* - 支持容器层级结构,子容器可以覆盖父容器的服务
|
|
82
|
+
* - 自动初始化实现了 Service 接口的服务
|
|
83
|
+
*
|
|
84
|
+
* @param props 组件属性
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* function App() {
|
|
89
|
+
* return (
|
|
90
|
+
* <ServiceProvider services={[UserService, ProductService]}>
|
|
91
|
+
* <Router>
|
|
92
|
+
* <Routes>
|
|
93
|
+
* <Route path="/users" element={<UserPage />} />
|
|
94
|
+
* <Route path="/products" element={<ProductPage />} />
|
|
95
|
+
* </Routes>
|
|
96
|
+
* </Router>
|
|
97
|
+
* </ServiceProvider>
|
|
98
|
+
* )
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* // 嵌套服务提供者
|
|
102
|
+
* function UserPage() {
|
|
103
|
+
* return (
|
|
104
|
+
* <ServiceProvider services={[LocalUserService]}>
|
|
105
|
+
* <UserList />
|
|
106
|
+
* </ServiceProvider>
|
|
107
|
+
* )
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
/** 自动复用 Container,除非没有,否则不会重新创建 Container */
|
|
112
|
+
export declare function ServiceProvider(props: ServiceProviderProps): React.FunctionComponentElement<React.ProviderProps<Container | undefined>>;
|
|
113
|
+
export {};
|
|
114
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../source/service/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAA2D,MAAM,OAAO,CAAA;AAQ/E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,wBAAgB,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAuB3E;AAED;;GAEG;AACH,UAAU,oBAAoB;IAC5B,sBAAsB;IACtB,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAA;IAC7B,UAAU;IACV,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,6CAA6C;AAE7C,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,8EAuB1D"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React 服务层集成工具
|
|
3
|
+
*
|
|
4
|
+
* 本模块提供了在 React 应用中集成依赖注入(DI)和状态管理(Mobx)的工具函数和组件。
|
|
5
|
+
* 主要功能包括:
|
|
6
|
+
* - 通过 useService Hook 使用全局注册的服务,并自动响应状态变化
|
|
7
|
+
* - 通过 useLocalService Hook 创建局部服务实例,避免全局状态污染
|
|
8
|
+
* - 通过 ServiceProvider 组件提供服务容器上下文
|
|
9
|
+
* - 自动初始化服务实例
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // 1. 定义服务
|
|
14
|
+
* class UserService extends Service {
|
|
15
|
+
* @observable users: User[] = []
|
|
16
|
+
* async init() { return true }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // 2. 在根组件提供服务
|
|
20
|
+
* function App() {
|
|
21
|
+
* return (
|
|
22
|
+
* <ServiceProvider services={[UserService]}>
|
|
23
|
+
* <UserList />
|
|
24
|
+
* </ServiceProvider>
|
|
25
|
+
* )
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // 3. 在组件中使用服务
|
|
29
|
+
* function UserList() {
|
|
30
|
+
* const users = useService(UserService, s => s.users)
|
|
31
|
+
* return <div>{users.map(user => <div key={user.id}>{user.name}</div>)}</div>
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import { reaction, runInAction } from 'mobx';
|
|
36
|
+
import { catchIt } from '@taicode/common-base';
|
|
37
|
+
import { Container } from '@needle-di/core';
|
|
38
|
+
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
|
39
|
+
/**
|
|
40
|
+
* React Context 用于传递 DI 容器实例
|
|
41
|
+
* 通过上下文在组件树中共享依赖注入容器
|
|
42
|
+
*/
|
|
43
|
+
const ctx = React.createContext(undefined);
|
|
44
|
+
/**
|
|
45
|
+
* 使用全局注册的服务 Hook
|
|
46
|
+
*
|
|
47
|
+
* 该 Hook 从 DI 容器中获取指定的服务实例,并通过 selector 函数选择需要的数据。
|
|
48
|
+
* 当选择的数据发生变化时,组件会自动重新渲染。
|
|
49
|
+
*
|
|
50
|
+
* @template T 服务类型
|
|
51
|
+
* @template U 选择器返回的数据类型
|
|
52
|
+
* @param target 服务的 Token,用于从容器中获取服务实例
|
|
53
|
+
* @param selector 选择器函数,从服务实例中选择需要的数据
|
|
54
|
+
* @returns 选择器函数的返回值
|
|
55
|
+
*
|
|
56
|
+
* @throws {Error} 当组件不在 ServiceProvider 内部时抛出错误
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* // 获取用户列表
|
|
61
|
+
* const users = useService(UserService, service => service.users)
|
|
62
|
+
*
|
|
63
|
+
* // 获取当前用户信息
|
|
64
|
+
* const currentUser = useService(UserService, service => service.currentUser)
|
|
65
|
+
*
|
|
66
|
+
* // 获取用户数量
|
|
67
|
+
* const userCount = useService(UserService, service => service.users.length)
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
// DI + Mobx 自动
|
|
71
|
+
export function useService(target, selector) {
|
|
72
|
+
const container = useContext(ctx);
|
|
73
|
+
// 使用 useState 强制组件重新渲染
|
|
74
|
+
const [, refresh] = useState({});
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (container == null)
|
|
77
|
+
return;
|
|
78
|
+
// 从容器中获取服务实例
|
|
79
|
+
const service = container.get(target);
|
|
80
|
+
// 设置 Mobx reaction,当 selector 返回的数据变化时触发重新渲染
|
|
81
|
+
return reaction(() => selector(service), () => refresh({}) // 触发组件重新渲染
|
|
82
|
+
);
|
|
83
|
+
}, [container, selector, target]);
|
|
84
|
+
if (container == null) {
|
|
85
|
+
throw new Error('Must be a child of ServiceProvider.');
|
|
86
|
+
}
|
|
87
|
+
return selector(container.get(target));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 服务提供者组件
|
|
91
|
+
*
|
|
92
|
+
* 该组件为其子组件树提供依赖注入容器上下文。
|
|
93
|
+
* 它会自动复用父级容器(如果存在),并创建子容器来管理新的服务实例。
|
|
94
|
+
*
|
|
95
|
+
* 特性:
|
|
96
|
+
* - 自动复用父级容器,避免重复创建
|
|
97
|
+
* - 支持容器层级结构,子容器可以覆盖父容器的服务
|
|
98
|
+
* - 自动初始化实现了 Service 接口的服务
|
|
99
|
+
*
|
|
100
|
+
* @param props 组件属性
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* function App() {
|
|
105
|
+
* return (
|
|
106
|
+
* <ServiceProvider services={[UserService, ProductService]}>
|
|
107
|
+
* <Router>
|
|
108
|
+
* <Routes>
|
|
109
|
+
* <Route path="/users" element={<UserPage />} />
|
|
110
|
+
* <Route path="/products" element={<ProductPage />} />
|
|
111
|
+
* </Routes>
|
|
112
|
+
* </Router>
|
|
113
|
+
* </ServiceProvider>
|
|
114
|
+
* )
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* // 嵌套服务提供者
|
|
118
|
+
* function UserPage() {
|
|
119
|
+
* return (
|
|
120
|
+
* <ServiceProvider services={[LocalUserService]}>
|
|
121
|
+
* <UserList />
|
|
122
|
+
* </ServiceProvider>
|
|
123
|
+
* )
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
/** 自动复用 Container,除非没有,否则不会重新创建 Container */
|
|
128
|
+
// di 实现也有限制 https://needle-di.io/advanced/child-containers.html
|
|
129
|
+
export function ServiceProvider(props) {
|
|
130
|
+
const parentContainer = useContext(ctx);
|
|
131
|
+
const { children, services } = props;
|
|
132
|
+
// 创建子容器,复用父容器的服务配置
|
|
133
|
+
const container = useMemo(() => {
|
|
134
|
+
const currentContainer = new Container(parentContainer);
|
|
135
|
+
const typed = services;
|
|
136
|
+
currentContainer.bindAll(...typed); // 批量绑定服务到容器
|
|
137
|
+
// bindAll 要求参数必须是元组类型
|
|
138
|
+
return currentContainer;
|
|
139
|
+
}, [parentContainer, services]);
|
|
140
|
+
// 使用 React.createElement 避免将文件改为 .tsx
|
|
141
|
+
const init = React.createElement(InitService, { services, children });
|
|
142
|
+
return React.createElement(ctx.Provider, { value: container, children: container ? init : null });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 服务初始化组件
|
|
146
|
+
*
|
|
147
|
+
* 该组件负责自动初始化实现了 Service 接口的服务实例。
|
|
148
|
+
* 它会在组件挂载时检查每个服务是否需要初始化,并调用其 init 方法。
|
|
149
|
+
*
|
|
150
|
+
* 初始化流程:
|
|
151
|
+
* 1. 从容器中获取服务实例
|
|
152
|
+
* 2. 检查服务是否实现了 Service 接口(有 init 方法)
|
|
153
|
+
* 3. 检查服务是否已经初始化(inited 属性)
|
|
154
|
+
* 4. 如果未初始化,则调用 init 方法并更新状态
|
|
155
|
+
*
|
|
156
|
+
* @param props 组件属性,与 ServiceProviderProps 相同
|
|
157
|
+
*/
|
|
158
|
+
function InitService(props) {
|
|
159
|
+
const { services, children } = props;
|
|
160
|
+
const container = useContext(ctx);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (container == null)
|
|
163
|
+
return;
|
|
164
|
+
if (services.length == 0)
|
|
165
|
+
return;
|
|
166
|
+
// 异步初始化所有服务
|
|
167
|
+
(async () => {
|
|
168
|
+
const initPromises = services.map(async (service) => {
|
|
169
|
+
// 尝试从容器中获取服务实例(可选的,避免未注册时报错)
|
|
170
|
+
const instance = container.get(service, { optional: true });
|
|
171
|
+
if (instance != null && typeof instance === 'object') {
|
|
172
|
+
// 检查实例是否实现了 Service 接口
|
|
173
|
+
if ('init' in instance && typeof instance.init === 'function') {
|
|
174
|
+
// 检查是否已经初始化
|
|
175
|
+
if (instance.inited == false) {
|
|
176
|
+
// 安全地调用 init 方法,捕获可能的异常
|
|
177
|
+
const result = await catchIt(() => instance.init());
|
|
178
|
+
// 使用 runInAction 确保状态更新被 Mobx 正确跟踪
|
|
179
|
+
runInAction(() => instance.inited = !result.isError());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// 等待所有服务初始化完成
|
|
185
|
+
await Promise.all(initPromises);
|
|
186
|
+
})();
|
|
187
|
+
}, [container, services]);
|
|
188
|
+
return children;
|
|
189
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../source/service/service.test.tsx"],"names":[],"mappings":""}
|