@taicode/common-web 1.1.4 → 1.1.6

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.
@@ -37,22 +37,34 @@ import React from 'react';
37
37
  /**
38
38
  * 使用全局注册的服务 Hook
39
39
  *
40
- * 该 Hook 从 DI 容器中获取指定的服务实例,并通过 selector 函数选择需要的数据。
40
+ * 该 Hook 从 DI 容器中获取指定的服务实例,支持可选的 selector 函数来选择需要的数据。
41
41
  * 当选择的数据发生变化时,组件会自动重新渲染。
42
42
  *
43
+ * 特性:
44
+ * - 从全局容器获取服务实例
45
+ * - 支持 Mobx 响应式更新
46
+ * - 支持可选 selector,不传时监听整个服务的所有变化
47
+ * - 智能检测 selector 返回值,优化响应式监听策略
48
+ *
43
49
  * @template T 服务类型
44
50
  * @template U 选择器返回的数据类型
45
51
  * @param target 服务的 Token,用于从容器中获取服务实例
46
- * @param selector 选择器函数,从服务实例中选择需要的数据
47
- * @returns 选择器函数的返回值
52
+ * @param selector 可选的选择器函数。如果不传入,返回服务实例并监听所有 observable 属性变化
53
+ * @returns 如果传入了 selector,返回选择器的结果;否则返回服务实例
48
54
  *
49
55
  * @throws {Error} 当组件不在 ServiceProvider 内部时抛出错误
50
56
  *
51
57
  * @example
52
58
  * ```tsx
53
- * // 获取用户列表
59
+ * // 使用选择器,只监听特定属性
54
60
  * const users = useService(UserService, service => service.users)
55
61
  *
62
+ * // 不使用选择器,监听整个服务的所有变化
63
+ * const userService = useService(UserService)
64
+ *
65
+ * // 选择器返回服务实例本身(智能检测,自动优化响应式监听)
66
+ * const service = useService(UserService, s => s)
67
+ *
56
68
  * // 获取当前用户信息
57
69
  * const currentUser = useService(UserService, service => service.currentUser)
58
70
  *
@@ -60,7 +72,83 @@ import React from 'react';
60
72
  * const userCount = useService(UserService, service => service.users.length)
61
73
  * ```
62
74
  */
63
- export declare function useService<T, U>(target: Token<T>, selector: (v: T) => U): U;
75
+ export declare function useService<T>(target: Token<T>): T;
76
+ export declare function useService<T, U>(target: Token<T>, selector: (service: T) => U): U;
77
+ /**
78
+ * 使用局部服务实例 Hook
79
+ *
80
+ * 该 Hook 创建指定服务的局部实例,既能注入全局依赖,又不会污染全局上下文。
81
+ * 局部实例在 Hook 的整个生命周期内保持引用稳定,支持自动初始化和响应式更新。
82
+ *
83
+ * 特性:
84
+ * - 创建局部服务实例,避免全局状态污染
85
+ * - 继承全局容器的所有依赖,支持依赖注入
86
+ * - 在组件重渲染时保持实例引用稳定
87
+ * - 自动初始化实现了 Service 接口的服务
88
+ * - 支持 Mobx 响应式更新
89
+ * - 支持可选 selector,不传时监听整个服务的所有变化
90
+ *
91
+ * @template T 服务类型
92
+ * @template U 选择器返回的数据类型
93
+ * @param ServiceClass 服务构造函数/类
94
+ * @param selector 可选的选择器函数。如果不传入,返回服务实例并监听所有 observable 属性变化
95
+ * @returns 如果传入了 selector,返回选择器的结果;否则返回服务实例
96
+ *
97
+ * @throws {Error} 当组件不在 ServiceProvider 内部时抛出错误
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * // 使用选择器,只监听特定属性
102
+ * const localUsers = useLocalService(LocalUserService, service => service.users)
103
+ *
104
+ * // 不使用选择器,监听整个服务的所有变化
105
+ * const service = useLocalService(LocalUserService)
106
+ *
107
+ * // 局部服务可以注入全局服务
108
+ * class LocalUserService extends Service {
109
+ * constructor(
110
+ * @inject(ApiService) private apiService: ApiService,
111
+ * @inject(ConfigService) private configService: ConfigService
112
+ * ) {
113
+ * super()
114
+ * }
115
+ *
116
+ * @observable users: User[] = []
117
+ *
118
+ * async init() {
119
+ * this.users = await this.apiService.getUsers()
120
+ * return true
121
+ * }
122
+ * }
123
+ *
124
+ * // 使用局部服务
125
+ * function UserComponent() {
126
+ * const users = useLocalService(LocalUserService, s => s.users)
127
+ * const userCount = useLocalService(LocalUserService, s => s.users.length)
128
+ *
129
+ * return (
130
+ * <div>
131
+ * <p>用户数量: {userCount}</p>
132
+ * {users.map(user => <div key={user.id}>{user.name}</div>)}
133
+ * </div>
134
+ * )
135
+ * }
136
+ *
137
+ * // 不使用选择器的例子
138
+ * function SimpleUserComponent() {
139
+ * const service = useLocalService(LocalUserService)
140
+ *
141
+ * return (
142
+ * <div>
143
+ * <p>用户数量: {service.users.length}</p>
144
+ * {service.users.map(user => <div key={user.id}>{user.name}</div>)}
145
+ * </div>
146
+ * )
147
+ * }
148
+ * ```
149
+ */
150
+ export declare function useLocalService<T>(ServiceClass: new (...args: any[]) => T): T;
151
+ export declare function useLocalService<T, U>(ServiceClass: new (...args: any[]) => T, selector: (service: T) => U): U;
64
152
  /**
65
153
  * ServiceProvider 组件的属性接口
66
154
  */
