怎么在 UmiJS 中管理全局状态和数据流

作者:组件日常

时间:2026-11-02

浏览:102

详细讲解在 UmiJS 中管理全局状态与数据流的实用方法,包括基于 Hooks 的 plugin-model、初始化状态 getInitialState、传统 Dva 方案以及状态选型与性能优化策略。

文章目录
UmiJS 实践 全局状态管理 plugin-model 阅读时长:约 10 分钟
怎么在 UmiJS 中管理全局状态和数据流
图 0:UmiJS 全局状态管理机制全景缩略图

在 UmiJS 中管理全局状态,核心方案由两层组成:绝大多数日常中后台业务直接使用框架内置的 plugin-model(基于 React Hooks 的轻量模型)搭配 src/app.ts 中的 getInitialState 即可满足;对于极少数拥有长时序、复杂 Saga 异步编排的大型工程或老项目迁移,才需要引入基于 Redux 的 @umijs/plugin-dva。

一、UmiJS 数据流体系的核心机制与架构分工

在传统 React 应用中,引入 Redux 或 MobX 往往需要编写大量的 Action Type、Reducer、Context Provider 与样板配置。UmiJS 针对中后台系统的开发高频场景,将数据流收敛为“约定驱动、开箱即用”的分层架构:

  • 应用启动级状态(@@initialState):负责在应用加载之初拉取当前登录用户信息、权限字典、站点主题等全局基石数据,供导航栏、路由守卫与权限组件直接消费。
  • 通用业务级状态(plugin-model):在 src/models 目录下编写标准自定义 React Hook,Umi 会自动将其注册为全局单例模型,任意组件通过 useModel 即可消费与更新。
  • 复杂事件流(plugin-dva):基于 Redux 与 Redux-Saga,适合高并发联动、长事务处理以及需要从老旧 Umi 2/3 工程平滑迁移的业务。
怎么在 UmiJS 中管理全局状态和数据流
图 1:UmiJS 全局数据流分层架构与数据流向关系

这种分层避免了“所有状态一股脑扔进全局单一 Store”带来的混乱与性能损耗。不同生命周期的数据各司其职,既保证了开发效率,又维持了清晰的职责边界。

二、轻量首选:使用 plugin-model 管理业务全局状态

plugin-model 是 UmiJS 官方推荐的通用全局状态方案。它的核心理念是将大家熟悉的 React Custom Hook 直接提升为全局单例状态,无需学习新的状态机概念。

步骤 1:在 src/models 下创建 Hook 模型

在项目的 src/models 目录下新建文件,例如 useUserModel.ts。模型就是一个普通的自定义 Hook,内部可以使用 useState、useCallback 等标准 React API:

// 示例文件位置:src/models/useUserModel.ts
import { useState, useCallback } from 'react';

export interface UserProfile {
  id: string;
  name: string;
  role: string;
}

export default function useUserModel() {
  const [user, setUser] = useState<UserProfile | null>({
    id: 'U1001',
    name: '张三',
    role: 'admin',
  });
  const [collapsed, setCollapsed] = useState<boolean>(false);

  // 模拟登出操作
  const logout = useCallback(() => {
    setUser(null);
  }, []);

  // 模拟修改用户名
  const updateName = useCallback((newName: string) => {
    setUser((prev) => (prev ? { ...prev, name: newName } : null));
  }, []);

  const toggleCollapsed = useCallback(() => {
    setCollapsed((prev) => !prev);
  }, []);

  return {
    user,
    collapsed,
    logout,
    updateName,
    toggleCollapsed,
  };
}
命名与约定规则 文件创建在 src/models/ 目录下后,Umi 会以文件名的基础部分作为 model 的命名空间。例如 useUserModel.ts 或 user.ts,在后续使用时对应的 Key 分别为 'useUserModel' 或 'user'。

步骤 2:在组件中订阅与更新模型

在工程内的任何函数组件中,直接从 umi 中导入 useModel,传入模型的名字即可:

// 示例文件位置:src/pages/Header/index.tsx
import React from 'react';
import { useModel } from 'umi';

