将 1,100 个文件迁移到 Redux Toolkit v2,同时避免冻结 Kibana 单体仓库

发布时间:2026/8/31 5:05:08
将 1,100 个文件迁移到 Redux Toolkit v2,同时避免冻结 Kibana 单体仓库 作者来自 Elastic Walter RafelsbergerKibana 为 Redux Toolkit v2 设置了默认的软件包名称并将 v1 放到了一个显式别名上这颠倒了通常的迁移顺序。Webpack 外部依赖、yarn resolutions 和一条 ESLint 规则确保 React Redux v7 和 v9 互不干扰。测试 Elastic 最前沿的开箱即用功能。深入了解我们在 Elasticsearch Labs 仓库 中的示例 notebooks开始免费云试用或者现在就在你的本地机器上试用 Elastic。我们将 Kibana 单体仓库中大约 1,100 个文件迁移到了 Redux ToolkitRTKv2 别名上而且没有要求任何一个插件团队暂停功能开发。通常的迁移模式正好相反。现在默认的软件包名称reduxjs/toolkit、react-redux、redux解析到 v2而现有的 v1 代码则位于显式别名之后例如redux-toolkit-v1和react-redux-v7。两个版本同时存在于node_modules中并通过 npm 别名和 webpack 模块替换在运行时彼此隔离。一条作用于 36 个插件路径的 ESLint 规则会捕获任何试图跨越边界的代码。当某个团队准备好之后只需从该列表中删除自己的路径并切换回默认导入周围的团队则可以继续发布功能。为什么升级到 Redux Toolkit v2RTK v2 于 2023 年底发布。这意味着在 JavaScript 生态系统中使用最广泛的状态管理库之一中我们已经在一个落后了主要版本近三年的版本上运行。这也反映了在 Kibana 这样规模的代码库中这次升级有多么困难。一次之前的尝试采用了大爆炸式的方法但当实际范围变得更加清晰后便陷入停滞。那么v2 实际上带来了什么它与 Redux core 5.0、React-Redux 9.0、Reselect 5.0 和 Redux Thunk 3.0 一起发布。React-Redux 9.0 要求 React 18并移除了 v8 为 React 16/17 携带的useSyncExternalStoreshim。由于 Kibana 已经运行在 React 18 上升级可以去掉遗留的兼容代码并让 Kibana 保持在积极维护的 Redux 主版本上。RTK v2 还带来了真正有用的新功能包括createSlice中的内联选择器以及通过定制的buildCreateSlice设置选择性启用内联异步 thunk同时还提供了带有切片 reducer 注入功能的combineSlicesAPI用于代码拆分。最后这一点对于 Kibana 的插件架构尤其有趣因为延迟加载是其常态。Redux 在 Kibana 单体仓库中的使用方式在深入了解解决方案之前有必要先了解 Redux 在 Kibana 中的使用方式到底有多么多样化。对整个代码库进行的完整审计记录在 #239863 中发现了几个明显不同的阵营模式插件和软件包迁移需求Redux Toolkit v1Discover、Lens、Synthetics、Security Solution完整的 v1 到 v2 迁移Plain Redux v4Canvas、Maps、Index Management、Cross-Cluster Replication仅使用redux-v4别名不进行 RTK 迁移KeaEnterprise Search150 个文件、Content Connectors使用react-redux-v7别名不进行 RTK 迁移redux-sagaSynthetics、Graph、Uptime仅迁移 store 设置saga 与版本无关typescript-fsaSecurity Solution data-table 软件包不在范围内类型和单次导入Expressions、Monitoring仅交换别名Discover、Lens、Synthetics 和 Security Solution 等插件使用 RTK v1 API包括createSlice、configureStore、createAsyncThunk和createSelector。这些才是真正需要进行 v1 到 v2 迁移的部分。但即便如此复杂程度也存在很大差异。Lens 使用独立的getDefaultMiddleware在 v2 中已移除和PreloadedState同样已移除。Security Solution 是最大的使用者拥有 300 个文件将现代 RTK 与遗留的 Plain Redux 模式混合使用。Canvas、Maps、Index Management、Cross-Cluster Replication 以及其他几个插件仍然通过createStore、combineReducers、applyMiddleware和connect使用 Plain Redux v4这些都是 RTK 时代之前的经典模式。它们根本不需要进行 RTK 迁移因为它们一开始就没有使用 RTK但由于默认的redux软件包现在是 v5因此它们确实需要redux-v4别名。Enterprise Search 和 Content Connectors 使用kea这是一个带有自己逻辑构建器kea()、useValues、useActions的 Redux 抽象层。仅 Enterprise Search 就有超过 150 个文件。这里不适用 RTK 迁移因为kea有自己独立的体系。但它在底层确实依赖react-reduxv7这正是打包器技巧发挥作用的地方。Synthetics、Graph 和 Uptime 使用redux-saga处理副作用。Saga 集成实际上与 RTK 版本无关但这些插件需要迁移其 store 设置。Security Solution data-table 软件包完全没有使用 RTK而是使用typescript-fsa和typescript-fsa-reducers其 reducer 嵌入 Security Solution 的主 store 中因此也完全不属于 RTK 迁移范围。Expressions 插件只从react-redux导入shallowEqual而 Monitoring 只导入类型。这些只需要交换别名即可。要求每个团队同时进行迁移是完全不可行的。RTK v2 中的破坏性变更包括更严格的类型检查以及被移除的 API例如 immer 中的enableES5()、已经完全移除的getDefaultMiddleware和PreloadedState、由UnknownAction替代的AnyAction以及 middleware 配置方式上的行为变化。同时运行 Redux Toolkit v1 和 v2解决方案是彻底颠倒典型的迁移模式。与其让默认导入继续使用 v1再通过别名引入 v2不如现在让默认的软件包名称例如reduxjs/toolkit、react-redux和redux指向 v2。旧版本则使用带版本号的别名redux-toolkit-v1react-redux-v7redux-v4immer-v9reselect-v4redux-thunk-v2{ reduxjs/toolkit: 2.12.0, redux-toolkit-v1: npm:reduxjs/toolkit1.9.7, react-redux: 9.2.0, react-redux-v7: npm:react-redux7.2.8 }这是 npm 的别名语法。react-redux-v7: npm:react-redux7.2.8会将旧版本安装到一个不同的名称下。两个版本可以同时存在于node_modules中而不会发生冲突。这里的关键洞察是此拉取请求PR中的所有现有代码都被迁移到了 v1 别名。每一个import { useSelector } from react-redux都变成了import { useSelector } from react-redux-v7。这涉及约 1,100 个文件但其中绝大多数约 1,000 个只是机械地进行单行导入替换。当某个团队准备迁移到 v2 时只需切换回默认的导入名称即可。一旦代码库中所有 v1 别名都消失就可以彻底移除旧软件包。这避免了另一种方案即 v2 导入最终会永久使用非标准名称从长远来看导致代码库中一直存在非标准导入。通过打包器提供两个版本让同一个库的两个版本在运行时共存这才是真正有趣的地方。Kibana 使用kbn-ui-shared-deps-npm将公共依赖打包为共享的 webpack 外部依赖。这需要同时提供新的 v2 软件包和 v1 别名以便两个版本都能在运行时使用。使用 yarn resolutions 固定 elastic/charts然后是elastic/charts。它内部依赖 RTK v1而且由于它是一个上游软件包不能单独直接升级。yarn resolutions 将其嵌套依赖固定到 v1 版本{ elastic/charts/reduxjs/toolkit: npm:reduxjs/toolkit1.9.7 }共享依赖 webpack 配置中的NormalModuleReplacementPlugin会检测immer、reduxjs/toolkit、redux、react-redux或reselect的导入是否来自elastic/charts内部如果是则将解析重定向到嵌套的 v1 副本。这确保elastic/charts解析到与其兼容的 v1 依赖集合。使用 webpack 外部依赖让 Kea 保持在 React Redux v7kea库是另一个有趣的案例。它将react-redux声明为 peer dependency 7因此如果没有特殊处理它的导入会解析到 Kibana 默认的 v9 软件包。此次迁移让 Kea 的使用者继续使用react-redux-v7因此 Kea 必须使用相同的 React 上下文。解决方案使用基于函数的 webpack/rspack 外部依赖当导入来自node_modules/kea时跳过对react-redux的外部化同时结合NormalModuleReplacementPlugin将其重写为react-redux-v7。这样可以确保kea使用与其使用者外层Provider相匹配的 v7 React 上下文。webpackkbn-optimizer和 rspackkbn-rspack-optimizer配置都需要进行这些修改同时提取了一个共享的isKeaReactReduxImporthelper以保持逻辑一致。使用 ESLint 规则防止跨版本导入既然两个版本都可用意外的跨版本导入就是最大的风险。新的kbn/imports/no_redux_toolkit_v2_importsESLint 规则会捕获尚未完成迁移的代码中对 v2 默认软件包例如reduxjs/toolkit、react-redux或redux等的任何导入。它甚至可以自动将文件导入和 Jest mock 路径修复为 v1 别名。该规则通过.eslintrc.js中的 override 限定在目前使用 v1 的约 36 个插件和软件包路径上。当某个团队完成迁移后只需从 override 列表中删除自己的路径即可。这种简洁的自助式方式不需要任何协调。// .eslintrc.js (simplified) overrides: [{ files: [ src/platform/plugins/shared/discover/**/*.{ts,tsx}, src/platform/plugins/shared/workflows_management/**/*.{ts,tsx}, // ... 34 more paths ], rules: { kbn/imports/no_redux_toolkit_v2_imports: error, }, }]为什么混用 React Redux v7 和 v9 会破坏上下文这一点值得特别指出因为这是升级过程中一个很容易被忽略的故障模式。react-reduxv9 和 v7 会创建独立的 React 上下文。如果一个组件树顶部使用 v9 的Provider但子组件调用的是来自 v7 的useSelector反之亦然React-Redux 就无法找到匹配的上下文。在开发环境中它会抛出错误说明该组件必须使用匹配的Provider进行包装在生产环境中当 hook 访问 store 时缺失的上下文会导致运行时错误。Error: could not find react-redux context value; please ensure the component is wrapped in a Provider这意味着每个插件都需要明确固定到其中一个版本。使用react-redux的共享软件包只能被相同版本的代码使用因为两个版本无法混用。这一约束使得迁移天然需要以插件为单位而不是以文件为单位。迁移批次哪些内容可以独立迁移双版本设置为每个团队提供了清晰的前进路径而跟踪 issue 中的依赖关系图分析也确定了自然的迁移批次批次 1独立、自包含的 store。kbn-coloring、transform、timelines和expandable-flyout等软件包拥有完全内部的 Redux store其类型不会通过公共 API 泄漏。这些软件包可以由负责它们的团队独立迁移风险很低。批次 2相互耦合的软件包。一些软件包跨边界共享 RTK 类型必须一起迁移。机器学习ML/IT 运维人工智能AIOps链就是一个例子kbn/ml-response-stream导出一个streamSlice一个createSlice返回值而kbn/aiops-log-rate-analysis直接将其嵌入自己的configureStore中。如果只迁移其中一个就会导致 v1 和 v2 切片类型之间出现类型不匹配。Lens 生态系统中也存在类似的耦合。Lens 插件依赖kbn/coloring它拥有自己的 RTK store、kbn/lens-embeddable-utils和kbn/lens-common同时它自身又被图表表达式、可视化、Maps、Canvas 和可观测性插件中的 40 个软件包和插件使用。Redux 类型是否通过软件包的公共 API 泄漏决定了它能否独立迁移还是需要协调迁移。kbn-coloring的 store 位于其 React 组件内部因此可以安全地单独迁移但其他耦合点则需要仔细分析。批次 3大型项目。Discover、Security Solution 和 Lens 都有各自的迁移时间表。Security Solution 拥有 300 个文件并且混用了 RTK、Plain Redux v4 和typescript-fsa因此是规模最大的工作但这些不同模式可以分别处理。Lens 在 v2 中关于 middleware 配置的破坏性变更最为棘手独立的getDefaultMiddleware和PreloadedState在 v2 中都已移除而且它还有四个类型定义复杂的自定义 middleware 文件。除了分批迁移之外已弃用的功能可以继续使用 v1 别名。当功能被移除时v1 导入会随着代码删除而消失无需进行任何迁移工作。Plain Redux v4 插件Canvas、Maps 和其他插件完全不属于 RTK 迁移范围。它们确实可以从现代化中受益但这是另一个独立的工作。Kea 插件最终需要将react-redux-v7更新为react-redux别名但不需要进行 RTK 迁移。更长期的问题即是否继续使用 Kea还是迁移到 RTK v2是一个独立的决策。双版本方案会在迁移期间增加可量化的软件包体积开销。这是一种权衡但在迁移期间是可以接受的。对其他大型单体仓库升级的经验ESLint 规则最终成为关键环节。如果没有自动化强制措施带别名的导入会在几周内逐渐重新变回默认名称。有了它迁移状态可以通过 override 中列出的路径清晰可见。截至最初的 PR没有任何文件从reduxjs/toolkitv2 导入。所有 RTK 使用都通过redux-toolkit-v1别名进行。这就是迁移的起点。准备工作也不仅限于导入路径。引用react-redux的 Jest mock 需要更新为react-redux-v7Storybook 预览、测试 helper 和环境类型声明也需要更新。多轮运行node scripts/eslint_all_files --no-cache --fix捕获了机械性的修改剩余情况则需要手动修复。如果你正在大型单体仓库中面对类似的主要依赖升级那么可以考虑采用这种模式让新版本使用默认名称让旧版本使用显式别名。新代码自然会使用当前版本而旧代码则会一直保持可见且可追踪直到其数量降为零。原文Redux Toolkit v2 migration: 1,100 files, no code freeze | Elasticsearch Labs