You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.9 KiB

状态管理最佳实践

LobeChat 不同于传统 CRUD 的网页,存在大量的富交互能力,如何设计一个易于开发与易于维护的数据流架构非常重要。本篇文档将介绍 LobeChat 中的数据流管理最佳实践。

TOC

概念要素

概念名词 解释
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 即可。
DataFill/store
├── index.ts
└── initialState.ts
  • 一般复杂度 :一般复杂度存在 5 ~ 15 个 state、 5 ~ 10 个 action可能会存在 selector 实现派生状态,也有可能存在 reducer 简化部分数据变更的复杂度。此时的结构一般为一个 store.ts + 一个 initialState.ts + 一个 selectors.ts/reducer.ts
IconPicker/store
├── index.ts
├── initialState.ts
├── selectors.ts
└── store.ts
SortableList/store
├── index.ts
├── initialState.ts
├── listDataReducer.ts
└── store.ts
  • 中等复杂度 中等复杂度存在 15 ~ 30 个 state、 10 ~ 20 个 action大概率会存在 selector 来聚合派生状态,大概率存在 reducer 简化部分数据变更的复杂度。

此时结构,用单一的 action store 已经较难维护,往往会拆解出来多个 slice 用于管理不同的 action。 下方的代码代表了 SortableTree 组件的内部数据流:

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 的模块化和分形架构的心智,我们可以很容易地找到对应的模块,新增功能与迭代都很易于维护。

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 模式 来组织数据流。下面是 LobeChat SessionStore 的目录结构,其中每个目录和文件都有其特定的用途:

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

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<SessionStore, [['zustand/devtools', never]]> = (...parameters) => ({
  ...initialState,
  ...createAgentSlice(...parameters),
  ...createSessionSlice(...parameters),
});



//  ===============  实装 useStore ============ //

export const useSessionStore = createWithEqualityFn<SessionStore>()(
  persist(
    subscribeWithSelector(
      devtools(createStore, {
        name: 'LobeChat_Session' + (isDev ? '_DEV' : ''),
      }),
    ),
    persistOptions,
  ),
  shallow,
);

在这个 store.ts 文件中,我们创建了一个 useSessionStore 钩子,它使用 zustand 库来创建一个全局状态管理器。我们将 initialState 和每个 Slice 的状态和动作合并,以创建完整的 SessionStore。

slices/session/action.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<any>;
}

export const createSessionSlice: StateCreator<
  SessionStore,
  [['zustand/devtools', never]],
  [],
  SessionAction
> = (set, get) => ({
  useFetchSessions: () => {
    // ...初始化会话的逻辑
  },
  // ...其他动作的实现
});

action.ts 文件中,我们定义了一个 SessionActions 接口来描述会话相关的动作,并且实现了一个 useFetchSessions 函数来创建这些动作。然后,我们将这些动作与初始状态合并,以形成会话相关的 Slice。

通过这种结构分层和模块化的方法,我们可以确保 LobeChat 的 SessionStore 是清晰、可维护的,同时也便于扩展和测试。