export default function HeaderComponent() {
  // 直接解构获取数据和更新方法
  const { user, logout } = useModel('useUserModel');

  if (!user) {
    return <div>未登录</div>;
  }

  return (
    <header style={{ display: 'flex', justifyContent: 'space-between', padding: 16 }}>
      <span>当前用户:{user.name}({user.role})</span>
      <button onClick={logout}>退出登录</button>
    </header>
  );
}

步骤 3:使用 Selector 避免不必要的重渲染

当一个模型中包含多个属性时,默认调用 useModel('modelName') 会导致该模型中任何属性改变时,所有使用了该 Hook 的组件都重新渲染。

为了解决性能瓶颈,useModel 支持传入第二个参数 selector,只有在 selector 返回的字段发生变化时,当前组件才会触发 re-render:

// 示例文件位置:src/pages/Sidebar/index.tsx
import React from 'react';
import { useModel } from 'umi';

export default function SidebarComponent() {
  // 仅在 collapsed 改变时重新渲染,user 的变动不会影响该侧边栏
  const collapsed = useModel('useUserModel', (model) => model.collapsed);
  const toggleCollapsed = useModel('useUserModel', (model) => model.toggleCollapsed);

  return (
    <aside style={{ width: collapsed ? 80 : 200 }}>
      <button onClick={toggleCollapsed}>
        {collapsed ? '展开' : '收起'}
      </button>
    </aside>
  );
}
怎么在 UmiJS 中管理全局状态和数据流
图 2:plugin-model 标准四步开发流程与 Selector 性能优化

三、应用级初始化数据流:getInitialState 与 @@initialState

多数管理系统在渲染页面前,必须先获取当前登录用户的鉴权 Token、角色信息以及全局配置。如果把这些逻辑写在普通页面的 useEffect 中,很容易出现页面布局闪烁或权限判定滞后。

UmiJS 提供了标准运行时配置文件 src/app.ts,配合 getInitialState 机制专门解决这一问题。

1. 在 app.ts 中定义初始化函数

导出名为 getInitialState 的异步函数。Umi 会在应用挂载的第一时间调用它,并将其返回值全局托管在名为 @@initialState 的特殊 model 中:

// 示例文件位置:src/app.ts
import { queryCurrentUser, querySiteSettings } from '@/services/user';

export async function getInitialState(): Promise<{
  currentUser?: API.CurrentUser;
  settings?: API.SiteSettings;
}> {
  // 检查本地 Token
  const token = localStorage.getItem('access_token');
  if (!token) {
    return {};
  }

  try {
    // 并行请求当前登录人与全局字典
    const [currentUser, settings] = await Promise.all([
      queryCurrentUser(),
      querySiteSettings(),
    ]);
    return {
      currentUser,
      settings,
    };
  } catch (error) {
    // 异常处理:例如登录失效
    localStorage.removeItem('access_token');
    return {};
  }
}

2. 在业务中消费与动态刷新 @@initialState

通过 useModel('@@initialState'),不仅可以获取初始化的数据,还能获得 setInitialState 和 refresh 方法:

// 示例文件位置:src/pages/Login/index.tsx
import React from 'react';
import { useModel, history } from 'umi';
import { loginAccount } from '@/services/auth';

export default function LoginPage() {
  const { initialState, setInitialState, refresh } = useModel('@@initialState');

  const handleLogin = async (credentials: any) => {
    const res = await loginAccount(credentials);
    if (res.token) {
      localStorage.setItem('access_token', res.token);
      // 方式 1:调用 refresh(),自动重新执行 app.ts 中的 getInitialState
      await refresh();
      history.push('/dashboard');
    }
  };

  const handleThemeChange = (newTheme: string) => {
    // 方式 2:手动合并修改状态
    setInitialState((prev) => ({
      ...prev,
      settings: { ...prev?.settings, theme: newTheme },
    }));
  };

  return (
    <div>
      <button onClick={() => handleLogin({ user: 'admin', pass: '123' })}>登录</button>
      <button onClick={() => handleThemeChange('dark')}>切换暗色模式</button>
    </div>
  );
}
怎么在 UmiJS 中管理全局状态和数据流
图 3:getInitialState 启动装配、消费与动态刷新生命周期

