news 2026/9/14 7:24:40

umi @umi/max 数据流管理实战:model 插件、useModel 与全局初始状态

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
umi @umi/max 数据流管理实战:model 插件、useModel 与全局初始状态

umi @umi/max 数据流管理实战:model 插件、useModel 与全局初始状态

【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi

@umi/max内置了基于 hooks 范式的轻量级数据流管理方案,可以在 Umi 项目中以约定式目录的方式定义全局共享 Model,并通过useModel()在任意组件中消费;同时它还内置了全局初始状态(@@initialState)和 Qiankun 微前端父子应用通信能力。读完本文,你将掌握 Model 的创建规范与命名空间规则、model配置项(extraModelssort)的用法、useModel的 selector 性能优化技巧、getInitialState的全局初始状态机制,以及这套方案在仓库源码中的文件生成与运行时订阅实现原理。

配置

数据流管理插件通过model配置项开启,一个典型的配置示例如下(e.g.):

// config/config.ts export default { model: { extraModels: ['src/models/userModel.ts'], sort: (a, b) => a.namespace.localeCompare(b.namespace), }, };

对应地看插件入口 packages/plugins/src/model.ts,其api.describe中通过 zod 定义的 schema 与文档完全一致:extraModelsstring[]sort为可选函数;并且enableBy: api.EnableBy.config意味着该插件只有显式配置model后才会启用

extraModels

  • Type:string[]
  • Default:[]

配置extraModels后,插件会自动将这些 Model 文件添加到数据流管理中。

从源码结构看,extraModels并不是简单地追加路径。在 packages/plugins/src/model.ts 的getAllModels()中,它还会先执行api.applyPlugins({ key: 'addExtraModels' }),把其它插件(例如全局初始状态插件)注入的 Model 一并收集后合并:

// packages/plugins/src/model.ts(节选) return new ModelUtils(api, { astTest({ node }) { return t.isArrowFunctionExpression(node) || t.isFunctionDeclaration(node); }, }).getAllModels({ sort: {}, extraModels: [...extraModels, ...(api.config.model.extraModels || [])], });

也就是说,官方内置的@@initialState等“系统 Model”正是通过addExtraModels这个插件钩子被纳入同一个数据流体系的。

sort

  • Type:(a: Model, b: Model) => number
  • Default:(a, b) => a.namespace.localeCompare(b.namespace)

配置sort后,插件会根据sort函数返回的值对 Model 进行排序。实现上,在 packages/plugins/src/model.ts 的onGenerateFiles钩子中:

// packages/plugins/src/model.ts(节选) api.onGenerateFiles(async () => { const models = await getAllModels(api); if (api.userConfig.model?.sort) { models.sort(api.userConfig.model.sort); } // 随后生成 model.ts / index.tsx / runtime.tsx 三个临时文件 ... });

值得注意的是,ModelUtils.getAllModels内部还会基于 Model 之间useModel()调用关系做一次拓扑排序(见 packages/plugins/src/utils/modelUtils.ts 中的topologicalSort静态方法,它会遍历 Model 文件 AST 收集useModel('xxx')字面量依赖,若存在循环依赖会抛出Circle dependency detected in models错误)。用户配置的sort是在这之后覆盖最终顺序的,可用于控制多个 Model 的执行次序。

开始使用

创建 Model

数据流管理插件采用约定式目录结构,我们约定可以在src/modelssrc/pages/xxxx/models/目录中,以及src/pages/xxxx/model.{js,jsx,ts,tsx}文件引入 Model 文件。Model 文件允许使用.(tsx|ts|jsx|js)四种后缀格式,命名空间(namespace)生成规则如下。

路径命名空间说明
src/models/count.tscountsrc/models目录下不支持目录嵌套定义 model
src/pages/pageA/model.tspageA.model
src/pages/pageB/models/product.tspageB.product
src/pages/pageB/models/fruit/apple.tspageB.fruit.applepages/xxx/models下 model 定义支持嵌套定义

