资讯动态

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

发布时间:2026/9/14 7:24:56 来源:尧图企业网站定制
umi umi/max 数据流管理实战model 插件、useModel 与全局初始状态【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umiumi/max内置了基于 hooks 范式的轻量级数据流管理方案可以在 Umi 项目中以约定式目录的方式定义全局共享 Model并通过useModel()在任意组件中消费同时它还内置了全局初始状态initialState和 Qiankun 微前端父子应用通信能力。读完本文你将掌握 Model 的创建规范与命名空间规则、model配置项extraModels、sort的用法、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 与文档完全一致extraModels为string[]sort为可选函数并且enableBy: api.EnableBy.config意味着该插件只有显式配置model后才会启用。extraModelsType: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这个插件钩子被纳入同一个数据流体系的。sortType:(a: Model, b: Model) numberDefault:(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/models、src/pages/xxxx/models/目录中以及src/pages/xxxx/model.{js,jsx,ts,tsx}文件引入 Model 文件。Model 文件允许使用.(tsx|ts|jsx|js)四种后缀格式命名空间namespace生成规则如下。路径命名空间说明src/models/count.tscountsrc/models目录下不支持目录嵌套定义 modelsrc/pages/pageA/model.tspageA.modelsrc/pages/pageB/models/product.tspageB.productsrc/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(.); }可以看到实现上会过滤掉src、pages、models三段目录再把剩余目录与文件名去掉扩展名并剥掉.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.isModelValidpackages/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 }三元组refresh为useCallback包裹的重新执行函数先置loading: true成功后写入initialState失败则记录errorsetInitialState支持直接赋值或函数式更新。此外该插件还会生成一个InitialStateProviderProvider.tsx在loading为true且应用尚未完成首次加载时它会渲染加载占位组件并阻止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 };具体的使用方法请查阅 微前端的父子应用通信章节。APIuseModeluseModel()是一个钩子函数提供了使用 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字段联合而成useModelN(namespace)的返回类型即对应 Model hook 的ReturnType因此命名空间拼写错误会得到类型提示updater参数的入参也是精确的 Model 返回结构。底层实现速览从 Model 文件到运行时 Provider将上述各部分串起来可以完整还原这套数据流方案的工作链路便于理解它为什么是“无黑魔法”的收集onGenerateFiles阶段按三个约定路径 glob 扫描 Model 文件src/models/**、src/pages/**/models/**、src/pages/**/model.*扩展名ts/tsx/js/jsx叠加extraModels与addExtraModels钩子注入的 Model如initialState并完成“默认导出函数”校验、重复命名空间检查与拓扑排序packages/plugins/src/utils/modelUtils.ts。生成插件在临时目录生成三个文件packages/plugins/src/model.tsmodel.ts静态导入全部 Model导出{ [id]: { namespace, model } }注册表index.tsx即 packages/plugins/libs/model.tsx包含Provider、Executor、Dispatcher与useModel运行时runtime.tsx导出dataflowProvider(container, opts)以useMemo将注册表按namespace建索引后注入Provider并作为运行时插件挂载到应用外层。运行Provider为每个 Model 渲染一个Executor组件——它调用 Model hook将返回值写入Dispatcher.data[namespace]并在 state 变化时通过dispatcher.update(namespace)通知该命名空间下的所有订阅者useModel则通过 React Context 拿到Dispatcher注册回调并按 selector 投影 深比较决定是否更新本地 state。热更新api.addTmpGenerateWatcherPaths将src/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.ts的getInitialState()提供useModel(initialState)可获取initialState / loading / error / refresh / setInitialState首次加载完成前页面渲染会被 Loading Provider 阻止Qiankun 子应用可经useModel(qiankunStateFromMaster)与父应用通信详见 微前端章节。如果你希望快速验证以上行为仓库中的 examples/with-use-model 示例提供了getInitialStateapp.ts、Model 间相互引用models/count.ts与页面消费pages/index.tsx的完整可运行参照。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价