@@ -1 +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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAClD,wBAAgB,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAgDlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;AAC9E,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAoG9G;;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"}
@@ -35,39 +35,12 @@
35
35
  import { reaction, runInAction } from 'mobx';
36
36
  import { catchIt } from '@taicode/common-base';
37
37
  import { Container } from '@needle-di/core';
38
- import React, { useContext, useEffect, useMemo, useState } from 'react';
38
+ import React, { useContext, useEffect, useMemo, useState, useRef } from 'react';
39
39
  /**
40
40
  * React Context 用于传递 DI 容器实例
41
41
  * 通过上下文在组件树中共享依赖注入容器
42
42
  */
43
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
44
  export function useService(target, selector) {
72
45
  const container = useContext(ctx);
73
46
  // 使用 useState 强制组件重新渲染
@@ -78,13 +51,110 @@ export function useService(target, selector) {
78
51
  // 从容器中获取服务实例
79
52
  const service = container.get(target);
80
53
  // 设置 Mobx reaction,当 selector 返回的数据变化时触发重新渲染
81
- return reaction(() => selector(service), () => refresh({}) // 触发组件重新渲染
54
+ const dispose = reaction(() => {
55
+ // 如果没有提供 selector,使用默认策略监听整个服务的变化
56
+ if (!selector) {
57
+ // 直接访问服务实例,让 Mobx 自动收集依赖
58
+ return service;
59
+ }
60
+ // 执行 selector 获取结果
61
+ const result = selector(service);
62
+ // 智能检测:如果 selector 返回的就是服务实例本身,
63
+ // 则切换到监听所有属性变化的模式
64
+ if (result === service) {
65
+ // 通过访问服务实例让 Mobx 收集所有 observable 属性的依赖
66
+ return service;
67
+ }
68
+ return result;
69
+ }, () => refresh({}) // 触发组件重新渲染
82
70
  );
71
+ return dispose;
83
72
  }, [container, selector, target]);
84
73
  if (container == null) {
85
74
  throw new Error('Must be a child of ServiceProvider.');
86
75
  }
87
- return selector(container.get(target));
76
+ // 返回选择器结果或服务实例
77
+ const service = container.get(target);
78
+ return selector ? selector(service) : service;
79
+ }
80
+ export function useLocalService(ServiceClass, selector) {
81
+ const parentContainer = useContext(ctx);
82
+ // 使用 useRef 保持局部容器和服务实例的引用稳定
83
+ const localContainerRef = useRef(null);
84
+ const serviceInstanceRef = useRef(null);
85
+ // 使用 useState 强制组件重新渲染,完全模仿 useService
86
+ const [, refresh] = useState({});
87
+ if (parentContainer == null) {
88
+ throw new Error('Must be a child of ServiceProvider.');
89
+ }
90
+ // 确保服务实例已创建
91
+ if (serviceInstanceRef.current == null) {
92
+ // 创建继承父容器的子容器
93
+ localContainerRef.current = new Container(parentContainer);
94
+ // 将服务类绑定到局部容器
95
+ localContainerRef.current.bindAll(ServiceClass);
96
+ // 获取服务实例
97
+ serviceInstanceRef.current = localContainerRef.current.get(ServiceClass);
98
+ }
99
+ // 模仿 useService 的结构:在 useEffect 中设置 reaction 和初始化
100
+ React.useEffect(() => {
101
+ if (parentContainer == null)
102
+ return;
103
+ const service = serviceInstanceRef.current;
104
+ // 设置 Mobx reaction,当 selector 返回的数据变化时触发重新渲染
105
+ const dispose = reaction(() => {
106
+ // 如果没有提供 selector,使用默认策略监听整个服务的变化
107
+ if (!selector) {
108
+ // 直接访问服务实例,让 Mobx 自动收集依赖
109
+ return service;
110
+ }
111
+ // 执行 selector 获取结果
112
+ const result = selector(service);
113
+ // 智能检测:如果 selector 返回的就是服务实例本身,
114
+ // 则切换到监听所有属性变化的模式
115
+ if (result === service) {
116
+ // 通过访问服务实例让 Mobx 收集所有 observable 属性的依赖
117
+ return service;
118
+ }
119
+ return result;
120
+ }, () => refresh({}) // 触发组件重新渲染
121
+ );
122
+ return dispose;
123
+ }, [parentContainer, selector, ServiceClass]);
124
+ // 单独的 useEffect 处理服务初始化,避免与 selector 变化的冲突
125
+ React.useEffect(() => {
126
+ if (parentContainer == null)
127
+ return;
128
+ const service = serviceInstanceRef.current;
129
+ // 自动初始化服务(如果实现了 Service 接口)
130
+ if (service != null &&
131
+ typeof service === 'object' &&
132
+ 'init' in service &&
133
+ typeof service.init === 'function') {
134
+ // 直接调用 init 方法
135
+ const initResult = service.init();
136
+ if (initResult && typeof initResult.then === 'function') {
137
+ // 异步初始化
138
+ initResult.then((result) => {
139
+ runInAction(() => {
140
+ service.inited = result;
141
+ });
142
+ }).catch((error) => {
143
+ runInAction(() => {
144
+ service.inited = false;
145
+ });
146
+ });
147
+ }
148
+ else {
149
+ // 同步初始化
150
+ runInAction(() => {
151
+ service.inited = true;
152
+ });
153
+ }
154
+ }
155
+ }, [parentContainer, ServiceClass]); // 注意:这里不依赖 selector
156
+ // 返回选择器结果或服务实例
157
+ return selector ? selector(serviceInstanceRef.current) : serviceInstanceRef.current;
88
158
  }