由于 @@initialState 深度集成了 Umi 的 Access(权限守卫)与 Layout(通用中后台布局),配置完成后无需编写额外的 Context Provider,即可自动控制菜单显示、路由放行以及头像展示。

四、传统与重型场景:Dva 数据流的适用与实践

如果项目是从 Umi 2/3 演进而来,或者业务中充斥着复杂的异步时序处理(如长轮询联动、防抖节流异步流、多接口因果依赖),使用基于 Redux 与 Redux-Saga 的 Dva 插件依然是一种可选方案。

Dva 模型的基本结构

在开启 dva: {} 插件配置后,在 src/models 下创建符合 Dva 规范的模型对象:

// 示例文件位置:src/models/orderDva.ts
import { Effect, Reducer, Subscription } from 'umi';
import { fetchOrderList, cancelOrderApi } from '@/services/order';

export interface OrderModelState {
  list: any[];
  total: number;
}

export interface OrderModelType {
  namespace: 'order';
  state: OrderModelState;
  effects: {
    query: Effect;
    cancel: Effect;
  };
  reducers: {
    save: Reducer<OrderModelState>;
  };
}

const OrderModel: OrderModelType = {
  namespace: 'order',
  state: {
    list: [],
    total: 0,
  },
  effects: {
    *query({ payload }, { call, put }) {
      const response = yield call(fetchOrderList, payload);
      yield put({
        type: 'save',
        payload: response.data,
      });
    },
    *cancel({ payload }, { call, put }) {
      yield call(cancelOrderApi, payload.id);
      // 取消成功后再次触发刷新
      yield put({ type: 'query', payload: { page: 1 } });
    },
  },
  reducers: {
    save(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
  },
};

export default OrderModel;
架构选型建议 现代 Umi(如 Umi 4 / Ant Design Pro 新版)默认倡导轻量化的 plugin-model。若无强烈的 Saga 异步逻辑需求,新项目尽量不要引入 Dva。Dva 的 action 字符串分发与 Generator 语法会增加初学者的认知负担与样板代码量。

五、数据流方案横向对比与关键避坑指南

在实际项目研发中,选对方案并规范使用方式,才能避免代码膨胀和性能退化。

方案 实现原理 典型使用场景 上手成本 性能控制手段
plugin-model React Custom Hook + Context 广播 用户身份、购物车、页面跨级通信、通用全局标记 低(原生 React Hooks) useModel(key, selector) 字段级订阅
@@initialState 应用启动异步函数 + 预置 Model 权限元数据、登录态、基础配置字典 低(框架内置) 仅存放启动常量与登录基石,不存大列表
Dva Plugin Redux Store + Redux-Saga 复杂业务工作流、连续异步串联、老项目重构 高(Action/Saga/Reducer) useSelector 细粒度读取,避免全局 connect
局部 useState React 组件本地状态 表单字段、Modal 弹窗显隐、选项卡激活项 极低 状态下沉,随组件卸载自动销毁
怎么在 UmiJS 中管理全局状态和数据流
图 4:UmiJS 数据流方案选型决策树与特性对比

常见陷阱与排查清单

  1. 避免“状态提升泛滥”:很多开发者习惯把每一个页面的表单临时输入、弹窗开关状态都写进 src/models。这不仅会污染全局命名空间,还会延长内存驻留周期。只有至少两个跨路由页面或深层级无关组件都需要共享的数据,才有资格进入全局 Model。
  2. 谨慎在非 React 上下文中调用状态:useModel 必须且只能在 React 函数组件内使用。如果你需要在纯 Axios 拦截器或工具函数中获取当前 Token,正确的做法是从 localStorage 读取,或者通过闭包导出引用的方式传递,切忌在普通 JS 函数中违规执行 Hook。
  3. 不要在 Model 内部执行无清理机制的长监听:如果在 plugin-model 的 Hook 内部绑定了 window.addEventListener 或 setInterval,务必使用 useEffect 并在返回函数中清理,避免内存泄漏。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章