状态管理

XinAdmin 前端采用 Zustand 作为状态管理库,它是一个轻量级、简单且功能强大的状态管理解决方案。相比 Redux,Zustand 具有更少的模板代码和更直观的 API 设计,非常适合现代项目的状态管理需求。

Zustand 简介

Zustand 是一个德语单词,意思是 "state"(状态)。它的设计哲学是提供最小但完整的状态管理解决方案,具有以下特点:

  • 轻量级:压缩后仅 1KB 左右
  • 无模板代码:无需 actions、reducers、ActionTypes 等
  • React DevTools 支持:支持时间旅行调试
  • TypeScript 支持:内置完善的类型推断
  • 中间件支持:可扩展功能(如持久化、日志等)
  • 支持服务端渲染

状态管理基本使用

安装和基本用法

// 安装
npm install zustand

// 基本示例
import { create } from 'zustand'

interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
}

const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

状态更新机制

Zustand 使用 set 函数来更新状态,该函数接收一个新的状态对象或一个函数,该函数接收当前状态并返回新的状态。这种方式类似于 React 的 useState Hook。

// 直接传入新状态
set({ count: 1 })

// 基于当前状态计算新状态
set((state) => ({ count: state.count + 1 }))

中间件

Zustand 支持中间件来扩展功能,XinAdmin 项目中使用了以下中间件:

  • devtools:为开发工具提供时间旅行和记录功能
  • persist:状态持久化到本地存储
  • createJSONStorage:指定存储方式(localStorage/sessionStorage)
import { create } from 'zustand'
import { devtools, persist, createJSONStorage } from 'zustand/middleware'

const useStore = create(
  devtools(
    persist(
      (set, get) => ({
        // 状态和方法
      }),
      {
        name: 'store-name', // 持久化键名
        storage: createJSONStorage(() => localStorage), // 存储方式
      }
    )
  )
);

系统状态

XinAdmin 采用切片模式(Slice Pattern)组织状态管理,将不同功能领域的状态分别管理,并最终合并成统一的 Store。主要包含两个核心 Store:useAuthStoreuseGlobalStore

认证状态

认证状态管理负责用户认证相关的所有状态,包括用户信息、菜单、权限等。

核心状态

  • 用户状态 (UserState):存储当前登录用户信息和权限列表

    • user:当前登录用户对象
    • access:用户权限列表
  • 菜单状态 (MenuState):管理导航菜单和面包屑

    • menus:完整菜单数组
    • menuMap:菜单映射,用于快速查找菜单项
    • breadcrumbMap:面包屑映射
    • localRoute:是否使用本地路由
  • 认证状态 (AuthenticationState):认证初始化状态

    • initialized:应用是否已初始化

主要方法

  • login(credentials):用户登录,存储 token 到 localStorage
  • logout():用户登出,清理本地状态
  • getInfo():获取用户信息和菜单
  • initApp():应用初始化
  • isAuthenticated():检查是否已认证
  • setAccess(access):设置用户权限
  • setMenus(menus):设置菜单数据
  • setLocalRoute(isLocal):设置是否使用本地路由

状态持久化

只有 localRoute 字段被持久化到 localStorage,敏感数据如 token 直接存储在 localStorage 中而非 Store 中。

const persistedKeys: (keyof typeof initialAuthState)[] = [
  'localRoute',
];

全局状态

全局状态管理负责整个应用的全局配置,包括网站信息、布局、主题等。

核心状态

  • 网站信息状态 (SiteState):网站基础信息

    • logo:网站 Logo
    • title:网站标题
    • subtitle:网站副标题
    • describe:网站描述
    • documentTitle:文档标题
  • 布局状态 (LayoutState):页面布局相关配置

    • layout:布局类型(side/mix/columns)
    • collapsed:侧边栏折叠状态
    • isMobile:是否移动端
    • mobileMenuOpen:移动端菜单打开状态
    • breadcrumb:当前面包屑
    • menuParentKey:当前父级菜单键
  • 主题状态 (ThemeState):主题配置

    • themeConfig:主题配置对象
    • themeDrawer:主题抽屉打开状态

主要方法

  • 网站信息操作

    • initWebInfo():初始化网站信息
    • setDocumentTitle(title):设置文档标题
  • 布局操作

    • setLayout(layout):设置布局类型
    • setCollapsed(collapsed):设置侧边栏折叠状态
    • setIsMobile(isMobile):设置移动端状态
    • setMobileMenuOpen(mobileMenuOpen):设置移动端菜单状态
    • setBreadcrumb(breadcrumb):设置面包屑
    • setMenuParentKey(menuParentKey):设置父级菜单键
  • 主题操作

    • setThemeConfig(themeConfig):设置主题配置
    • setThemeDrawer(themeDrawer):设置主题抽屉状态

状态持久化

以下字段被持久化到 localStorage:

const persistedKeys: (keyof typeof initialGlobalState)[] = [
  'layout',
  'themeConfig',
  'menuParentKey',
];

注意:临时性状态如 mobileMenuOpenthemeDrawerbreadcrumb 等不会被持久化。

状态管理最佳实践

切片模式

XinAdmin 使用切片模式组织状态,每个功能模块有自己的 slice,然后通过组合创建完整的 store。这样可以提高代码的可维护性和模块化程度。

export const useAuthStore = create<AuthStore>()(
  devtools(
    persist(
      (...args) => ({
        ...createUserSlice(...args),
        ...createMenuSlice(...args),
        ...createAuthenticationSlice(...args),
      }),
      {
        // 持久化配置
      }
    )
  )
);

类型安全

XinAdmin 充分利用 TypeScript 的类型系统,为所有状态和方法定义明确的接口,确保类型安全。

export interface UserState {
  user: ISysUser | null;
  access: string[];
}

export interface UserAction {
  setAccess: (access: string[]) => void;
}

export type UserSlice = UserState & UserAction;

状态初始化

每个 slice 都有对应的初始状态,在需要重置或测试时非常有用。

export const initialUserState: UserState = {
  user: null,
  access: [],
};

状态订阅

在组件中使用状态时,建议只订阅需要的部分,避免不必要的重渲染。

// 好的做法:只订阅需要的状态
const { user, access } = useAuthStore((state) => ({
  user: state.user,
  access: state.access,
}));

// 或者单独订阅
const user = useAuthStore((state) => state.user);