@taicode/common-web 1.1.5 → 1.1.7

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;AA+D/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;AAyF9G;;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"}
@@ -33,41 +33,57 @@
33
33
  * ```
34
34
  */
35
35
  import { reaction, runInAction } from 'mobx';
36
- import { catchIt } from '@taicode/common-base';
37
36
  import { Container } from '@needle-di/core';
38
- import React, { useContext, useEffect, useMemo, useState } from 'react';
37
+ import React, { useContext, useEffect, useMemo, useState, useRef } from 'react';
39
38
  /**
40
39
  * React Context 用于传递 DI 容器实例
41
40
  * 通过上下文在组件树中共享依赖注入容器
42
41
  */
43
42
  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 自动
43
+ const INIT_RETURN_ERROR_MESSAGE = 'Service init must return a boolean value.';
44
+ class InvalidInitReturnError extends Error {
45
+ constructor() {
46
+ super(INIT_RETURN_ERROR_MESSAGE);
47
+ this.name = 'InvalidInitReturnError';
48
+ }
49
+ }
50
+ function setServiceInited(target, value) {
51
+ runInAction(() => {
52
+ if (target != null && typeof target === 'object') {
53
+ target.inited = value;
54
+ }
55
+ });
56
+ }
57
+ function applyInitResult(target, result) {
58
+ if (typeof result === 'boolean') {
59
+ setServiceInited(target, result);
60
+ return;
61
+ }
62
+ setServiceInited(target, false);
63
+ throw new InvalidInitReturnError();
64
+ }
65
+ async function initializeServiceInstance(target) {
66
+ if (target == null ||
67
+ typeof target !== 'object' ||
68
+ !('init' in target) ||
69
+ typeof target.init !== 'function') {
70
+ return;
71
+ }
72
+ const service = target;
73
+ if (service.inited === true) {
74
+ return;
75
+ }
76
+ try {
77
+ const result = await service.init();
78
+ applyInitResult(service, result);
79
+ }
80
+ catch (error) {
81
+ if (error instanceof InvalidInitReturnError) {
82
+ throw error;
83
+ }
84
+ setServiceInited(service, false);
85
+ }
86
+ }
71
87
  export function useService(target, selector) {
72
88
  const container = useContext(ctx);
73
89
  // 使用 useState 强制组件重新渲染
@@ -78,13 +94,98 @@ export function useService(target, selector) {
78
94
  // 从容器中获取服务实例
79
95
  const service = container.get(target);
80
96
  // 设置 Mobx reaction,当 selector 返回的数据变化时触发重新渲染
81
- return reaction(() => selector(service), () => refresh({}) // 触发组件重新渲染
97
+ const dispose = reaction(() => {
98
+ // 如果没有提供 selector,使用默认策略监听整个服务的变化
99
+ if (!selector) {
100
+ // 直接访问服务实例,让 Mobx 自动收集依赖
101
+ return service;
102
+ }
103
+ // 执行 selector 获取结果
104
+ const result = selector(service);
105
+ // 智能检测:如果 selector 返回的就是服务实例本身,
106
+ // 则切换到监听所有属性变化的模式
107
+ if (result === service) {
108
+ // 通过访问服务实例让 Mobx 收集所有 observable 属性的依赖
109
+ return service;
110
+ }
111
+ return result;
112
+ }, () => refresh({}) // 触发组件重新渲染
82
113
  );
114
+ return dispose;
83
115
  }, [container, selector, target]);
84
116
  if (container == null) {
85
117
  throw new Error('Must be a child of ServiceProvider.');
86
118
  }
87
- return selector(container.get(target));
119
+ // 返回选择器结果或服务实例
120
+ const service = container.get(target);
121
+ return selector ? selector(service) : service;
122
+ }
123
+ export function useLocalService(ServiceClass, selector) {
124
+ const parentContainer = useContext(ctx);
125
+ // 使用 useRef 保持局部容器和服务实例的引用稳定
126
+ const localContainerRef = useRef(null);
127
+ const serviceInstanceRef = useRef(null);
128
+ // 使用 useState 强制组件重新渲染,完全模仿 useService
129
+ const [, refresh] = useState({});
130
+ const [initError, setInitError] = useState(null);
131
+ if (parentContainer == null) {
132
+ throw new Error('Must be a child of ServiceProvider.');
133
+ }
134
+ // 确保服务实例已创建
135
+ if (serviceInstanceRef.current == null) {
136
+ // 创建继承父容器的子容器
137
+ localContainerRef.current = new Container(parentContainer);
138
+ // 将服务类绑定到局部容器
139
+ localContainerRef.current.bindAll(ServiceClass);
140
+ // 获取服务实例
141
+ serviceInstanceRef.current = localContainerRef.current.get(ServiceClass);
142
+ }
143
+ // 模仿 useService 的结构:在 useEffect 中设置 reaction 和初始化
144
+ React.useEffect(() => {
145
+ if (parentContainer == null)
146
+ return;
147
+ const service = serviceInstanceRef.current;
148
+ // 设置 Mobx reaction,当 selector 返回的数据变化时触发重新渲染
149
+ const dispose = reaction(() => {
150
+ // 如果没有提供 selector,使用默认策略监听整个服务的变化
151
+ if (!selector) {
152
+ // 直接访问服务实例,让 Mobx 自动收集依赖
153
+ return service;
154
+ }
155
+ // 执行 selector 获取结果
156
+ const result = selector(service);
157
+ // 智能检测:如果 selector 返回的就是服务实例本身,
158
+ // 则切换到监听所有属性变化的模式
159
+ if (result === service) {
160
+ // 通过访问服务实例让 Mobx 收集所有 observable 属性的依赖
161
+ return service;
162
+ }
163
+ return result;
164
+ }, () => refresh({}) // 触发组件重新渲染
165
+ );
166
+ return dispose;
167
+ }, [parentContainer, selector, ServiceClass]);
168
+ // 单独的 useEffect 处理服务初始化,避免与 selector 变化的冲突
169
+ React.useEffect(() => {
170
+ if (parentContainer == null)
171
+ return;
172
+ let disposed = false;
173
+ setInitError(prev => (prev === null ? prev : null));
174
+ const service = serviceInstanceRef.current;
175
+ initializeServiceInstance(service).catch((error) => {
176
+ if (error instanceof InvalidInitReturnError && !disposed) {
177
+ setInitError(error);
178
+ }
179
+ });
180
+ return () => {
181
+ disposed = true;
182
+ };
183
+ }, [parentContainer, ServiceClass]); // 注意:这里不依赖 selector
184
+ if (initError) {
185
+ throw initError;
186
+ }
187
+ // 返回选择器结果或服务实例
188
+ return selector ? selector(serviceInstanceRef.current) : serviceInstanceRef.current;
88
189
  }
89
190
  /**
90
191
  * 服务提供者组件
@@ -158,32 +259,38 @@ export function ServiceProvider(props) {
158
259
  function InitService(props) {
159
260
  const { services, children } = props;
160
261
  const container = useContext(ctx);
262
+ const [initError, setInitError] = React.useState(null);
161
263
  useEffect(() => {
162
264
  if (container == null)
163
265
  return;
164
266
  if (services.length == 0)
165
267
  return;
268
+ let disposed = false;
269
+ setInitError(prev => (prev === null ? prev : null));
166
270
  // 异步初始化所有服务
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
- }
271
+ const initPromises = services.map(async (service) => {
272
+ // 尝试从容器中获取服务实例(可选的,避免未注册时报错)
273
+ const instance = container.get(service, { optional: true });
274
+ if (instance != null && typeof instance === 'object') {
275
+ // 检查实例是否实现了 Service 接口
276
+ if ('init' in instance && typeof instance.init === 'function') {
277
+ if (instance.inited !== true) {
278
+ await initializeServiceInstance(instance).catch((error) => {
279
+ if (error instanceof InvalidInitReturnError && !disposed) {
280
+ setInitError(error);
281
+ }
282
+ });
181
283
  }
182
284
  }
183
- });
184
- // 等待所有服务初始化完成
185
- await Promise.all(initPromises);
186
- })();
285
+ }
286
+ });
287
+ void Promise.all(initPromises);
288
+ return () => {
289
+ disposed = true;
290
+ };
187
291
  }, [container, services]);
292
+ if (initError) {
293
+ throw initError;
294
+ }
188
295
  return children;
189
296
  }