# 状态管理最佳实践 LobeChat 不同于传统 CRUD 的网页,存在大量的富交互能力,如何设计一个易于开发与易于维护的数据流架构非常重要。本篇文档将介绍 LobeChat 中的数据流管理最佳实践。 ## TOC - [概念要素](#概念要素) - [结构分层](#结构分层) - [LobeChat SessionStore 目录结构最佳实践](#lobechat-sessionstore-目录结构最佳实践) - [SessionStore 的实现](#sessionstore-的实现) ## 概念要素 | 概念名词 | 解释 | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | store | 状态库 (store),包含存储应用的状态、动作。允许在应用渲染中访问和修改状态。 | | state | 状态 (state) 是指应用程序的数据,存储了应用程序的当前状态,状态的变化**一定会触发应用的重新渲染**,以反映新的状态。 | | action | 动作 (action) 是一个操作函数,它描述了应用程序中发生的交互事件。动作通常是由用户交互、网络请求或定时器等触发。 action 可以是**同步**的,也可以是**异步**的。 | | reducer | 归约器 (reducer) 是一个纯函数,它接收当前状态和动作作为参数,并返回一个新的状态。它用于根据动作类型来更新应用程序的状态。Reducer 是一个纯函数,不存在副作用,因此一定是 **同步** 函数。 | | selector | 选择器 (selector) 是一个函数,用于从应用程序的状态中获取特定的数据。它接收应用程序的状态作为参数,并返回经过计算或转换后的数据。Selector 可以将状态的一部分或多个状态组合起来,以生成派生的数据。Selector 通常用于将应用程序的状态映射到组件的 props,以供组件使用。 | | slice | 切片 (slice) 是一个概念,用于表达数据模型状态的一部分。它指定了一个状态切片(slice),以及与该切片相关的 state、action、reducer 和 selector。使用 Slice 可以将大型的 Store 拆分为更小的、可维护的子类型。 | ## 结构分层 在不同的复杂度下,我们可以将 Store 的结构组织可以由很大的不同: - **较低复杂度**:一般包含 2\~5 个 state 、3 \~ 4 个 action。此时的结构一般直接一个 `store.ts` + 一个 `initialState.ts` 即可。 ```bash DataFill/store ├── index.ts └── initialState.ts ``` - **一般复杂度** :一般复杂度存在 5 \~ 15 个 state、 5 \~ 10 个 action,可能会存在 selector 实现派生状态,也有可能存在 reducer 简化部分数据变更的复杂度。此时的结构一般为一个 `store.ts` + 一个 `initialState.ts` + 一个 `selectors.ts`/`reducer.ts`。 ```bash IconPicker/store ├── index.ts ├── initialState.ts ├── selectors.ts └── store.ts ``` ```bash SortableList/store ├── index.ts ├── initialState.ts ├── listDataReducer.ts └── store.ts ``` - **中等复杂度** : 中等复杂度存在 15 \~ 30 个 state、 10 \~ 20 个 action,大概率会存在 selector 来聚合派生状态,大概率存在 reducer 简化部分数据变更的复杂度。 此时结构,用单一的 action store 已经较难维护,往往会拆解出来多个 slice 用于管理不同的 action。 下方的代码代表了 `SortableTree` 组件的内部数据流: ```bash SortableTree/store ├── index.ts ├── initialState.ts ├── selectors.ts ├── slices ├── crudSlice.ts ├── dndSlice.ts └── selectionSlice.ts ├── store.ts └── treeDataReducer.ts ``` - 高等复杂度:高等复杂度存在 30 个以上的 state、 20 个以上的 action。必然需要 slice 做模块化内聚。在每个 slice 中都各自声明了各自的 initState、 action、reducer 与 selector。 下述这个数据流的目录结构是之前一版 SessionStore,具有很高的复杂度,实现了大量的业务逻辑。但借助于 slice 的模块化和分形架构的心智,我们可以很容易地找到对应的模块,新增功能与迭代都很易于维护。 ```bash LobeChat SessionStore ├── index.ts ├── initialState.ts ├── selectors.ts ├── slices │ ├── agentConfig │ │ ├── action.ts │ │ ├── index.ts │ │ ├── initialState.ts │ │ └── selectors.ts │ ├── chat │ │ ├── actions │ │ │ ├── index.ts │ │ │ ├── message.ts │ │ │ └── topic.ts │ │ ├── index.ts │ │ ├── initialState.ts │ │ ├── reducers │ │ │ ├── message.ts │ │ │ └── topic.ts │ │ ├── selectors │ │ │ ├── chat.ts │ │ │ ├── index.ts │ │ │ ├── token.ts │ │ │ ├── topic.ts │ │ │ └── utils.ts │ │ └── utils.ts │ └── session │ ├── action.ts │ ├── index.ts │ ├── initialState.ts │ ├── reducers │ │ └── session.ts │ └── selectors │ ├── export.ts │ ├── index.ts │ └── list.ts └── store.ts ``` ### LobeChat SessionStore 目录结构最佳实践 在 LobeChat 应用中,由于会话管理是一个复杂的功能模块,因此我们采用了 [slice 模式](https://github.com/pmndrs/zustand/blob/main/docs/guides/slices-pattern.md) 来组织数据流。下面是 LobeChat SessionStore 的目录结构,其中每个目录和文件都有其特定的用途: ```fish src/store/session ├── index.ts # SessionStore 的聚合导出文件 ├── initialState.ts # 聚合了所有 slice 的 initialState ├── selectors.ts # 从各个 slices 导出的 selector ├── store.ts # SessionStore 的创建和使用 ├── helpers.ts # 辅助函数 └── slices # 各个独立的功能切片    ├── agent # 助理 Slice    │   ├── action.ts    │   ├── index.ts    │   └── selectors.ts    └── session # 会话 Slice       ├── action.ts       ├── helpers.ts       ├── initialState.ts       └── selectors          ├── export.ts          ├── list.ts          └── index.ts ``` ## SessionStore 的实现 在 LobeChat 中,SessionStore 被设计为管理会话状态和逻辑的核心模块。它由多个 Slices 组成,每个 Slice 管理一部分相关的状态和逻辑。下面是一个简化的 SessionStore 的实现示例: #### store.ts ```ts import { PersistOptions, devtools, persist, subscribeWithSelector } from 'zustand/middleware'; import { shallow } from 'zustand/shallow'; import { devtools } from 'zustand/middleware'; import { createWithEqualityFn } from 'zustand/traditional'; import { SessionStoreState, initialState } from './initialState'; import { AgentAction, createAgentSlice } from './slices/agent/action'; import { SessionAction, createSessionSlice } from './slices/session/action'; // =============== 聚合 createStoreFn ============ // export type SessionStore = SessionAction & AgentAction & SessionStoreState; const createStore: StateCreator = (...parameters) => ({ ...initialState, ...createAgentSlice(...parameters), ...createSessionSlice(...parameters), }); // =============== 实装 useStore ============ // export const useSessionStore = createWithEqualityFn()( persist( subscribeWithSelector( devtools(createStore, { name: 'LobeChat_Session' + (isDev ? '_DEV' : ''), }), ), persistOptions, ), shallow, ); ``` 在这个 `store.ts` 文件中,我们创建了一个 `useSessionStore` 钩子,它使用 `zustand` 库来创建一个全局状态管理器。我们将 initialState 和每个 Slice 的状态和动作合并,以创建完整的 SessionStore。 #### slices/session/action.ts ```ts import { StateCreator } from 'zustand'; import { SessionStore } from '@/store/session'; export interface SessionActions { /** * A custom hook that uses SWR to fetch sessions data. */ useFetchSessions: () => SWRResponse; } export const createSessionSlice: StateCreator< SessionStore, [['zustand/devtools', never]], [], SessionAction > = (set, get) => ({ useFetchSessions: () => { // ...初始化会话的逻辑 }, // ...其他动作的实现 }); ``` 在 `action.ts` 文件中,我们定义了一个 `SessionActions` 接口来描述会话相关的动作,并且实现了一个 `useFetchSessions` 函数来创建这些动作。然后,我们将这些动作与初始状态合并,以形成会话相关的 Slice。 通过这种结构分层和模块化的方法,我们可以确保 LobeChat 的 SessionStore 是清晰、可维护的,同时也便于扩展和测试。