@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,905 @@
1
+ # 前端状态管理
2
+
3
+ 本指南详细介绍 linyjs 前端的全局状态管理机制,包括状态的定义、注册、消费和跨模块共享。
4
+
5
+ ## 概述
6
+
7
+ linyjs 前端状态管理基于 [Jotai](https://jotai.org/) 实现,采用 **模块化声明 + 全局注册表** 的架构:
8
+
9
+ - 📦 **模块化声明** — 每个客户端模块通过 `IGlobalStateService` 声明自己的 atoms
10
+ - 🌐 **全局注册表** — 框架自动收集所有模块的 atoms,合并到全局注册表
11
+ - 🔗 **跨模块共享** — 任意模块的组件都可以读取和修改其他模块注册的状态
12
+ - ⚡ **SSR 兼容** — 状态通过 React Context 提供,SSR 和 CSR 模式下均可使用
13
+
14
+ ### 架构流程
15
+
16
+ ```
17
+ 模块 A (IGlobalStateService) 模块 B (IGlobalStateService)
18
+ atoms: { user, theme } atoms: { cart, wishlist }
19
+ │ │
20
+ └──────────┬─────────────────────┘
21
+
22
+ 框架收集 & 注册
23
+ ClientGlobalStateContext
24
+ { 'moduleA.state.user': atom,
25
+ 'moduleA.state.theme': atom,
26
+ 'moduleB.state.cart': atom, ... }
27
+
28
+ ┌──────────┴──────────┐
29
+ ▼ ▼
30
+ 组件 useContext() 组件 useContext()
31
+ + useAtom() + useAtom()
32
+ ```
33
+
34
+ ---
35
+
36
+ ## 核心接口
37
+
38
+ ### IAtom
39
+
40
+ `IAtom` 是 Jotai Atom 的兼容类型,框架不直接依赖 Jotai,但运行时应使用 Jotai 的 `atom()` 创建:
41
+
42
+ ```typescript
43
+ interface IAtom<T = any> {
44
+ readonly key: symbol // atom 唯一标识(由 atom() 自动生成)
45
+ init?: T // 初始值
46
+ [key: string]: unknown // 其他 Jotai 内部属性
47
+ }
48
+ ```
49
+
50
+ > 💡 实际开发中,你不需要手动构造 `IAtom` 对象,直接使用 Jotai 的 `atom()` 函数即可。
51
+
52
+ ### IGlobalStateService
53
+
54
+ 全局状态服务接口,模块通过实现此接口来声明 atoms:
55
+
56
+ ```typescript
57
+ interface IGlobalStateService {
58
+ atoms: Record<string, IAtom>
59
+ }
60
+ ```
61
+
62
+ ### IGlobalStateDef
63
+
64
+ 全局状态服务的 ServiceDef 类型:
65
+
66
+ ```typescript
67
+ type IGlobalStateDef = IServiceDef<IGlobalStateService, IGlobalStateMetadata>
68
+
69
+ interface IGlobalStateMetadata extends IServiceMetadata {
70
+ usageType: 'global_state'
71
+ }
72
+ ```
73
+
74
+ ### ClientGlobalStateContext
75
+
76
+ React Context,持有所有模块注册的 atoms 注册表。框架在根组件中自动提供此 Context:
77
+
78
+ ```typescript
79
+ const ClientGlobalStateContext: React.Context<Record<string, IAtom>>
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 定义状态
85
+
86
+ ### 安装 Jotai
87
+
88
+ linyjs 前端状态管理依赖 Jotai,在插件项目中安装:
89
+
90
+ ```bash
91
+ npm install jotai
92
+ ```
93
+
94
+ ### 基本状态
95
+
96
+ 使用 Jotai 的 `atom()` 创建原始状态:
97
+
98
+ ```typescript
99
+ import { atom } from 'jotai'
100
+ import type { IGlobalStateService, IGlobalStateDef, IModule } from '@linyjs/client-module-interface'
101
+
102
+ // 基础状态
103
+ const userAtom = atom<{ id: string; name: string } | null>(null)
104
+ const themeAtom = atom<'light' | 'dark'>('light')
105
+ const countAtom = atom(0)
106
+
107
+ // 状态服务
108
+ class MyStateService implements IGlobalStateService {
109
+ atoms = {
110
+ user: userAtom,
111
+ theme: themeAtom,
112
+ count: countAtom,
113
+ }
114
+ }
115
+
116
+ // ServiceDef
117
+ const myStateDef: IGlobalStateDef = {
118
+ serviceTag: 'myState',
119
+ serviceImpl: MyStateService,
120
+ meta: { usageType: 'global_state' },
121
+ }
122
+
123
+ // 模块声明
124
+ export const myClientModule: IModule = {
125
+ name: 'myClient',
126
+ serviceDefs: [myStateDef],
127
+ }
128
+ ```
129
+
130
+ ### 派生状态(只读)
131
+
132
+ 派生状态从其他 atom 计算而来,只读不可写:
133
+
134
+ ```typescript
135
+ import { atom } from 'jotai'
136
+
137
+ const userAtom = atom<{ id: string; name: string; roles: string[] } | null>(null)
138
+
139
+ // 派生状态:用户是否已登录
140
+ const isLoggedInAtom = atom((get) => get(userAtom) !== null)
141
+
142
+ // 派生状态:用户是否是管理员
143
+ const isAdminAtom = atom((get) => {
144
+ const user = get(userAtom)
145
+ return user?.roles?.includes('admin') ?? false
146
+ })
147
+
148
+ // 派生状态:用户显示名称
149
+ const displayNameAtom = atom((get) => {
150
+ const user = get(userAtom)
151
+ return user ? user.name : '游客'
152
+ })
153
+ ```
154
+
155
+ ### 可写派生状态
156
+
157
+ 可写派生状态既可以从其他 atom 读取,也可以接受写入操作:
158
+
159
+ ```typescript
160
+ import { atom } from 'jotai'
161
+
162
+ const itemsAtom = atom<{ id: string; name: string; done: boolean }[]>([])
163
+
164
+ // 可写派生状态:未完成任务数量(只读)
165
+ const pendingCountAtom = atom((get) => {
166
+ return get(itemsAtom).filter(item => !item.done).length
167
+ })
168
+
169
+ // 可写派生状态:切换任务完成状态
170
+ const toggleItemAtom = atom(
171
+ (get) => get(itemsAtom),
172
+ (get, set, itemId: string) => {
173
+ const items = get(itemsAtom)
174
+ set(itemsAtom, items.map(item =>
175
+ item.id === itemId ? { ...item, done: !item.done } : item
176
+ ))
177
+ }
178
+ )
179
+ ```
180
+
181
+ ### 异步状态
182
+
183
+ Jotai 支持异步 atom,适合从 API 加载数据:
184
+
185
+ ```typescript
186
+ import { atom } from 'jotai'
187
+
188
+ // 异步 atom:从 API 加载用户信息
189
+ const userProfileAtom = atom(async () => {
190
+ const token = localStorage.getItem('token')
191
+ if (!token) return null
192
+
193
+ const response = await fetch('/api/auth/me', {
194
+ headers: { 'Authorization': `Bearer ${token}` }
195
+ })
196
+ const result = await response.json()
197
+ return result.user ?? null
198
+ })
199
+
200
+ // 依赖其他 atom 的异步 atom
201
+ const userIdAtom = atom<string | null>(null)
202
+ const userDetailAtom = atom(async (get) => {
203
+ const userId = get(userIdAtom)
204
+ if (!userId) return null
205
+
206
+ const response = await fetch(`/api/users/${userId}`)
207
+ return response.json()
208
+ })
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 注册状态
214
+
215
+ ### 完整模块示例
216
+
217
+ ```typescript
218
+ import { atom } from 'jotai'
219
+ import type {
220
+ IModule,
221
+ IGlobalStateService,
222
+ IGlobalStateDef,
223
+ IRouteService,
224
+ IRouteDef,
225
+ } from '@linyjs/client-module-interface'
226
+
227
+ // ==================== 状态定义 ====================
228
+
229
+ interface CartItem {
230
+ id: string
231
+ name: string
232
+ price: number
233
+ quantity: number
234
+ }
235
+
236
+ // 基础状态
237
+ const cartItemsAtom = atom<CartItem[]>([])
238
+ const couponAtom = atom<string | null>(null)
239
+
240
+ // 派生状态
241
+ const cartTotalAtom = atom((get) => {
242
+ const items = get(cartItemsAtom)
243
+ return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
244
+ })
245
+
246
+ const cartCountAtom = atom((get) => {
247
+ return get(cartItemsAtom).reduce((sum, item) => sum + item.quantity, 0)
248
+ })
249
+
250
+ // ==================== 状态服务 ====================
251
+
252
+ class CartStateService implements IGlobalStateService {
253
+ atoms = {
254
+ cartItems: cartItemsAtom,
255
+ coupon: couponAtom,
256
+ cartTotal: cartTotalAtom,
257
+ cartCount: cartCountAtom,
258
+ }
259
+ }
260
+
261
+ const cartStateDef: IGlobalStateDef = {
262
+ serviceTag: 'cartState',
263
+ serviceImpl: CartStateService,
264
+ meta: { usageType: 'global_state' },
265
+ }
266
+
267
+ // ==================== 路由服务 ====================
268
+
269
+ class CartRouteService implements IRouteService {
270
+ basePath = '/shop'
271
+
272
+ getRoutes() {
273
+ return [
274
+ { path: '/cart', component: CartPage, key: 'cart-page' }
275
+ ]
276
+ }
277
+ }
278
+
279
+ const cartRouteDef: IRouteDef = {
280
+ serviceTag: 'cartRoute',
281
+ serviceImpl: CartRouteService,
282
+ meta: { usageType: 'route' },
283
+ }
284
+
285
+ // ==================== 模块声明 ====================
286
+
287
+ export const shopClientModule: IModule = {
288
+ name: 'shopClient',
289
+ serviceDefs: [cartStateDef, cartRouteDef],
290
+ }
291
+ ```
292
+
293
+ ### 注册表 Key 格式
294
+
295
+ 框架收集 atoms 时,会自动生成全局唯一的注册表 key,采用 **三段式格式**:
296
+
297
+ ```
298
+ {moduleName}.{serviceTag}.{atomName}
299
+ ```
300
+
301
+ 以上面的 `shopClient` 模块为例,注册后的 key 为:
302
+
303
+ | atom 名 | 注册表 key |
304
+ |---------|-----------|
305
+ | `cartItems` | `shopClient.cartState.cartItems` |
306
+ | `coupon` | `shopClient.cartState.coupon` |
307
+ | `cartTotal` | `shopClient.cartState.cartTotal` |
308
+ | `cartCount` | `shopClient.cartState.cartCount` |
309
+
310
+ > ⚠️ 消费状态时必须使用完整的注册表 key,而非原始的 atom 引用。
311
+
312
+ ---
313
+
314
+ ## 消费状态
315
+
316
+ ### 基本用法
317
+
318
+ 通过 `useContext` 获取 atoms 注册表,再用 Jotai 的 `useAtom` 读取和修改状态:
319
+
320
+ ```typescript
321
+ import { useContext } from 'react'
322
+ import { useAtom } from 'jotai'
323
+ import { ClientGlobalStateContext } from '@linyjs/client-module-interface'
324
+
325
+ function CartPage() {
326
+ // 1. 从 Context 获取 atoms 注册表
327
+ const atoms = useContext(ClientGlobalStateContext)
328
+
329
+ // 2. 通过注册表 key 获取 atom
330
+ const cartItemsAtom = atoms['shopClient.cartState.cartItems']
331
+ const cartTotalAtom = atoms['shopClient.cartState.cartTotal']
332
+
333
+ // 3. 使用 useAtom 读取和修改状态
334
+ const [cartItems, setCartItems] = useAtom(cartItemsAtom)
335
+ const [cartTotal] = useAtom(cartTotalAtom)
336
+
337
+ const removeItem = (id: string) => {
338
+ setCartItems(cartItems.filter(item => item.id !== id))
339
+ }
340
+
341
+ return (
342
+ <div>
343
+ <h1>购物车 ({cartItems.length})</h1>
344
+ <ul>
345
+ {cartItems.map(item => (
346
+ <li key={item.id}>
347
+ {item.name} × {item.quantity} = ¥{item.price * item.quantity}
348
+ <button onClick={() => removeItem(item.id)}>删除</button>
349
+ </li>
350
+ ))}
351
+ </ul>
352
+ <p>总计: ¥{cartTotal}</p>
353
+ </div>
354
+ )
355
+ }
356
+ ```
357
+
358
+ ### 只读状态
359
+
360
+ 对于派生状态(只读 atom),使用 `useAtomValue`:
361
+
362
+ ```typescript
363
+ import { useAtomValue } from 'jotai'
364
+ import { useContext } from 'react'
365
+ import { ClientGlobalStateContext } from '@linyjs/client-module-interface'
366
+
367
+ function CartBadge() {
368
+ const atoms = useContext(ClientGlobalStateContext)
369
+ const cartCountAtom = atoms['shopClient.cartState.cartCount']
370
+
371
+ // 只读取,不需要 setter
372
+ const count = useAtomValue(cartCountAtom)
373
+
374
+ return <span className="badge">{count}</span>
375
+ }
376
+ ```
377
+
378
+ ### 只写状态
379
+
380
+ 如果只需要修改状态而不需要读取当前值,使用 `useSetAtom`:
381
+
382
+ ```typescript
383
+ import { useSetAtom } from 'jotai'
384
+ import { useContext } from 'react'
385
+ import { ClientGlobalStateContext } from '@linyjs/client-module-interface'
386
+
387
+ function AddToCartButton({ product }: { product: { id: string; name: string; price: number } }) {
388
+ const atoms = useContext(ClientGlobalStateContext)
389
+ const cartItemsAtom = atoms['shopClient.cartState.cartItems']
390
+
391
+ // 只获取 setter,不订阅状态变化
392
+ const setCartItems = useSetAtom(cartItemsAtom)
393
+
394
+ const handleAdd = () => {
395
+ setCartItems(items => [...items, { ...product, quantity: 1 }])
396
+ }
397
+
398
+ return <button onClick={handleAdd}>加入购物车</button>
399
+ }
400
+ ```
401
+
402
+ ---
403
+
404
+ ## 跨模块状态访问
405
+
406
+ 全局状态的核心优势是跨模块共享。任何模块的组件都可以读取其他模块注册的状态。
407
+
408
+ ### 示例场景
409
+
410
+ 假设 `authClient` 模块注册了用户状态,`shopClient` 模块需要读取用户信息:
411
+
412
+ ```typescript
413
+ // authClient 模块注册的状态
414
+ // key: authClient.authState.user
415
+ class AuthStateService implements IGlobalStateService {
416
+ atoms = {
417
+ user: atom<{ id: string; name: string; roles: string[] } | null>(null)
418
+ }
419
+ }
420
+ ```
421
+
422
+ ```typescript
423
+ // shopClient 模块的组件读取 authClient 的用户状态
424
+ import { useContext } from 'react'
425
+ import { useAtomValue } from 'jotai'
426
+ import { ClientGlobalStateContext } from '@linyjs/client-module-interface'
427
+
428
+ function CheckoutPage() {
429
+ const atoms = useContext(ClientGlobalStateContext)
430
+
431
+ // ✅ 跨模块读取:使用完整的三段式 key
432
+ const userAtom = atoms['authClient.authState.user']
433
+ const user = useAtomValue(userAtom)
434
+
435
+ if (!user) {
436
+ return <div>请先登录</div>
437
+ }
438
+
439
+ return <div>结账 - 用户: {user.name}</div>
440
+ }
441
+ ```
442
+
443
+ ### 封装自定义 Hook
444
+
445
+ 为避免在多个组件中重复 `useContext` + `useAtom` 的样板代码,建议封装自定义 Hook:
446
+
447
+ ```typescript
448
+ import { useContext } from 'react'
449
+ import { useAtom, useAtomValue, useSetAtom } from 'jotai'
450
+ import { ClientGlobalStateContext, type IAtom } from '@linyjs/client-module-interface'
451
+
452
+ /** 获取全局状态注册表中的 atom */
453
+ function useGlobalAtom<T>(key: string): IAtom<T> | undefined {
454
+ const atoms = useContext(ClientGlobalStateContext)
455
+ return atoms[key] as IAtom<T> | undefined
456
+ }
457
+
458
+ /** 读取和修改全局状态 */
459
+ function useGlobalState<T>(key: string): [T, (value: T | ((prev: T) => T)) => void] {
460
+ const atom = useGlobalAtom<T>(key)
461
+ if (!atom) {
462
+ throw new Error(`[useGlobalState] Atom not found: ${key}`)
463
+ }
464
+ return useAtom(atom as any)
465
+ }
466
+
467
+ /** 只读全局状态 */
468
+ function useGlobalStateValue<T>(key: string): T {
469
+ const atom = useGlobalAtom<T>(key)
470
+ if (!atom) {
471
+ throw new Error(`[useGlobalStateValue] Atom not found: ${key}`)
472
+ }
473
+ return useAtomValue(atom as any)
474
+ }
475
+
476
+ /** 只写全局状态 */
477
+ function useSetGlobalState<T>(key: string): (value: T | ((prev: T) => T)) => void {
478
+ const atom = useGlobalAtom<T>(key)
479
+ if (!atom) {
480
+ throw new Error(`[useSetGlobalState] Atom not found: ${key}`)
481
+ }
482
+ return useSetAtom(atom as any)
483
+ }
484
+ ```
485
+
486
+ 使用封装后的 Hook:
487
+
488
+ ```typescript
489
+ // 读取当前模块的状态
490
+ const [cartItems, setCartItems] = useGlobalState<CartItem[]>('shopClient.cartState.cartItems')
491
+ const cartTotal = useGlobalStateValue<number>('shopClient.cartState.cartTotal')
492
+
493
+ // 跨模块读取
494
+ const user = useGlobalStateValue<User | null>('authClient.authState.user')
495
+ ```
496
+
497
+ ---
498
+
499
+ ## 封装为公共 Hook 注册
500
+
501
+ 如果希望将状态消费逻辑封装为可复用的 Hook,可以通过 `IHooksService` 注册,供其他模块通过 `useClientHook` 消费:
502
+
503
+ ```typescript
504
+ import { atom, useAtomValue, useSetAtom } from 'jotai'
505
+ import { useContext } from 'react'
506
+ import type {
507
+ IModule,
508
+ IGlobalStateService,
509
+ IGlobalStateDef,
510
+ IHooksService,
511
+ IHooksDef,
512
+ } from '@linyjs/client-module-interface'
513
+ import { ClientGlobalStateContext } from '@linyjs/client-module-interface'
514
+
515
+ // ==================== 状态定义 ====================
516
+
517
+ const userAtom = atom<{ id: string; name: string } | null>(null)
518
+
519
+ class AuthStateService implements IGlobalStateService {
520
+ atoms = { user: userAtom }
521
+ }
522
+
523
+ const authStateDef: IGlobalStateDef = {
524
+ serviceTag: 'authState',
525
+ serviceImpl: AuthStateService,
526
+ meta: { usageType: 'global_state' },
527
+ }
528
+
529
+ // ==================== 封装 Hook ====================
530
+
531
+ function useAuthUser() {
532
+ const atoms = useContext(ClientGlobalStateContext)
533
+ const userAtom = atoms['authClient.authState.user']
534
+ return useAtomValue(userAtom)
535
+ }
536
+
537
+ function useSetAuthUser() {
538
+ const atoms = useContext(ClientGlobalStateContext)
539
+ const userAtom = atoms['authClient.authState.user']
540
+ return useSetAtom(userAtom)
541
+ }
542
+
543
+ // ==================== Hook 注册 ====================
544
+
545
+ class AuthHooksService implements IHooksService {
546
+ hooks = {
547
+ useAuthUser,
548
+ useSetAuthUser,
549
+ }
550
+ }
551
+
552
+ const authHooksDef: IHooksDef = {
553
+ serviceTag: 'authHooks',
554
+ serviceImpl: AuthHooksService,
555
+ meta: { usageType: 'hook' },
556
+ }
557
+
558
+ // ==================== 模块声明 ====================
559
+
560
+ export const authClientModule: IModule = {
561
+ name: 'authClient',
562
+ serviceDefs: [authStateDef, authHooksDef],
563
+ }
564
+ ```
565
+
566
+ 其他模块通过 `useClientHook` 消费:
567
+
568
+ ```typescript
569
+ import { useClientHook } from '@linyjs/client-module-interface'
570
+
571
+ type UseAuthUser = () => { id: string; name: string } | null
572
+
573
+ function NavBar() {
574
+ const useAuthUser = useClientHook<UseAuthUser>('authClient.authHooks.useAuthUser')
575
+
576
+ if (!useAuthUser) return null
577
+
578
+ const user = useAuthUser()
579
+
580
+ return <span>{user ? user.name : '未登录'}</span>
581
+ }
582
+ ```
583
+
584
+ > 📖 关于 `useClientHook` 的详细用法,请阅读 [公共 Hooks 使用](./client-hooks.md)。
585
+
586
+ ---
587
+
588
+ ## SSR 注意事项
589
+
590
+ ### Context 自动提供
591
+
592
+ 在 SSR 模式下,框架的 SSR 中间件会自动将 atoms 注册表通过 `ClientGlobalStateContext.Provider` 提供给组件树:
593
+
594
+ ```typescript
595
+ // 框架内部实现(简化)
596
+ createElement(
597
+ ClientGlobalStateContext.Provider,
598
+ { value: registries.atoms }, // 所有模块的 atoms 注册表
599
+ contentElement
600
+ )
601
+ ```
602
+
603
+ 客户端 hydration 时同样会提供 Context,确保 SSR 和 CSR 行为一致。
604
+
605
+ ### SSR 中的状态读取
606
+
607
+ SSR 渲染时,atoms 使用初始值(`atom(initialValue)` 的参数)。如果需要 SSR 时从服务端获取数据,请使用路由的 `loader` 预加载数据,而非异步 atom:
608
+
609
+ ```typescript
610
+ // ✅ 推荐:使用 loader 预加载数据(SSR 友好)
611
+ {
612
+ path: '/dashboard',
613
+ component: Dashboard,
614
+ key: 'dashboard',
615
+ loader: async () => {
616
+ const res = await fetch('/api/dashboard/stats')
617
+ return { stats: await res.json() }
618
+ }
619
+ }
620
+
621
+ // ⚠️ 注意:异步 atom 在 SSR 中可能不会完成请求
622
+ const statsAtom = atom(async () => {
623
+ const res = await fetch('/api/dashboard/stats')
624
+ return res.json()
625
+ })
626
+ ```
627
+
628
+ > 📖 关于路由 Loader 的详细用法,请阅读 [前端页面路由](./client-routes.md)。
629
+
630
+ ---
631
+
632
+ ## 最佳实践
633
+
634
+ ### 1. 按领域拆分状态
635
+
636
+ 将不同领域的状态放在不同的 ServiceDef 中,保持职责清晰:
637
+
638
+ ```typescript
639
+ // ✅ 好的做法:按领域拆分
640
+ class UserStateService implements IGlobalStateService {
641
+ atoms = { user: userAtom, isLoggedIn: isLoggedInAtom }
642
+ }
643
+
644
+ class ThemeStateService implements IGlobalStateService {
645
+ atoms = { theme: themeAtom, fontSize: fontSizeAtom }
646
+ }
647
+
648
+ class CartStateService implements IGlobalStateService {
649
+ atoms = { cartItems: cartItemsAtom, cartTotal: cartTotalAtom }
650
+ }
651
+ ```
652
+
653
+ ### 2. 使用派生状态避免冗余
654
+
655
+ 不要在多个 atom 中存储相同的数据,使用派生状态从源头计算:
656
+
657
+ ```typescript
658
+ // ✅ 好的做法:单一数据源 + 派生状态
659
+ const cartItemsAtom = atom<CartItem[]>([])
660
+ const cartTotalAtom = atom((get) =>
661
+ get(cartItemsAtom).reduce((sum, i) => sum + i.price * i.quantity, 0)
662
+ )
663
+ const cartCountAtom = atom((get) =>
664
+ get(cartItemsAtom).reduce((sum, i) => sum + i.quantity, 0)
665
+ )
666
+
667
+ // ❌ 避免:在多个 atom 中存储冗余数据
668
+ const cartItemsAtom = atom<CartItem[]>([])
669
+ const cartTotalAtom = atom(0) // 需要手动同步,容易出错
670
+ const cartCountAtom = atom(0) // 需要手动同步,容易出错
671
+ ```
672
+
673
+ ### 3. 为 atom 命名时使用清晰的名称
674
+
675
+ 注册表 key 由 `moduleName.serviceTag.atomName` 组成,确保 atom 名具有描述性:
676
+
677
+ ```typescript
678
+ // ✅ 好的命名
679
+ atoms = {
680
+ currentUser: userAtom, // shopClient.shopState.currentUser
681
+ cartItems: cartItemsAtom, // shopClient.shopState.cartItems
682
+ selectedCategoryId: categoryAtom, // shopClient.shopState.selectedCategoryId
683
+ }
684
+
685
+ // ❌ 避免模糊的命名
686
+ atoms = {
687
+ data: dataAtom, // 什么 data?
688
+ value: valueAtom, // 什么 value?
689
+ state: stateAtom, // 什么 state?
690
+ }
691
+ ```
692
+
693
+ ### 4. 处理 atom 未找到的情况
694
+
695
+ 从注册表获取 atom 时,可能因为模块未加载或 key 拼写错误导致获取到 `undefined`:
696
+
697
+ ```typescript
698
+ function CartBadge() {
699
+ const atoms = useContext(ClientGlobalStateContext)
700
+ const cartCountAtom = atoms['shopClient.cartState.cartCount']
701
+
702
+ // ✅ 始终检查 atom 是否存在
703
+ if (!cartCountAtom) {
704
+ return <span className="badge">0</span>
705
+ }
706
+
707
+ const count = useAtomValue(cartCountAtom)
708
+ return <span className="badge">{count}</span>
709
+ }
710
+ ```
711
+
712
+ ### 5. 避免在渲染期间创建 atom
713
+
714
+ atom 应在模块顶层创建,不要在组件内部或 ServiceDef 的方法中动态创建:
715
+
716
+ ```typescript
717
+ // ✅ 好的做法:在模块顶层创建
718
+ const userAtom = atom(null)
719
+
720
+ class MyStateService implements IGlobalStateService {
721
+ atoms = { user: userAtom }
722
+ }
723
+
724
+ // ❌ 避免:在组件内部创建(每次渲染都会创建新 atom)
725
+ function MyComponent() {
726
+ const dataAtom = atom(null) // 错误!
727
+ const [data] = useAtom(dataAtom)
728
+ }
729
+ ```
730
+
731
+ ### 6. 遵循 ServiceDef 代码风格规范
732
+
733
+ > 📐 **重要**: ServiceDef 必须声明为独立变量,禁止内联在 `serviceDefs` 数组中。详见 [ServiceDef 代码风格规范](../best-practices/service-definition.md)。
734
+
735
+ ```typescript
736
+ // ✅ 好的做法:独立变量声明
737
+ const myStateDef: IGlobalStateDef = {
738
+ serviceTag: 'myState',
739
+ serviceImpl: MyStateService,
740
+ meta: { usageType: 'global_state' },
741
+ }
742
+
743
+ export const myModule: IModule = {
744
+ name: 'myClient',
745
+ serviceDefs: [myStateDef],
746
+ }
747
+
748
+ // ❌ 避免:内联声明
749
+ export const myModule: IModule = {
750
+ name: 'myClient',
751
+ serviceDefs: [
752
+ { serviceTag: 'myState', serviceImpl: MyStateService, meta: { usageType: 'global_state' } }
753
+ ],
754
+ }
755
+ ```
756
+
757
+ ---
758
+
759
+ ## 完整示例
760
+
761
+ 以下示例展示一个主题管理模块,包含状态定义、消费组件和跨模块共享:
762
+
763
+ ```typescript
764
+ // theme-client-module.ts
765
+ import { atom, useAtom } from 'jotai'
766
+ import { useContext } from 'react'
767
+ import type {
768
+ IModule,
769
+ IGlobalStateService,
770
+ IGlobalStateDef,
771
+ } from '@linyjs/client-module-interface'
772
+ import { ClientGlobalStateContext } from '@linyjs/client-module-interface'
773
+
774
+ // ==================== 类型定义 ====================
775
+
776
+ type Theme = 'light' | 'dark'
777
+
778
+ interface ThemeState {
779
+ theme: Theme
780
+ fontSize: number
781
+ sidebarCollapsed: boolean
782
+ }
783
+
784
+ // ==================== 状态定义 ====================
785
+
786
+ const themeStateAtom = atom<ThemeState>({
787
+ theme: 'light',
788
+ fontSize: 14,
789
+ sidebarCollapsed: false,
790
+ })
791
+
792
+ // 派生状态
793
+ const isDarkThemeAtom = atom((get) => get(themeStateAtom).theme === 'dark')
794
+
795
+ // 可写派生状态
796
+ const toggleThemeAtom = atom(
797
+ (get) => get(themeStateAtom).theme,
798
+ (get, set) => {
799
+ const current = get(themeStateAtom)
800
+ set(themeStateAtom, {
801
+ ...current,
802
+ theme: current.theme === 'light' ? 'dark' : 'light',
803
+ })
804
+ }
805
+ )
806
+
807
+ // ==================== 状态服务 ====================
808
+
809
+ class ThemeStateService implements IGlobalStateService {
810
+ atoms = {
811
+ themeState: themeStateAtom,
812
+ isDarkTheme: isDarkThemeAtom,
813
+ toggleTheme: toggleThemeAtom,
814
+ }
815
+ }
816
+
817
+ const themeStateDef: IGlobalStateDef = {
818
+ serviceTag: 'themeState',
819
+ serviceImpl: ThemeStateService,
820
+ meta: { usageType: 'global_state' },
821
+ }
822
+
823
+ export const themeClientModule: IModule = {
824
+ name: 'themeClient',
825
+ serviceDefs: [themeStateDef],
826
+ }
827
+
828
+ // ==================== 消费组件 ====================
829
+
830
+ // 主题切换按钮(同模块消费)
831
+ function ThemeToggle() {
832
+ const atoms = useContext(ClientGlobalStateContext)
833
+ const toggleThemeAtom = atoms['themeClient.themeState.toggleTheme']
834
+
835
+ const [theme, toggle] = useAtom(toggleThemeAtom)
836
+
837
+ return (
838
+ <button onClick={toggle}>
839
+ {theme === 'light' ? '🌙 深色模式' : '☀️ 浅色模式'}
840
+ </button>
841
+ )
842
+ }
843
+
844
+ // 布局组件(跨模块消费,读取主题状态)
845
+ function AdminLayout({ children }: { children: React.ReactNode }) {
846
+ const atoms = useContext(ClientGlobalStateContext)
847
+ const isDarkAtom = atoms['themeClient.themeState.isDarkTheme']
848
+ const themeStateAtom = atoms['themeClient.themeState.themeState']
849
+
850
+ const isDark = useAtomValue(isDarkAtom)
851
+ const themeState = useAtomValue(themeStateAtom)
852
+
853
+ return (
854
+ <div style={{
855
+ backgroundColor: isDark ? '#1a1a1a' : '#ffffff',
856
+ color: isDark ? '#ffffff' : '#000000',
857
+ fontSize: `${themeState.fontSize}px`,
858
+ }}>
859
+ <Sidebar collapsed={themeState.sidebarCollapsed} />
860
+ <main>{children}</main>
861
+ </div>
862
+ )
863
+ }
864
+ ```
865
+
866
+ ---
867
+
868
+ ## ⚠️ 构建注意事项:Jotai 由宿主提供
869
+
870
+ Jotai 与 React 一样,**必须由宿主环境统一提供**,插件打包时不应该将 Jotai 打包进 bundle。
871
+
872
+ CLI 默认已将 `jotai` 设为 external,无需额外配置。宿主 SSR 应用通过 Import Map 提供全局 Jotai 实例:
873
+
874
+ ```html
875
+ <script type="importmap">
876
+ {
877
+ "imports": {
878
+ "react": "/client/vendor/react.js",
879
+ "react-dom": "/client/vendor/react-dom.js",
880
+ "jotai": "/client/vendor/jotai.js"
881
+ }
882
+ }
883
+ </script>
884
+ ```
885
+
886
+ **为什么 Jotai 必须 external?**
887
+
888
+ 所有插件必须共享同一个 Jotai 实例。如果每个插件打包自己的 Jotai,则:
889
+ - 一个插件创建的 `atom()` 无法被另一个插件的 `useAtom()` 识别
890
+ - `ClientGlobalStateContext` 中注册的全局 atom 无法跨模块消费
891
+ - Store 实例隔离,状态互不可见
892
+
893
+
894
+ ---
895
+
896
+ ## 相关文档
897
+
898
+ - 💻 [客户端模块开发](./client-module.md) — 模块结构总览
899
+ - 🪝 [公共 Hooks 使用](./client-hooks.md) — `useClientHook` 获取跨模块 Hook
900
+ - 🛤️ [前端页面路由](./client-routes.md) — 路由 Loader 预加载数据
901
+ - 🔧 [API 参考](../api-reference/index.md) — `IGlobalStateService`、`IAtom` 接口定义
902
+
903
+ ---
904
+
905
+ **上一篇**: [公共 Hooks 使用](./client-hooks.md) | **下一篇**: [UI 扩展插槽](./ui-slots.md)