命名空间的生成逻辑可以直接在 packages/plugins/src/utils/modelUtils.ts 的getNamespace()函数中找到印证:

// packages/plugins/src/utils/modelUtils.ts(节选) export function getNamespace(absFilePath: string, absSrcPath: string) { const relPath = winPath(relative(winPath(absSrcPath), winPath(absFilePath))); const parts = relPath.split('/'); const dirs = parts.slice(0, -1); const file = parts[parts.length - 1]; // src/pages/foo/models/bar > foo/bar const validDirs = dirs.filter( (dir) => !['src', 'pages', 'models'].includes(dir), ); let normalizedFile = basename(file, extname(file)); // foo.model > foo if (normalizedFile.endsWith('.model')) { normalizedFile = normalizedFile.split('.').slice(0, -1).join('.'); } return [...validDirs, normalizedFile].join('.'); }

可以看到,实现上会过滤掉srcpagesmodels三段目录,再把剩余目录与文件名(去掉扩展名,并剥掉.model后缀)用.连接——这与上表的规则一一对应。

所谓的 Model,就是一个自定义的hooks,没有任何使用者需要关注的“黑魔法”。当我们需要获取 Model 中的全局数据时,调用该命名空间即可。例如,对于 Model 文件userModel.ts,它的命名空间为userModel

编写一个默认导出的函数:

// src/models/userModel.ts export default () => { const user = { username: 'umi', }; return { user }; };

这就是一个 Model。插件所做的工作就是将其中的状态或数据变成了全局数据,不同的组件在使用该 Model 时,拿到的是同一份状态或数据。

提示:Model 文件需要默认导出一个函数,此函数定义了一个hook。对于不符合此规范的文件,将会被过滤掉,并无法通过命名空间调用。

这一点在源码中有明确体现:packages/plugins/src/model.ts 构造ModelUtils时传入了astTest,而ModelUtils.isModelValid(packages/plugins/src/utils/modelUtils.ts)会用 esbuild 先转译文件内容、再用 Babel 遍历 AST,仅当ExportDefaultDeclaration箭头函数或函数声明时才判定为合法 Model。此外getModels还会直接过滤掉.d.ts文件和.test/.e2e/.spec测试文件,并检查命名空间是否重复(重复时抛出Duplicate namespace in models错误)。

Model 中允许使用其它hooks,以计数器为例:

// src/models/counterModel.ts import { useState, useCallback } from 'react'; export default function Page() { const [counter, setCounter] = useState(0); const increment = useCallback(() => setCounter((c) => c + 1), []); const decrement = useCallback(() => setCounter((c) => c - 1), []); return { counter, increment, decrement }; };

在项目实践中,我们通常需要请求后端接口,来获取所需的数据。现在让我们来扩展前面获取用户信息的例子:

// src/models/userModel.ts import { useState, useEffect } from 'react'; import { getUser } from '@/services/user'; export default function Page() { const [user, setUser] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { getUser().then((res) => { setUser(res); setLoading(false); }); }, []); return { user, loading, }; };

如果你项目中已经引入了 ahooks,可以像这样组织代码:

// src/models/userModel.ts import { useRequest } from 'ahooks'; import { getUser } from '@/services/user'; export default function Page() { const { data: user, loading: loading } = useRequest(async () => { const res = await getUser(); if (res) { return res; } return {}; }); return { user, loading, }; };

Model 之间也可以互相调用。仓库中的示例 examples/with-use-model/models/count.ts 就展示了一个 Model 内部消费另一个 Model 与全局初始状态的写法:

// examples/with-use-model/models/count.ts import { useModel } from 'umi'; export default function () { const { todos } = useModel('todo'); const initialState = useModel('@@initialState'); console.log('todos length', todos); console.log('initialState', initialState); return { total: 123, }; }

使用 Model

现在,您想要在某个组件中使用全局的 Model。以用户信息为例,只需要调用useModel这一钩子函数:

