@linyjs/plugin-docs 0.0.15 → 0.0.17

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.
@@ -0,0 +1,626 @@
1
+ # 公共 Hooks 使用
2
+
3
+ 本指南详细介绍 linyjs 框架提供的公共 Hooks,用于在组件中消费跨模块注册的资源(组件、Hooks、服务)和路由功能。
4
+
5
+ ## 概述
6
+
7
+ linyjs 通过 React Context 提供全局注册表,使模块间可以共享资源。框架提供以下 5 个公共 Hooks:
8
+
9
+ | Hook | 用途 | 返回值 |
10
+ |------|------|--------|
11
+ | `useClientComponent` | 获取跨模块注册的组件 | `IComponent \| undefined` |
12
+ | `useClientHook` | 获取跨模块注册的 Hook | `Function \| undefined` |
13
+ | `useClientService` | 获取跨模块注册的服务实例 | `T \| undefined` |
14
+ | `useClientRoute` | 获取路由功能(导航、参数等) | `any` |
15
+
16
+ 所有 Hooks 从 `@linyjs/client-module-interface` 导入:
17
+
18
+ ```typescript
19
+ import {
20
+ useClientComponent,
21
+ useClientHook,
22
+ useClientService,
23
+ useClientRoute,
24
+ } from '@linyjs/client-module-interface';
25
+ ```
26
+
27
+ ## 资源引用格式
28
+
29
+ 所有跨模块资源使用统一的引用格式:
30
+
31
+ ### RegistryKey 格式
32
+
33
+ 资源在全局注册表中的 key 采用 `moduleName.serviceTag.name` 三段式格式:
34
+
35
+ ```
36
+ {moduleName}.{serviceTag}.{name}
37
+ ```
38
+
39
+ ### 两种引用方式
40
+
41
+ | 引用方式 | 格式 | 示例 | 说明 |
42
+ |----------|------|------|------|
43
+ | 模块内引用 | `serviceTag.name` | `'userComponents.Avatar'` | 省略 moduleName,框架自动补全 |
44
+ | 跨模块引用 | `moduleName.serviceTag.name` | `'userModule.userComponents.Avatar'` | 显式指定完整路径 |
45
+
46
+ > 💡 **自动补全规则**:如果传入的引用路径只有 1 个点(即 `serviceTag.name`),框架会尝试补全当前模块名。如果传入 2 个及以上点(即 `moduleName.serviceTag.name`),框架视为完整路径直接使用。
47
+
48
+ ### 不同资源类型的引用格式
49
+
50
+ | 资源类型 | 模块内引用 | 跨模块引用 |
51
+ |----------|-----------|-----------|
52
+ | 组件 | `'userComponents.Avatar'` | `'userModule.userComponents.Avatar'` |
53
+ | Hook | `'authHooks.useAuth'` | `'authModule.authHooks.useAuth'` |
54
+ | 服务 | `'userService'` | `'userModule.userService'` |
55
+
56
+ > ⚠️ **服务的引用格式不同**:服务只有两段 `moduleName.serviceTag`,不含 `.name`,因为服务本身就是整个实例。
57
+
58
+ ---
59
+
60
+ ## useClientComponent
61
+
62
+ 获取跨模块注册的 React 组件。
63
+
64
+ ### 签名
65
+
66
+ ```typescript
67
+ function useClientComponent<P = any>(refPath: string): IComponent<P> | undefined
68
+ ```
69
+
70
+ ### 参数
71
+
72
+ | 参数 | 类型 | 说明 |
73
+ |------|------|------|
74
+ | `refPath` | `string` | 组件引用路径 |
75
+ | `P`(泛型) | - | 组件 Props 类型,用于类型推断 |
76
+
77
+ ### 返回值
78
+
79
+ 返回对应的 React 组件。如果未找到,返回 `undefined` 并在控制台输出警告。
80
+
81
+ ### 使用示例
82
+
83
+ #### 跨模块引用组件
84
+
85
+ ```typescript
86
+ import { useClientComponent } from '@linyjs/client-module-interface';
87
+
88
+ function MyPage() {
89
+ // 从 userModule 获取 Avatar 组件
90
+ const Avatar = useClientComponent<{ src: string; size?: number }>(
91
+ 'userModule.userComponents.Avatar'
92
+ );
93
+
94
+ if (!Avatar) {
95
+ return <div>组件加载中...</div>;
96
+ }
97
+
98
+ return <Avatar src="/avatar.jpg" size={48} />;
99
+ }
100
+ ```
101
+
102
+ #### 模块内引用组件
103
+
104
+ ```typescript
105
+ // 假设当前组件属于 userModule
106
+ function UserCard() {
107
+ // 省略 moduleName,框架自动补全
108
+ const Avatar = useClientComponent('userComponents.Avatar');
109
+
110
+ return (
111
+ <div>
112
+ {Avatar && <Avatar src="/avatar.jpg" />}
113
+ <span>用户名</span>
114
+ </div>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## useClientHook
122
+
123
+ 获取跨模块注册的自定义 Hook。
124
+
125
+ ### 签名
126
+
127
+ ```typescript
128
+ function useClientHook<T extends Function>(refPath: string): T | undefined
129
+ ```
130
+
131
+ ### 参数
132
+
133
+ | 参数 | 类型 | 说明 |
134
+ |------|------|------|
135
+ | `refPath` | `string` | Hook 引用路径 |
136
+ | `T`(泛型) | `extends Function` | Hook 的函数类型 |
137
+
138
+ ### 返回值
139
+
140
+ 返回对应的 Hook 函数。如果未找到,返回 `undefined` 并在控制台输出警告。
141
+
142
+ ### 使用示例
143
+
144
+ #### 跨模块引用 Hook
145
+
146
+ ```typescript
147
+ import { useClientHook } from '@linyjs/client-module-interface';
148
+
149
+ function MyComponent() {
150
+ // 从 authModule 获取 useAuth Hook
151
+ const useAuth = useClientHook<() => { user: any; login: Function; logout: Function }>(
152
+ 'authModule.authHooks.useAuth'
153
+ );
154
+
155
+ if (!useAuth) {
156
+ return <div>认证功能加载中...</div>;
157
+ }
158
+
159
+ // 调用获取到的 Hook
160
+ const { user, login, logout } = useAuth();
161
+
162
+ if (!user) {
163
+ return <button onClick={() => login()}>登录</button>;
164
+ }
165
+
166
+ return (
167
+ <div>
168
+ <span>欢迎, {user.name}</span>
169
+ <button onClick={() => logout()}>退出</button>
170
+ </div>
171
+ );
172
+ }
173
+ ```
174
+
175
+ #### 带参数的 Hook
176
+
177
+ ```typescript
178
+ // 假设 dataModule 注册了一个 useFetchData Hook
179
+ function Dashboard() {
180
+ type UseFetchData = (url: string) => {
181
+ data: any;
182
+ loading: boolean;
183
+ error: string | null;
184
+ };
185
+
186
+ const useFetchData = useClientHook<UseFetchData>(
187
+ 'dataModule.dataHooks.useFetchData'
188
+ );
189
+
190
+ if (!useFetchData) return <div>Loading...</div>;
191
+
192
+ const { data, loading, error } = useFetchData('/api/stats');
193
+
194
+ if (loading) return <div>加载中...</div>;
195
+ if (error) return <div>错误: {error}</div>;
196
+
197
+ return <div>{JSON.stringify(data)}</div>;
198
+ }
199
+ ```
200
+
201
+ ---
202
+
203
+ ## useClientService
204
+
205
+ 获取跨模块注册的服务实例。
206
+
207
+ ### 签名
208
+
209
+ ```typescript
210
+ function useClientService<T = any>(refPath: string): T | undefined
211
+ ```
212
+
213
+ ### 参数
214
+
215
+ | 参数 | 类型 | 说明 |
216
+ |------|------|------|
217
+ | `refPath` | `string` | 服务引用路径 |
218
+ | `T`(泛型) | - | 服务实例类型 |
219
+
220
+ ### 返回值
221
+
222
+ 返回对应的服务实例。如果未找到,返回 `undefined` 并在控制台输出警告。
223
+
224
+ > ⚠️ **注意**:服务的引用格式是 `moduleName.serviceTag`(两段),不是三段。服务实例是整个对象,不需要 `.name`。
225
+
226
+ ### 使用示例
227
+
228
+ #### 跨模块引用服务
229
+
230
+ ```typescript
231
+ import { useClientService } from '@linyjs/client-module-interface';
232
+
233
+ interface IAnalyticsService {
234
+ track(event: string, data?: Record<string, any>): void;
235
+ pageView(path: string): void;
236
+ }
237
+
238
+ function MyPage() {
239
+ // 从 analyticsModule 获取分析服务
240
+ const analytics = useClientService<IAnalyticsService>(
241
+ 'analyticsModule.analyticsService'
242
+ );
243
+
244
+ useEffect(() => {
245
+ // 记录页面访问
246
+ analytics?.pageView('/my-page');
247
+ }, []);
248
+
249
+ const handleClick = () => {
250
+ analytics?.track('button_click', { button: 'cta' });
251
+ };
252
+
253
+ return <button onClick={handleClick}>点击</button>;
254
+ }
255
+ ```
256
+
257
+ #### 模块内引用服务
258
+
259
+ ```typescript
260
+ // 假设当前组件属于 userModule
261
+ function UserList() {
262
+ // 省略 moduleName,框架自动补全为 userModule.userService
263
+ const userService = useClientService<{ getUsers: () => Promise<any[]> }>(
264
+ 'userService'
265
+ );
266
+
267
+ const [users, setUsers] = useState([]);
268
+
269
+ useEffect(() => {
270
+ userService?.getUsers().then(setUsers);
271
+ }, []);
272
+
273
+ return (
274
+ <ul>
275
+ {users.map(user => <li key={user.id}>{user.name}</li>)}
276
+ </ul>
277
+ );
278
+ }
279
+ ```
280
+
281
+ ---
282
+
283
+ ## useClientRoute
284
+
285
+ 获取路由相关的组件和 Hooks。底层通过路由适配器(`IRouterAdapter`)实现,切换适配器后消费端代码不变。
286
+
287
+ ### 签名
288
+
289
+ ```typescript
290
+ function useClientRoute(key: string): any
291
+ ```
292
+
293
+ ### 参数
294
+
295
+ | 参数 | 类型 | 说明 |
296
+ |------|------|------|
297
+ | `key` | `string` | 路由功能标识 |
298
+
299
+ ### 可用的 Key
300
+
301
+ | Key | 返回类型 | 说明 |
302
+ |-----|----------|------|
303
+ | `'NavLink'` | `React.ComponentType` | 导航链接组件,支持 active 状态样式 |
304
+ | `'Link'` | `React.ComponentType` | 普通链接组件 |
305
+ | `'useNavigate'` | `() => NavigateFunction` | 编程式导航 Hook |
306
+ | `'useParams'` | `() => Record<string, string>` | 获取路由参数 Hook |
307
+ | `'useLocation'` | `() => Location` | 获取当前位置 Hook |
308
+ | `'useSearchParams'` | `() => [URLSearchParams, Function]` | 获取/设置查询参数 Hook |
309
+
310
+ ### 使用示例
311
+
312
+ #### 导航链接
313
+
314
+ ```typescript
315
+ import { useClientRoute } from '@linyjs/client-module-interface';
316
+
317
+ function NavBar() {
318
+ const NavLink = useClientRoute('NavLink');
319
+ const Link = useClientRoute('Link');
320
+
321
+ if (!NavLink) return null;
322
+
323
+ return (
324
+ <nav>
325
+ {/* NavLink 支持 active 状态 */}
326
+ <NavLink to="/" end className="nav-link">
327
+ 首页
328
+ </NavLink>
329
+ <NavLink to="/articles" className="nav-link">
330
+ 文章
331
+ </NavLink>
332
+ <NavLink to="/kanban" className="nav-link">
333
+ 看板
334
+ </NavLink>
335
+ </nav>
336
+ );
337
+ }
338
+ ```
339
+
340
+ #### 编程式导航
341
+
342
+ ```typescript
343
+ function LoginForm() {
344
+ const useNavigate = useClientRoute('useNavigate');
345
+ const navigate = useNavigate();
346
+
347
+ const handleLogin = async (credentials) => {
348
+ const result = await login(credentials);
349
+ if (result.success) {
350
+ navigate('/dashboard'); // 导航到仪表盘
351
+ // navigate('/dashboard', { replace: true }); // 替换历史记录
352
+ } else {
353
+ navigate('/login?error=invalid');
354
+ }
355
+ };
356
+
357
+ return <button onClick={() => handleLogin({ ... })}>登录</button>;
358
+ }
359
+ ```
360
+
361
+ #### 获取路由参数
362
+
363
+ ```typescript
364
+ function TaskDetail() {
365
+ const useParams = useClientRoute('useParams');
366
+ const params = useParams();
367
+
368
+ return <div>任务 ID: {params.id}</div>;
369
+ }
370
+
371
+ // 多参数路由 /groups/:groupId/tasks/:taskId
372
+ function GroupTaskDetail() {
373
+ const useParams = useClientRoute('useParams');
374
+ const { groupId, taskId } = useParams();
375
+
376
+ return (
377
+ <div>
378
+ 分组: {groupId}, 任务: {taskId}
379
+ </div>
380
+ );
381
+ }
382
+ ```
383
+
384
+ #### 获取当前位置
385
+
386
+ ```typescript
387
+ function Breadcrumb() {
388
+ const useLocation = useClientRoute('useLocation');
389
+ const location = useLocation();
390
+
391
+ const paths = location.pathname.split('/').filter(Boolean);
392
+
393
+ return (
394
+ <nav>
395
+ {paths.map((path, index) => (
396
+ <span key={index}>
397
+ {index > 0 && ' / '}
398
+ {path}
399
+ </span>
400
+ ))}
401
+ </nav>
402
+ );
403
+ }
404
+ ```
405
+
406
+ #### 获取和设置查询参数
407
+
408
+ ```typescript
409
+ function ArticleList() {
410
+ const useSearchParams = useClientRoute('useSearchParams');
411
+ const [searchParams, setSearchParams] = useSearchParams();
412
+
413
+ const page = Number(searchParams.get('page') || '1');
414
+ const limit = Number(searchParams.get('limit') || '10');
415
+
416
+ const handlePageChange = (newPage: number) => {
417
+ setSearchParams({ page: String(newPage), limit: String(limit) });
418
+ };
419
+
420
+ return (
421
+ <div>
422
+ <p>第 {page} 页,每页 {limit} 条</p>
423
+ <button onClick={() => handlePageChange(page + 1)}>下一页</button>
424
+ </div>
425
+ );
426
+ }
427
+ ```
428
+
429
+ > 📖 **路由功能的详细说明**:`useClientRoute` 的更多用法请阅读 [前端页面路由](./client-routes.md)。
430
+
431
+ ---
432
+
433
+ ## 资源注册
434
+
435
+ 以上 Hooks 消费的资源由各模块通过 ServiceDef 声明注册。以下是注册方式的简要说明,详见 [客户端模块开发](./client-module.md)。
436
+
437
+ ### 注册组件
438
+
439
+ ```typescript
440
+ import type { IComponentListService, IComponentListDef } from '@linyjs/client-module-interface';
441
+
442
+ class MyComponentService implements IComponentListService {
443
+ components = {
444
+ MyButton,
445
+ MyCard
446
+ };
447
+ }
448
+
449
+ const componentDef: IComponentListDef = {
450
+ serviceTag: 'myComponents',
451
+ serviceImpl: MyComponentService,
452
+ meta: { usageType: 'component' }
453
+ };
454
+
455
+ // 注册后可通过 useClientComponent('moduleName.myComponents.MyButton') 获取
456
+ ```
457
+
458
+ ### 注册 Hook
459
+
460
+ ```typescript
461
+ import type { IHooksService, IHooksDef } from '@linyjs/client-module-interface';
462
+
463
+ class MyHooksService implements IHooksService {
464
+ hooks = {
465
+ useFetchData,
466
+ useLocalStorage
467
+ };
468
+ }
469
+
470
+ const hookDef: IHooksDef = {
471
+ serviceTag: 'myHooks',
472
+ serviceImpl: MyHooksService,
473
+ meta: { usageType: 'hook' }
474
+ };
475
+
476
+ // 注册后可通过 useClientHook('moduleName.myHooks.useFetchData') 获取
477
+ ```
478
+
479
+ ### 注册服务
480
+
481
+ ```typescript
482
+ import type { INormalServiceDef } from '@linyjs/client-module-interface';
483
+
484
+ class MyService {
485
+ doSomething() { /* ... */ }
486
+ }
487
+
488
+ const serviceDef: INormalServiceDef = {
489
+ serviceTag: 'myService',
490
+ serviceImpl: MyService,
491
+ meta: { usageType: 'normal_service' }
492
+ };
493
+
494
+ // 注册后可通过 useClientService('moduleName.myService') 获取
495
+ ```
496
+
497
+ ---
498
+
499
+ ## React Context 提供者
500
+
501
+ 所有 Hooks 依赖以下 React Context。框架在根组件中提供这些 Context,确保所有子组件都能访问注册表:
502
+
503
+ | Context | 类型 | 说明 |
504
+ |---------|------|------|
505
+ | `ClientComponentsContext` | `Record<string, IComponent>` | 组件注册表 |
506
+ | `ClientHooksContext` | `Record<string, Function>` | Hook 注册表 |
507
+ | `ClientServicesContext` | `Record<string, any>` | 服务注册表 |
508
+ | `ClientRouterContext` | `any` | 路由适配器实例 |
509
+
510
+ > ⚠️ **SSR 注意**:在 SSR 模式下,这些 Context 由 SSR 中间件自动提供。在 CSR 模式下,需要确保根组件包裹了所有 Provider。
511
+
512
+ ---
513
+
514
+ ## 最佳实践
515
+
516
+ ### 1. 使用 TypeScript 泛型获取类型安全
517
+
518
+ ```typescript
519
+ // ✅ 好的做法:使用泛型参数
520
+ const Avatar = useClientComponent<{ src: string; size?: number }>(
521
+ 'userModule.userComponents.Avatar'
522
+ );
523
+
524
+ const useAuth = useClientHook<() => { user: User | null }>(
525
+ 'authModule.authHooks.useAuth'
526
+ );
527
+
528
+ const analytics = useClientService<IAnalyticsService>(
529
+ 'analyticsModule.analyticsService'
530
+ );
531
+
532
+ // ❌ 避免:使用 any 类型,丢失类型检查
533
+ const Avatar = useClientComponent('userModule.userComponents.Avatar'); // 类型为 any
534
+ ```
535
+
536
+ ### 2. 处理资源未找到的情况
537
+
538
+ ```typescript
539
+ function MyPage() {
540
+ const Avatar = useClientComponent('userModule.userComponents.Avatar');
541
+
542
+ // 始终检查 undefined
543
+ if (!Avatar) {
544
+ return <div>组件加载中...</div>;
545
+ }
546
+
547
+ return <Avatar src="/avatar.jpg" />;
548
+ }
549
+ ```
550
+
551
+ ### 3. 避免在渲染期间调用获取到的 Hook
552
+
553
+ ```typescript
554
+ function MyComponent() {
555
+ const useAuth = useClientHook('authModule.authHooks.useAuth');
556
+
557
+ // ✅ 正确:先获取 Hook,再在组件顶层调用
558
+ const { user } = useAuth ? useAuth() : { user: null };
559
+
560
+ return <div>{user?.name}</div>;
561
+ }
562
+ ```
563
+
564
+ ### 4. 使用完整的跨模块引用路径
565
+
566
+ ```typescript
567
+ // ✅ 推荐:使用完整路径,明确来源
568
+ const Avatar = useClientComponent('userModule.userComponents.Avatar');
569
+
570
+ // ⚠️ 谨慎:模块内引用依赖自动补全,可能在跨模块场景下失效
571
+ const Avatar = useClientComponent('userComponents.Avatar');
572
+ ```
573
+
574
+ ---
575
+
576
+ ## 常见问题
577
+
578
+ ### 1. Hook 返回 undefined
579
+
580
+ **原因**:资源未注册或引用路径错误。
581
+
582
+ **排查步骤**:
583
+ 1. 检查控制台是否有 `[useClientComponent] Component not found` 等警告
584
+ 2. 确认模块已正确加载(检查模块加载日志)
585
+ 3. 确认引用路径格式正确(三段式 `moduleName.serviceTag.name`)
586
+ 4. 确认 ServiceDef 的 `meta.usageType` 正确
587
+
588
+ ### 2. 服务引用格式错误
589
+
590
+ ```typescript
591
+ // ❌ 错误:服务引用使用了三段式
592
+ const service = useClientService('moduleName.serviceTag.someMethod');
593
+
594
+ // ✅ 正确:服务引用使用两段式
595
+ const service = useClientService('moduleName.serviceTag');
596
+ // 然后通过 service.someMethod() 调用方法
597
+ ```
598
+
599
+ ### 3. 路由功能返回 undefined
600
+
601
+ ```typescript
602
+ const useNavigate = useClientRoute('useNavigate');
603
+
604
+ if (!useNavigate) {
605
+ // RouterProvider 未设置或路由适配器未初始化
606
+ // 检查是否在 RouterProvider 内部使用此 Hook
607
+ console.warn('路由功能不可用');
608
+ return;
609
+ }
610
+
611
+ const navigate = useNavigate();
612
+ ```
613
+
614
+ ---
615
+
616
+ ## 相关文档
617
+
618
+ - 💻 [客户端模块开发](./client-module.md) — 资源注册方式
619
+ - 🛤️ [前端页面路由](./client-routes.md) — `useClientRoute` 详细用法
620
+ - 📊 [前端状态管理](./state-management.md) — Jotai 全局状态管理
621
+ - 🔧 [API 参考](../api-reference/index.md) — Hooks 接口定义
622
+ - 🎨 [UI 扩展插槽](./ui-slots.md) — UI 组件注入机制
623
+
624
+ ---
625
+
626
+ **上一篇**: [前端页面路由](./client-routes.md) | **下一篇**: [前端状态管理](./state-management.md)
@@ -164,7 +164,7 @@ class BlogRouteService implements IRouteService {
164
164
 
165
165
  ## 全局状态(Global State)
166
166
 
167
- 使用 Jotai 进行状态管理。
167
+ 使用 Jotai 进行状态管理。关于状态管理的完整说明(包括派生状态、跨模块共享、SSR 注意事项等),请阅读 [前端状态管理](./state-management.md)。
168
168
 
169
169
  ### 定义状态
170
170
 
@@ -595,10 +595,13 @@ function SafeComponent() {
595
595
 
596
596
  ## 下一步
597
597
 
598
+ - 🛤️ 阅读 [前端页面路由](./client-routes.md) — 路由定义、参数、守卫、Loader、useClientRoute
599
+ - 🪝 阅读 [公共 Hooks 使用](./client-hooks.md) — useClientComponent、useClientHook、useClientService、useClientRoute
600
+ - 📊 阅读 [前端状态管理](./state-management.md) — Jotai 全局状态管理
598
601
  - 🎨 阅读 [UI 扩展插槽](./ui-slots.md)
599
602
  - 🔒 阅读 [权限管理](./permissions.md)
600
603
  - 💾 阅读 [数据持久化](./data-persistence.md)
601
604
 
602
605
  ---
603
606
 
604
- **上一篇**: [服务端模块开发](./server-module.md) | **下一篇**: [UI 扩展插槽](./ui-slots.md)
607
+ **上一篇**: [服务端模块开发](./server-module.md) | **下一篇**: [前端页面路由](./client-routes.md)