89
159
  /**
90
160
  * 服务提供者组件
@@ -49,7 +49,7 @@ import { Service } from '@taicode/common-base';
49
49
  import { describe, it, expect, vi, beforeEach } from 'vitest';
50
50
  import { render, renderHook, waitFor } from '@testing-library/react';
51
51
  import { observable, makeObservable, runInAction, action } from 'mobx';
52
- import { useService, ServiceProvider } from './service';
52
+ import { useService, useLocalService, ServiceProvider } from './service';
53
53
  // 创建测试用的服务类
54
54
  let TestService = (() => {
55
55
  var _a, _TestService_count_accessor_storage, _TestService_users_accessor_storage, _TestService_inited_accessor_storage;
@@ -169,6 +169,143 @@ let SimpleService = (() => {
169
169
  })(),
170
170
  _a;
171
171
  })();
172
+ // 局部服务测试类
173
+ let LocalService = (() => {
174
+ var _a, _LocalService_data_accessor_storage, _LocalService_counter_accessor_storage, _LocalService_inited_accessor_storage;
175
+ let _classSuper = Service;
176
+ let _instanceExtraInitializers = [];
177
+ let _data_decorators;
178
+ let _data_initializers = [];
179
+ let _data_extraInitializers = [];
180
+ let _counter_decorators;
181
+ let _counter_initializers = [];
182
+ let _counter_extraInitializers = [];
183
+ let _inited_decorators;
184
+ let _inited_initializers = [];
185
+ let _inited_extraInitializers = [];
186
+ let _increment_decorators;
187
+ let _setData_decorators;
188
+ return _a = class LocalService extends _classSuper {
189
+ get data() { return __classPrivateFieldGet(this, _LocalService_data_accessor_storage, "f"); }
190
+ set data(value) { __classPrivateFieldSet(this, _LocalService_data_accessor_storage, value, "f"); }
191
+ get counter() { return __classPrivateFieldGet(this, _LocalService_counter_accessor_storage, "f"); }
192
+ set counter(value) { __classPrivateFieldSet(this, _LocalService_counter_accessor_storage, value, "f"); }
193
+ get inited() { return __classPrivateFieldGet(this, _LocalService_inited_accessor_storage, "f"); }
194
+ set inited(value) { __classPrivateFieldSet(this, _LocalService_inited_accessor_storage, value, "f"); }
195
+ constructor() {
196
+ super();
197
+ _LocalService_data_accessor_storage.set(this, (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _data_initializers, 'local')));
198
+ _LocalService_counter_accessor_storage.set(this, (__runInitializers(this, _data_extraInitializers), __runInitializers(this, _counter_initializers, 0)));
199
+ _LocalService_inited_accessor_storage.set(this, (__runInitializers(this, _counter_extraInitializers), __runInitializers(this, _inited_initializers, false)));
200
+ __runInitializers(this, _inited_extraInitializers);
201
+ makeObservable(this);
202
+ }
203
+ async init() {
204
+ runInAction(() => {
205
+ this.data = 'initialized';
206
+ });
207
+ return true;
208
+ }
209
+ increment() {
210
+ this.counter++;
211
+ }
212
+ setData(newData) {
213
+ this.data = newData;
214
+ }
215
+ },
216
+ _LocalService_data_accessor_storage = new WeakMap(),
217
+ _LocalService_counter_accessor_storage = new WeakMap(),
218
+ _LocalService_inited_accessor_storage = new WeakMap(),
219
+ (() => {
220
+ var _b;
221
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
222
+ _data_decorators = [observable];
223
+ _counter_decorators = [observable];
224
+ _inited_decorators = [observable];
225
+ _increment_decorators = [action];
226
+ _setData_decorators = [action];
227
+ __esDecorate(_a, null, _data_decorators, { kind: "accessor", name: "data", static: false, private: false, access: { has: obj => "data" in obj, get: obj => obj.data, set: (obj, value) => { obj.data = value; } }, metadata: _metadata }, _data_initializers, _data_extraInitializers);
228
+ __esDecorate(_a, null, _counter_decorators, { kind: "accessor", name: "counter", static: false, private: false, access: { has: obj => "counter" in obj, get: obj => obj.counter, set: (obj, value) => { obj.counter = value; } }, metadata: _metadata }, _counter_initializers, _counter_extraInitializers);
229
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
230
+ __esDecorate(_a, null, _increment_decorators, { kind: "method", name: "increment", static: false, private: false, access: { has: obj => "increment" in obj, get: obj => obj.increment }, metadata: _metadata }, null, _instanceExtraInitializers);
231
+ __esDecorate(_a, null, _setData_decorators, { kind: "method", name: "setData", static: false, private: false, access: { has: obj => "setData" in obj, get: obj => obj.setData }, metadata: _metadata }, null, _instanceExtraInitializers);
232
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
233
+ })(),
234
+ _a;
235
+ })();
236
+ // 依赖注入测试服务类 - 简化版本,不使用装饰器
237
+ let DependentService = (() => {
238
+ var _a, _DependentService_combinedData_accessor_storage, _DependentService_inited_accessor_storage;
239
+ let _classSuper = Service;
240
+ let _combinedData_decorators;
241
+ let _combinedData_initializers = [];
242
+ let _combinedData_extraInitializers = [];
243
+ let _inited_decorators;
244
+ let _inited_initializers = [];
245
+ let _inited_extraInitializers = [];
246
+ return _a = class DependentService extends _classSuper {
247
+ get combinedData() { return __classPrivateFieldGet(this, _DependentService_combinedData_accessor_storage, "f"); }
248
+ set combinedData(value) { __classPrivateFieldSet(this, _DependentService_combinedData_accessor_storage, value, "f"); }
249
+ get inited() { return __classPrivateFieldGet(this, _DependentService_inited_accessor_storage, "f"); }
250
+ set inited(value) { __classPrivateFieldSet(this, _DependentService_inited_accessor_storage, value, "f"); }
251
+ constructor() {
252
+ super();
253
+ _DependentService_combinedData_accessor_storage.set(this, __runInitializers(this, _combinedData_initializers, ''));
254
+ _DependentService_inited_accessor_storage.set(this, (__runInitializers(this, _combinedData_extraInitializers), __runInitializers(this, _inited_initializers, false)));
255
+ __runInitializers(this, _inited_extraInitializers);
256
+ makeObservable(this);
257
+ }
258
+ async init() {
259
+ runInAction(() => {
260
+ this.combinedData = `dependent-simple`;
261
+ });
262
+ return true;
263
+ }
264
+ },
265
+ _DependentService_combinedData_accessor_storage = new WeakMap(),
266
+ _DependentService_inited_accessor_storage = new WeakMap(),
267
+ (() => {
268
+ var _b;
269
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
270
+ _combinedData_decorators = [observable];
271
+ _inited_decorators = [observable];
272
+ __esDecorate(_a, null, _combinedData_decorators, { kind: "accessor", name: "combinedData", static: false, private: false, access: { has: obj => "combinedData" in obj, get: obj => obj.combinedData, set: (obj, value) => { obj.combinedData = value; } }, metadata: _metadata }, _combinedData_initializers, _combinedData_extraInitializers);
273
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
274
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
275
+ })(),
276
+ _a;
277
+ })();
278
+ // 普通类(不继承 Service)
279
+ let PlainClass = (() => {
280
+ var _a, _PlainClass_value_accessor_storage;
281
+ let _instanceExtraInitializers = [];
282
+ let _value_decorators;
283
+ let _value_initializers = [];
284
+ let _value_extraInitializers = [];
285
+ let _updateValue_decorators;
286
+ return _a = class PlainClass {
287
+ get value() { return __classPrivateFieldGet(this, _PlainClass_value_accessor_storage, "f"); }
288
+ set value(value) { __classPrivateFieldSet(this, _PlainClass_value_accessor_storage, value, "f"); }
289
+ constructor() {
290
+ _PlainClass_value_accessor_storage.set(this, (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _value_initializers, 'plain')));
291
+ __runInitializers(this, _value_extraInitializers);
292
+ makeObservable(this);
293
+ }
294
+ updateValue() {
295
+ this.value = 'updated';
296
+ }
297
+ },
298
+ _PlainClass_value_accessor_storage = new WeakMap(),
299
+ (() => {
300
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
301
+ _value_decorators = [observable];
302
+ _updateValue_decorators = [action];
303
+ __esDecorate(_a, null, _value_decorators, { kind: "accessor", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, _value_initializers, _value_extraInitializers);
304
+ __esDecorate(_a, null, _updateValue_decorators, { kind: "method", name: "updateValue", static: false, private: false, access: { has: obj => "updateValue" in obj, get: obj => obj.updateValue }, metadata: _metadata }, null, _instanceExtraInitializers);
305
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
306
+ })(),
307
+ _a;
308
+ })();
172
309
  // 创建测试用的 Provider 组件
173
310
  function createTestProvider(services) {
174
311
  return function TestProvider({ children }) {
@@ -355,4 +492,344 @@ describe('service integration', () => {
355
492
  expect(renderCount).toBeGreaterThanOrEqual(initialRenderCount);
356
493
  });
357
494
  });
495
+ describe('useLocalService', () => {
496
+ it('应该能够创建局部服务实例', async () => {
497
+ const TestProvider = createTestProvider([TestService]);
498
+ const { result } = renderHook(() => useLocalService(LocalService, service => service.data), { wrapper: TestProvider });
499
+ // 初始状态应该是已经初始化的,因为初始化是同步的
500
+ expect(result.current).toBe('initialized');
501
+ });
502
+ it('应该在没有 ServiceProvider 时抛出错误', () => {
503
+ expect(() => {
504
+ renderHook(() => useLocalService(LocalService, service => service.data));
505
+ }).toThrow('Must be a child of ServiceProvider.');
506
+ });
507
+ it('应该自动初始化局部服务实例', async () => {
508
+ const TestProvider = createTestProvider([TestService]);
509
+ const { result } = renderHook(() => useLocalService(LocalService, service => ({
510
+ data: service.data,
511
+ inited: service.inited
512
+ })), { wrapper: TestProvider });
513
+ // 等待初始化完成
514
+ await waitFor(() => {
515
+ expect(result.current.inited).toBe(true);
516
+ expect(result.current.data).toBe('initialized');
517
+ });
518
+ });
519
+ it('应该在服务数据变化时触发重新渲染', async () => {
520
+ const TestProvider = createTestProvider([TestService]);
521
+ const { result } = renderHook(() => useLocalService(LocalService, service => ({
522
+ counter: service.counter,
523
+ service
524
+ })), { wrapper: TestProvider });
525
+ expect(result.current.counter).toBe(0);
526
+ // 修改服务数据
527
+ act(() => {
528
+ result.current.service.increment();
529
+ });
530
+ await waitFor(() => {
531
+ expect(result.current.counter).toBe(1);
532
+ });
533
+ });
534
+ it('应该在组件重渲染时保持服务实例引用稳定', () => {
535
+ const TestProvider = createTestProvider([TestService]);
536
+ const { result, rerender } = renderHook(() => useLocalService(LocalService, service => service), { wrapper: TestProvider });
537
+ const firstInstance = result.current;
538
+ rerender();
539
+ const secondInstance = result.current;
540
+ expect(firstInstance).toBe(secondInstance);
541
+ });
542
+ it('应该为不同的 hook 调用创建不同的服务实例', () => {
543
+ const TestProvider = createTestProvider([TestService]);
544
+ const { result } = renderHook(() => ({
545
+ service1: useLocalService(LocalService, service => service),
546
+ service2: useLocalService(LocalService, service => service)
547
+ }), { wrapper: TestProvider });
548
+ expect(result.current.service1).not.toBe(result.current.service2);
549
+ });
550
+ it('应该支持依赖注入全局服务', async () => {
551
+ const TestProvider = createTestProvider([TestService]);
552
+ const { result } = renderHook(() => ({
553
+ localService: useLocalService(DependentService, service => service),
554
+ globalService: useService(TestService, service => service)
555
+ }), { wrapper: TestProvider });
556
+ // 修改全局服务的数据
557
+ act(() => {
558
+ result.current.globalService.increment();
559
+ result.current.globalService.increment();
560
+ });
561
+ // 等待局部服务初始化完成
562
+ await waitFor(() => {
563
+ expect(result.current.localService.inited).toBe(true);
564
+ expect(result.current.localService.combinedData).toBe('dependent-simple');
565
+ }, { timeout: 3000 });
566
+ });
567
+ it('应该能够处理复杂的选择器', async () => {
568
+ const TestProvider = createTestProvider([TestService]);
569
+ let serviceRef = null;
570
+ const { result } = renderHook(() => useLocalService(LocalService, service => {
571
+ serviceRef = service; // 保存服务引用
572
+ return {
573
+ hasData: service.data.length > 0,
574
+ upperData: service.data.toUpperCase(),
575
+ rawData: service.data, // 添加原始数据用于调试
576
+ };
577
+ }), { wrapper: TestProvider });
578
+ await waitFor(() => {
579
+ expect(result.current.hasData).toBe(true);
580
+ expect(result.current.upperData).toBe('INITIALIZED');
581
+ });
582
+ // 修改数据 - 使用选择器内部捕获的服务实例
583
+ act(() => {
584
+ serviceRef.setData('modified');
585
+ });
586
+ await waitFor(() => {
587
+ expect(result.current.upperData).toBe('MODIFIED');
588
+ });
589
+ });
590
+ it('应该能够处理选择器返回 undefined 的情况', () => {
591
+ const TestProvider = createTestProvider([TestService]);
592
+ const { result } = renderHook(() => useLocalService(LocalService, () => undefined), { wrapper: TestProvider });
593
+ expect(result.current).toBeUndefined();
594
+ });
595
+ it('应该能够处理局部服务初始化失败的情况', async () => {
596
+ // 创建一个初始化会失败的局部服务
597
+ let FailingLocalService = (() => {
598
+ var _a, _FailingLocalService_inited_accessor_storage;
599
+ let _classSuper = Service;
600
+ let _inited_decorators;
601
+ let _inited_initializers = [];
602
+ let _inited_extraInitializers = [];
603
+ return _a = class FailingLocalService extends _classSuper {
604
+ get inited() { return __classPrivateFieldGet(this, _FailingLocalService_inited_accessor_storage, "f"); }
605
+ set inited(value) { __classPrivateFieldSet(this, _FailingLocalService_inited_accessor_storage, value, "f"); }
606
+ constructor() {
607
+ super();
608
+ _FailingLocalService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
609
+ __runInitializers(this, _inited_extraInitializers);
610
+ makeObservable(this);
611
+ }
612
+ async init() {
613
+ throw new Error('Init failed');
614
+ }
615
+ },
616
+ _FailingLocalService_inited_accessor_storage = new WeakMap(),
617
+ (() => {
618
+ var _b;
619
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
620
+ _inited_decorators = [observable];
621
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
622
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
623
+ })(),
624
+ _a;
625
+ })();
626
+ const TestProvider = createTestProvider([TestService]);
627
+ const { result } = renderHook(() => useLocalService(FailingLocalService, service => service.inited), { wrapper: TestProvider });
628
+ // 等待初始化处理完成,即使失败也应该保持 inited 为 false
629
+ await waitFor(() => {
630
+ expect(result.current).toBe(false);
631
+ });
632
+ });
633
+ it('应该能够处理没有实现 Service 接口的局部服务', async () => {
634
+ const TestProvider = createTestProvider([TestService]);
635
+ const { result } = renderHook(() => useLocalService(PlainClass, service => ({
636
+ value: service.value,
637
+ service
638
+ })), { wrapper: TestProvider });
639
+ expect(result.current.value).toBe('plain');
640
+ // 修改数据
641
+ act(() => {
642
+ result.current.service.updateValue();
643
+ });
644
+ await waitFor(() => {
645
+ expect(result.current.value).toBe('updated');
646
+ });
647
+ });
648
+ it('应该在选择器函数变化时正确更新订阅', async () => {
649
+ const TestProvider = createTestProvider([TestService]);
650
+ let useData = true;
651
+ const { result, rerender } = renderHook(() => useLocalService(LocalService, service => ({
652
+ selected: useData ? service.data : service.counter,
653
+ service
654
+ })), { wrapper: TestProvider });
655
+ await waitFor(() => {
656
+ expect(result.current.selected).toBe('initialized');
657
+ });
658
+ // 修改 selector
659
+ useData = false;
660
+ rerender();
661
+ await waitFor(() => {
662
+ expect(result.current.selected).toBe(0);
663
+ });
664
+ // 修改计数器
665
+ act(() => {
666
+ result.current.service.increment();
667
+ });
668
+ await waitFor(() => {
669
+ expect(result.current.selected).toBe(1);
670
+ });
671
+ });
672
+ it('不应该产生全局状态的干扰', async () => {
673
+ const TestProvider = createTestProvider([TestService]);
674
+ let globalServiceRef = null;
675
+ let localServiceRef1 = null;
676
+ let localServiceRef2 = null;
677
+ // 第一个组件:使用全局服务和局部服务
678
+ const { result: result1 } = renderHook(() => ({
679
+ globalService: useService(TestService, service => {
680
+ globalServiceRef = service;
681
+ return service;
682
+ }),
683
+ localService: useLocalService(LocalService, service => {
684
+ localServiceRef1 = service;
685
+ return service;
686
+ })
687
+ }), { wrapper: TestProvider });
688
+ // 第二个组件:使用另一个局部服务实例
689
+ const { result: result2 } = renderHook(() => ({
690
+ localService: useLocalService(LocalService, service => {
691
+ localServiceRef2 = service;
692
+ return service;
693
+ })
694
+ }), { wrapper: TestProvider });
695
+ // 等待初始化完成
696
+ await waitFor(() => {
697
+ expect(result1.current.globalService.inited).toBe(true);
698
+ expect(result1.current.localService.inited).toBe(true);
699
+ expect(result2.current.localService.inited).toBe(true);
700
+ });
701
+ // 验证基本状态
702
+ expect(globalServiceRef).not.toBeNull();
703
+ expect(localServiceRef1).not.toBeNull();
704
+ expect(localServiceRef2).not.toBeNull();
705
+ // 验证局部服务实例是不同的
706
+ expect(localServiceRef1).not.toBe(localServiceRef2);
707
+ // 验证局部服务与全局服务是不同的
708
+ expect(localServiceRef1).not.toBe(globalServiceRef);
709
+ expect(localServiceRef2).not.toBe(globalServiceRef);
710
+ // 修改第一个局部服务的状态
711
+ act(() => {
712
+ localServiceRef1.increment();
713
+ localServiceRef1.setData('local1-changed');
714
+ });
715
+ await waitFor(() => {
716
+ expect(result1.current.localService.counter).toBe(1);
717
+ expect(result1.current.localService.data).toBe('local1-changed');
718
+ });
719
+ // 验证其他服务状态未受影响
720
+ expect(result2.current.localService.counter).toBe(0);
721
+ expect(result2.current.localService.data).toBe('initialized');
722
+ expect(result1.current.globalService.count).toBe(0);
723
+ // 修改第二个局部服务的状态
724
+ act(() => {
725
+ localServiceRef2.increment();
726
+ localServiceRef2.increment();
727
+ localServiceRef2.setData('local2-changed');
728
+ });
729
+ await waitFor(() => {
730
+ expect(result2.current.localService.counter).toBe(2);
731
+ expect(result2.current.localService.data).toBe('local2-changed');
732
+ });
733
+ // 验证其他服务状态仍未受影响
734
+ expect(result1.current.localService.counter).toBe(1);
735
+ expect(result1.current.localService.data).toBe('local1-changed');
736
+ expect(result1.current.globalService.count).toBe(0);
737
+ // 修改全局服务状态
738
+ act(() => {
739
+ globalServiceRef.increment();
740
+ globalServiceRef.addUser('Charlie');
741
+ });
742
+ await waitFor(() => {
743
+ expect(result1.current.globalService.count).toBe(1);
744
+ expect(result1.current.globalService.users).toEqual(['Alice', 'Bob', 'Charlie']);
745
+ });
746
+ // 验证局部服务状态未受全局服务影响
747
+ expect(result1.current.localService.counter).toBe(1);
748
+ expect(result1.current.localService.data).toBe('local1-changed');
749
+ expect(result2.current.localService.counter).toBe(2);
750
+ expect(result2.current.localService.data).toBe('local2-changed');
751
+ // 最终验证:每个服务实例都保持独立的状态
752
+ expect(result1.current.globalService.count).toBe(1);
753
+ expect(result1.current.globalService.users.length).toBe(3);
754
+ expect(result1.current.localService.counter).toBe(1);
755
+ expect(result1.current.localService.data).toBe('local1-changed');
756
+ expect(result2.current.localService.counter).toBe(2);
757
+ expect(result2.current.localService.data).toBe('local2-changed');
758
+ });
759
+ });
760
+ describe('useLocalService 可选 selector 功能', () => {
761
+ it('应该在不传入 selector 时返回完整的服务实例', async () => {
762
+ const TestProvider = createTestProvider([TestService]);
763
+ const { result } = renderHook(() => useLocalService(LocalService), { wrapper: TestProvider });
764
+ await waitFor(() => {
765
+ expect(result.current.inited).toBe(true);
766
+ });
767
+ expect(result.current.data).toBe('initialized');
768
+ expect(result.current.counter).toBe(0);
769
+ expect(typeof result.current.increment).toBe('function');
770
+ });
771
+ it('应该在不传入 selector 时响应任意属性的变化', async () => {
772
+ const TestProvider = createTestProvider([TestService]);
773
+ const { result } = renderHook(() => useLocalService(LocalService), { wrapper: TestProvider });
774
+ await waitFor(() => {
775
+ expect(result.current.inited).toBe(true);
776
+ });
777
+ // 初始值
778
+ expect(result.current.data).toBe('initialized');
779
+ expect(result.current.counter).toBe(0);
780
+ // 修改计数器
781
+ act(() => {
782
+ result.current.increment();
783
+ });
784
+ await waitFor(() => {
785
+ expect(result.current.counter).toBe(1);
786
+ });
787
+ // 修改数据
788
+ act(() => {
789
+ result.current.setData('updated');
790
+ });
791
+ await waitFor(() => {
792
+ expect(result.current.data).toBe('updated');
793
+ });
794
+ });
795
+ it('应该支持函数重载,有 selector 时返回选择器结果', async () => {
796
+ const TestProvider = createTestProvider([TestService]);
797
+ const { result } = renderHook(() => ({
798
+ service: useLocalService(LocalService),
799
+ counter: useLocalService(LocalService, s => s.counter),
800
+ data: useLocalService(LocalService, s => s.data)
801
+ }), { wrapper: TestProvider });
802
+ await waitFor(() => {
803
+ expect(result.current.service.inited).toBe(true);
804
+ });
805
+ // 类型验证:service 应该是完整的服务实例
806
+ expect(typeof result.current.service.increment).toBe('function');
807
+ expect(typeof result.current.service.setData).toBe('function');
808
+ // 类型验证:counter 应该是 number 类型
809
+ expect(typeof result.current.counter).toBe('number');
810
+ // 类型验证:data 应该是 string 类型
811
+ expect(typeof result.current.data).toBe('string');
812
+ expect(result.current.counter).toBe(0);
813
+ expect(result.current.data).toBe('initialized');
814
+ });
815
+ it('应该在不传入 selector 时支持多个属性同时变化的响应', async () => {
816
+ const TestProvider = createTestProvider([TestService]);
817
+ const { result } = renderHook(() => useLocalService(LocalService), { wrapper: TestProvider });
818
+ await waitFor(() => {
819
+ expect(result.current.inited).toBe(true);
820
+ });
821
+ // 初始状态
822
+ expect(result.current.counter).toBe(0);
823
+ expect(result.current.data).toBe('initialized');
824
+ // 同时修改多个属性
825
+ act(() => {
826
+ result.current.increment();
827
+ result.current.setData('changed');
828
+ });
829
+ await waitFor(() => {
830
+ expect(result.current.counter).toBe(1);
831
+ expect(result.current.data).toBe('changed');
832
+ });
833
+ });
834
+ });
358
835
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taicode/common-web",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "author": "Alain",
5
5
  "license": "ISC",
6
6
  "description": "",
@@ -9,6 +9,11 @@
9
9
  "types": "./output/*/index.d.ts",
10
10
  "import": "./output/*/index.js",
11
11
  "require": "./output/*/index.js"
12
+ },
13
+ "./catalyst/*": {
14
+ "types": "./output/catalyst/*.d.ts",
15
+ "import": "./output/catalyst/*.js",
16
+ "require": "./output/catalyst/*.js"
12
17
  }
13
18
  },
14
19
  "files": [