// src/components/Username/index.tsx import { useModel } from 'umi'; export default function Page() { const { user, loading } = useModel('userModel'); return ( {loading ? <></>: <div>{user.username}</div>} ); }

其中,useModel()方法传入的参数为 Model 的命名空间

提示:如果您使用 VSCode 作为 Umi 项目开发的 IDE,推荐搭配@umijs/plugin-model插件使用。它允许您快速跳转到定义 Model 的文件,便于在大型项目中按命名空间定位源码。

性能优化

useModel()方法可以接受可选的第二个参数,当组件只需要使用 Model 中的部分参数,而对其它参数的变化不感兴趣时,可以传入一个函数进行过滤。以实现计数器的操作按钮为例:

// src/components/CounterActions/index.tsx import { useModel } from 'umi'; export default function Page() { const { add, minus } = useModel('counterModel', (model) => ({ add: model.increment, minus: model.decrement, })); return ( <div> <button onClick={add}>add by 1</button> <button onClick={minus}>minus by 1</button> </div> ); };

上面的组件并不关心计数器 Model 中的counter值,只需要使用 Model 提供的increment()decrement()方法。于是我们传入了一个函数作为useModel()方法的第二个参数,该函数的返回值将作为useModel()方法的返回值。

这样,我们过滤掉了counter这一频繁变化的值,避免了组件重复渲染带来的性能损失。

这个优化在实现层面有真实依据。packages/plugins/libs/model.tsx 中useModel的订阅回调里,每次全局 data 更新时都会先执行selector,再用fast-deep-equal对 selector 结果做深度比较

// packages/plugins/libs/model.tsx(节选) const currentState = selectorRef.current ? selectorRef.current(data) : data; const previousState = stateRef.current; if (!isEqual(currentState, previousState)) { stateRef.current = currentState; setState(currentState); }

也就是说,即使 Model 整体数据(例如counter)频繁变化,只要 selector 投影出的{ add, minus }深比较相等,组件就不会触发setState,从而跳过重渲染。

全局初始状态

@umi/max内置了全局初始状态管理插件(实现见 packages/plugins/src/initial-state.ts),允许您快速构建并在组件内获取 Umi 项目全局的初始状态。

全局初始状态是一种特殊的 Model。它在整个 Umi 项目的最开始创建。编写src/app.ts的导出方法getInitialState(),其返回值将成为全局初始状态。例如:

// src/app.ts import { fetchInitialData } from '@/services/initial'; export async function getInitialState() { const initialData = await fetchInitialData(); return initialData; }

仓库中的示例 examples/with-use-model/app.ts 给出了一个可直接运行的最小版本:

// examples/with-use-model/app.ts async function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } export async function getInitialState() { await delay(500); return { name: 'Big Fish', size: 'big', color: 'blue', mood: 'happy', food: 'fish', location: 'sea', }; }

现在,各种插件和您定义的组件都可以通过useModel('@@initialState')直接获取到这份全局的初始状态,如下所示:

import { useModel } from 'umi'; export default function Page() { const { initialState, loading, error, refresh, setInitialState } = useModel('@@initialState'); return <>{initialState}</>; };

示例页面 examples/with-use-model/pages/index.tsx 同时消费了自定义 Model 与初始状态:

export default function HomePage() { const { todos } = useModel('todo'); const { total } = useModel('count'); // ... }
对象属性类型介绍
initialStateany导出的getInitialState()方法的返回值
loadingbooleangetInitialState()refresh()方法是否正在进行中。在首次获取到初始状态前,页面其他部分的渲染都会被阻止
errorError如果导出的getInitialState()方法运行时报错,报错的错误信息
refresh() => void重新执行getInitialState方法,并获取新的全局初始状态
setInitialState(state: any) => void手动设置initialState的值,手动设置完毕会将loading置为false

从源码实现看,@@initialState本身就是通过addExtraModels钩子注册进 model 体系的(packages/plugins/src/initial-state.ts 中api.register({ key: 'addExtraModels' })),命名空间为@@initialState。当src/app.ts中导出了getInitialState时,插件会在临时目录生成对应的 Model 实现,其核心逻辑即:useState持有{ initialState, loading, error }三元组,refreshuseCallback包裹的重新执行函数(先置loading: true,成功后写入initialState,失败则记录error),setInitialState支持直接赋值或函数式更新。

此外,该插件还会生成一个InitialStateProviderProvider.tsx):在loadingtrue且应用尚未完成首次加载时,它会渲染加载占位组件并阻止props.children渲染,这正是属性表中“首次获取到初始状态前页面渲染被阻止”的实现来源。initialState配置项还支持loading字段(Type:string),用于指定自定义的 Loading 组件路径;不配置时使用内置的空div占位。若src/app.ts未导出getInitialState,生成的 Model 会退化为() => ({ loading: false, refresh: () => {} })useModel('@@initialState')依然可安全调用。

Qiankun 父子应用间通信

@umi/max内置了Qiankun 微前端插件(实现见 packages/plugins/src/qiankun.ts),当使用数据流插件时,它允许微应用通过useModel('@@qiankunStateFromMaster')方法获取父应用传递给子应用的数据 Model,进而实现父子应用间的通信。

从源码结构看,子应用侧的@@qiankunStateFromMasterModel 由 packages/plugins/libs/qiankun/slave/qiankunModel.ts 提供,它是一个极简的useState封装:父应用每次下发新 state 时,通过模块级的setModelState触发setState,子应用组件随之以响应式方式更新:

// packages/plugins/libs/qiankun/slave/qiankunModel.ts let initState: any; let setModelState = (val: any) => { initState = val; }; export default () => { const [state, setState] = useState(initState); setModelState = (val: any) => { initState = val; setState(val); }; return state; }; export { setModelState };

具体的使用方法请查阅 微前端的父子应用通信章节。

API

useModel

useModel()是一个钩子函数,提供了使用 Model 的能力。它接受两个参数:

参数类型介绍
namespaceStringModel 文件的命名空间
updater(model: any) => any可选参数。传入一个函数,函数的返回值为当前组件中需要使用到的 Model 状态或数据,并作为useModel()方法的返回值。对优化组件性能具有重要意义。
// src/components/AdminInfo/index.tsx import { useModel } from 'umi'; export default function Page() { const { user, fetchUser } = useModel('adminModel', (model) => ({ user: model.admin, fetchUser: model.fetchAdmin, })); return <>hello</>; };

值得一提的是,useModel的实现(packages/plugins/libs/model.tsx)基于编译期生成的 Model 注册表提供了类型推导:Namespaces类型由所有已注册 Model 的namespace字段联合而成,useModel<N>(namespace)的返回类型即对应 Model hook 的ReturnType,因此命名空间拼写错误会得到类型提示,updater参数的入参也是精确的 Model 返回结构。

底层实现速览:从 Model 文件到运行时 Provider

将上述各部分串起来,可以完整还原这套数据流方案的工作链路,便于理解它为什么是“无黑魔法”的:

  1. 收集onGenerateFiles阶段按三个约定路径 glob 扫描 Model 文件(src/models/**src/pages/**/models/**src/pages/**/model.*,扩展名ts/tsx/js/jsx),叠加extraModelsaddExtraModels钩子注入的 Model(如@@initialState),并完成“默认导出函数”校验、重复命名空间检查与拓扑排序(packages/plugins/src/utils/modelUtils.ts)。
  2. 生成:插件在临时目录生成三个文件(packages/plugins/src/model.ts):
    • model.ts:静态导入全部 Model,导出{ [id]: { namespace, model } }注册表;
    • index.tsx:即 packages/plugins/libs/model.tsx,包含ProviderExecutorDispatcheruseModel运行时;
    • runtime.tsx:导出dataflowProvider(container, opts),以useMemo将注册表按namespace建索引后注入Provider,并作为运行时插件挂载到应用外层。
  3. 运行Provider为每个 Model 渲染一个Executor组件——它调用 Model hook,将返回值写入Dispatcher.data[namespace],并在 state 变化时通过dispatcher.update(namespace)通知该命名空间下的所有订阅者;useModel则通过 React Context 拿到Dispatcher,注册回调并按 selector 投影 + 深比较决定是否更新本地 state。
  4. 热更新api.addTmpGenerateWatcherPathssrc/models目录加入监听,在开发模式下新增/删除 Model 文件会触发临时文件重新生成。

由于 Model 本质就是普通 hook、共享状态由一个模块级Dispatcher单例承载,整套方案没有引入额外的状态库依赖,也没有跨组件的状态转发魔法——组件读到的数据始终来自同一个 Model 实例的返回值,这正是文档开头所说“不同的组件在使用该 Model 时,拿到的是同一份状态或数据”的源码级解释。

小结

  • 数据流插件需在model配置中显式开启,extraModels可补充约定目录之外的 Model 文件,sort可覆盖 Model 的默认排序;
  • Model 即“默认导出函数的 hook 文件”,命名空间由约定式路径推导,src/models下不支持目录嵌套而pages/xxx/models下支持;
  • useModel(namespace, updater)消费数据时,利用 selector 投影可跳过不关心字段的重复渲染;
  • 全局初始状态通过src/app.tsgetInitialState()提供,useModel('@@initialState')可获取initialState / loading / error / refresh / setInitialState,首次加载完成前页面渲染会被 Loading Provider 阻止;
  • Qiankun 子应用可经useModel('@@qiankunStateFromMaster')与父应用通信,详见 微前端章节。

如果你希望快速验证以上行为,仓库中的 examples/with-use-model 示例提供了getInitialState(app.ts)、Model 间相互引用(models/count.ts)与页面消费(pages/index.tsx)的完整可运行参照。

【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 7:23:32

ESP32-S3 N16R8开发板从零配置指南:环境搭建与项目实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 7:23:21

程序化工具调用与动态工作流引擎:解决Agent嵌套参数失控问题

在最近的Agent项目里&#xff0c;我发现自己不是在优化Prompt&#xff0c;而是在反复修补工具接口。典型的场景是&#xff1a;用户说“帮我查一下某只股票的行情&#xff0c;顺便看一下大盘走势”&#xff0c;模型理解得很准&#xff0c;结果一落到工具调用上&#xff0c;嵌套的…

作者头像 李华
网站建设 2026/9/14 7:22:24

COMSOL激光加工仿真:从烧蚀到沉积的多物理场建模指南

1. 为什么我用 COMSOL 折腾激光材料加工先说结论&#xff1a;激光和材料相互作用这件事&#xff0c;靠手算基本算不明白&#xff0c;靠实验硬试又太烧钱&#xff0c;COMSOL 属于那种“能把这个黑箱打开一条缝”的工具。我这两年主要拿它做激光烧蚀和激光沉积这两类仿真&#xf…

作者头像 李华
网站建设 2026/9/14 7:22:08

vue-neo4j可视化:Vue+D3自建Neo4j关系图谱

简介&#xff1a;面向需要在Web端实现图数据库可视化的前端开发者和数据可视化爱好者&#xff0c;这份源码工程演示了如何用Vue结合D3将Neo4j中的节点、关系与属性以交互式图谱形式呈现。项目为一个完整可运行的前端工程&#xff0c;包含Vue组件、D3绘图逻辑、路由与状态管理、…

作者头像 李华
网站建设 2026/9/14 7:16:38

Coze智能体与工作流:业务逻辑的可视化编程实战

1. 这不是又一个“点点点”教程&#xff1a;Coze智能体到底在解决什么真问题&#xff1f;你刷到这个标题时&#xff0c;大概率正被三类事情困扰&#xff1a;第一&#xff0c;手头有个具体业务场景——比如要给销售团队做个自动问答助手&#xff0c;或者想让客服话术生成更贴合产…

作者头像 李华