首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >将 1,100 个文件迁移至 Redux Toolkit v2,且不冻结 Kibana 单体仓库

将 1,100 个文件迁移至 Redux Toolkit v2,且不冻结 Kibana 单体仓库

原创
作者头像
点火三周
发布2026-08-30 21:42:11
发布2026-08-30 21:42:11
780
举报
文章被收录于专栏:Elastic Stack专栏Elastic Stack专栏

我们在 Kibana 单体仓库中大约将 1,100 个文件迁移到了 Redux Toolkit (RTK) v2 别名,而没有要求任何一个插件团队暂停功能开发。通常的迁移模式恰恰相反。默认的包名(@reduxjs/toolkitreact-reduxredux)现在解析为 v2,现有的 v1 代码位于显式别名之后,如 redux-toolkit-v1react-redux-v7。两个版本同时存在于 node_modules 中,通过 npm 别名和 webpack 模块替换在运行时隔离开。一条作用域覆盖 36 个插件路径的 ESLint 规则会捕获任何试图跨越的导入。当某个团队准备就绪时,它只需从该列表中删除自己的路径,然后切换回默认导入,其周围的团队继续正常发布。

为什么要升级到 Redux Toolkit v2?

RTK v2 于 2023 年底发布。这意味着在 JavaScript 生态中最广泛使用的状态管理库之一中,Kibana 已经落后了将近三个主要版本。这反映了在 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 携带的 useSyncExternalStore shim。由于 Kibana 已经运行在 React 18 上,升级可以消除遗留的兼容性代码,并使 Kibana 保持在积极维护的 Redux 主要版本上。

RTK v2 还带来了真正有用的新功能,包括 createSlice 中的内联选择器,以及通过自定义的 buildCreateSlice 设置实现的可选内联异步 thunk,还有用于代码分割的 combineSlices API(支持切片 reducer 注入)。对于以懒加载为常态的 Kibana 插件架构来说,最后一个功能尤其有趣。

Redux 在 Kibana 单体仓库中的使用方式

在深入解决方案之前,有必要了解 Redux 在 Kibana 中的使用有多么多样化。对代码库的全面审计(记录在 #239863 中)揭示了几个不同的阵营:

模式

插件和包

迁移需求

Redux Toolkit v1

Discover, Lens, Synthetics, Security Solution

完整的 v1 到 v2 迁移

纯 Redux v4

Canvas, Maps, Index Management, Cross-Cluster Replication

仅需 redux-v4 别名,无需 RTK 迁移

Kea

Enterprise Search (150+ 文件), Content Connectors

仅需 react-redux-v7 别名,无需 RTK 迁移

redux-saga

Synthetics, Graph, Uptime

仅需 Store 设置,saga 与版本无关

typescript-fsa

Security Solution data-table 包

不在范围之内

类型和单导入

Expressions, Monitoring

仅需别名交换

像 Discover、Lens、Synthetics 和 Security Solution 这样的插件使用了 RTK v1 的 API,包括 createSliceconfigureStorecreateAsyncThunkcreateSelector。这些是真正需要 v1 到 v2 迁移的插件。但即使在这些插件中,复杂性也差异巨大。Lens 使用了独立的 getDefaultMiddleware(在 v2 中已移除)和 PreloadedState(也已移除)。Security Solution 是最大的使用者,有 300 多个文件,混合了现代 RTK 和遗留的纯 Redux 模式。

Canvas、Maps、Index Management、Cross-Cluster Replication 以及其他几个插件仍然通过 createStorecombineReducersapplyMiddlewareconnect 使用纯 Redux v4,这些都是前 RTK 时代的经典模式。它们根本不需要 RTK 迁移,因为它们一开始就没有使用 RTK,但它们确实需要 redux-v4 别名,因为默认的 redux 包现在是 v5。

Enterprise Search 和 Content Connectors 使用了 kea,这是一个 Redux 抽象层,拥有自己的逻辑构建器(kea()useValuesuseActions)。仅 Enterprise Search 就有超过 150 个文件。RTK 迁移不适用于这里,因为 kea 是它自己的世界。但它确实在底层依赖 react-redux v7,这正是 bundler 技巧发挥作用的地方。

Synthetics、Graph 和 Uptime 使用 redux-saga 处理副作用。Saga 集成实际上与 RTK 版本无关,但这些插件需要迁移其 Store 设置。

Security Solution 的 data-table 包完全使用 typescript-fsatypescript-fsa-reducers 而不是 RTK,其 reducer 嵌入到 Security Solution 的主 Store 中,完全不在 RTK 迁移范围内。

Expressions 插件只从 react-redux 导入 shallowEqual,Monitoring 只导入类型。它们只需要别名交换。

要求每个团队同时迁移是不可行的。RTK v2 的破坏性变更包括更严格的类型检查和已移除的 API,例如 enableES5() 从 immer 中移除,getDefaultMiddlewarePreloadedState 完全消失,AnyActionUnknownAction 替换,以及中间件配置的行为变化。

同时运行 Redux Toolkit v1 和 v2

解决方案是将典型的迁移模式完全颠倒过来。不是将默认导入保留在 v1 上,并在别名下引入 v2,而是将默认包名(例如 @reduxjs/toolkitreact-reduxredux)现在指向 v2。旧版本位于版本化别名之下:

  • redux-toolkit-v1
  • react-redux-v7
  • redux-v4
  • immer-v9
  • reselect-v4
  • redux-thunk-v2
代码语言:json
复制
{
  "@reduxjs/toolkit": "2.12.0",
  "redux-toolkit-v1": "npm:@reduxjs/toolkit@1.9.7",
  "react-redux": "9.2.0",
  "react-redux-v7": "npm:react-redux@7.2.8"
}

这是 npm 的别名语法。"react-redux-v7": "npm:react-redux@7.2.8" 将旧版本以不同名称安装。两个版本在 node_modules 中并存,没有冲突。

这里的洞察是,这个拉取请求(PR)中的所有现有代码都被移到了 v1 别名。每个 import { useSelector } from 'react-redux' 都变成了 import { useSelector } from 'react-redux-v7'。这涉及大约 1,100 个文件,但绝大多数(约 1,000 个)是机械性的单行导入替换。当某个团队准备好迁移到 v2 时,他们切换回默认的导入名称。一旦所有 v1 别名从代码库中消失,旧包就可以完全移除。

这避免了另一种情况,即 v2 导入最终会永久地使用非标准名称,从而在代码库中长期留下非标准导入。

通过 bundler 同时提供两个版本

让同一个库的两个版本在运行时共存,这才是真正有趣的地方。Kibana 使用 kbn-ui-shared-deps-npm 将公共依赖打包为共享的 webpack externals。这需要同时提供新的 v2 包和 v1 别名,以便两者在运行时都可用。

使用 yarn resolutions 锁定 @elastic/charts

然后是 @elastic/charts。它在内部依赖 RTK v1,并且不能独立升级,因为它是一个上游包。yarn resolutions 将其嵌套依赖锁定到 v1 版本:

代码语言:json
复制
{
  "@elastic/charts/@reduxjs/toolkit": "npm:@reduxjs/toolkit@1.9.7"
}

共享依赖的 webpack 配置中的 NormalModuleReplacementPlugin 会检测何时从 @elastic/charts 内部导入 immer@reduxjs/toolkitreduxreact-reduxreselect,并将解析重定向到嵌套的 v1 副本。这确保了 @elastic/charts 解析到其兼容的 v1 依赖集。

使用 webpack externals 将 Kea 保留在 React Redux v7 上

kea 库是另一个有趣的案例。它将 react-redux 声明为对等依赖(>= 7),因此如果没有特殊处理,它的导入会解析到 Kibana 默认的 v9 包。迁移使 Kea 消费者保持在 react-redux-v7 上,因此 Kea 必须使用相同的 React 上下文。修复方案使用了基于函数的 webpack/rspack externals,当导入来自 node_modules/kea 时,它会跳过对 react-redux 的 externalize,并结合一个 NormalModuleReplacementPlugin 将其重写为 react-redux-v7。这确保了 kea 使用与包裹其消费者的 <Provider> 匹配的 v7 React 上下文。

webpack(kbn-optimizer)和 rspack(kbn-rspack-optimizer)配置都需要这些更改,并提取了一个共享的 isKeaReactReduxImport 辅助函数以保持逻辑一致性。

使用 ESLint 规则防止跨版本导入

当两个版本都可用时,意外的跨版本导入是最大的风险。一条新的 @kbn/imports/no_redux_toolkit_v2_imports ESLint 规则会捕获任何尚未迁移的代码中导入 v2 默认包(例如 @reduxjs/toolkitreact-reduxredux 等)的行为。它甚至会自动修复文件导入和 Jest mock 路径,将其改为 v1 别名。

该规则通过 .eslintrc.js 中的 override 作用域限定到当前使用 v1 的约 36 个插件和包路径。当某个团队迁移时,他们只需从 override 列表中删除自己的路径。这种干净、自助的方式无需任何协调。

代码语言:javascript
复制
// .eslintrc.js (简化版)overrides: [{  files: ['src/platform/plugins/shared/discover/**/*.{ts,tsx}','src/platform/plugins/shared/workflows_management/**/*.{ts,tsx}',// ... 另外 34 个路径],  rules: {'@kbn/imports/no_redux_toolkit_v2_imports': 'error',  },}]

为什么混合 React Redux v7 和 v9 会破坏上下文

这一点值得指出,因为在升级过程中很容易忽略这种失败模式。react-redux v9 和 v7 创建了不同的 React 上下文。如果组件树顶部有一个 v9 的 <Provider>,但子组件调用了 v7 的 useSelector(反之亦然),React-Redux 将无法找到匹配的上下文。在开发环境中,它会抛出一个错误,说明组件必须包裹在匹配的 <Provider> 中;在生产环境中,缺失的上下文会导致 hook 访问 store 时发生运行时错误。

代码语言:shell
复制
Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>

这意味着每个插件需要明确地固定在一个版本上。使用 react-redux 的共享包只能被同一版本的代码消费,因为混用是不可能的。这一约束使得迁移本质上是按插件进行,而不是按文件进行。

迁移批次:哪些可以独立移动

双版本设置为每个团队提供了清晰的路径,而跟踪问题中的依赖图分析确定了自然的迁移批次:

  • 批次 1:独立、自包含的 Store。kbn-coloringtransformtimelinesexpandable-flyout 这样的包拥有完全内部的 Redux Store,其类型不会通过公共 API 泄露。这些可以由其所属团队独立迁移,风险最小。
  • 批次 2:耦合的包。 一些包跨边界共享 RTK 类型,因此必须一起迁移。机器学习(ML)/人工智能运维(AIOps)链就是一个例子:@kbn/ml-response-stream 导出了一个 streamSlicecreateSlice 的返回值),而 @kbn/aiops-log-rate-analysis 将其直接嵌入到自己的 configureStore 中。只迁移其中之一而不迁移另一个会导致 v1 和 v2 切片类型不匹配。Lens 生态系统中也存在类似的耦合。Lens 插件依赖于 @kbn/coloring(它有自己的 RTK Store)、@kbn/lens-embeddable-utils@kbn/lens-common,同时自身又被 40 多个包和插件消费,涵盖图表表达式、可视化、Maps、Canvas 和可观测性插件。Redux 类型是否通过包的公共 API 泄露决定了它是可以独立迁移还是需要协调。kbn-coloring 的 Store 对其 React 组件是内部的,因此单独迁移是安全的,但其他耦合点需要仔细分析。
  • 批次 3+:大型插件。 Discover、Security Solution 和 Lens 各自有自己的迁移时间表。Security Solution 的 300 多个文件以及 RTK 与纯 Redux v4 和 typescript-fsa 的混合使其成为最大的工作,但不同的模式可以独立处理。Lens 在中间件配置方面有最棘手的 v2 破坏性变更;独立的 getDefaultMiddlewarePreloadedState 在 v2 中都被移除了,并且它有四个自定义中间件文件,类型复杂。

除了分批迁移:

  • 已弃用功能可以保留在 v1 别名上。当该功能被移除时,v1 导入会通过代码删除而消失,无需任何迁移工作。
  • 纯 Redux v4 插件(Canvas、Maps 等)完全不在 RTK 迁移范围内。它们会受益于现代化,但那是另一项计划。
  • Kea 插件最终需要将 react-redux-v7 别名更新为 react-redux,但无需 RTK 迁移。长期问题(是否保留 Kea 还是迁移到 RTK v2)是另一个决策。
  • 双版本方法在过渡期间会增加可量化的 bundle 开销。这是一个权衡,但在迁移期间是可以接受的。

对其他大型单体仓库升级的启示

ESLint 规则被证明是关键。没有自动化的强制措施,别名导入会在几周内漂回默认名称。有了它,迁移状态在 override 中列出的路径中可见。截至初始 PR,零个文件从 @reduxjs/toolkit v2 导入。每个 RTK 使用都通过 redux-toolkit-v1 别名。这是起跑线。

准备工作也超出了导入路径的范围。引用 react-redux 的 Jest mock 需要更新为 react-redux-v7,Storybook 预览、测试辅助函数和全局类型声明也是如此。多次运行 node scripts/eslint_all_files --no-cache --fix 捕获了机械性案例;剩余案例需要手动修复。

如果你在大型单体仓库中面临类似的主要依赖升级,那么将新版本赋予默认名称、旧版本赋予显式别名的模式值得考虑。新代码自然使用当前版本,而旧使用方式保持可见和可追踪,直到归零。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么要升级到 Redux Toolkit v2?
  • Redux 在 Kibana 单体仓库中的使用方式
  • 同时运行 Redux Toolkit v1 和 v2
  • 通过 bundler 同时提供两个版本
    • 使用 yarn resolutions 锁定 @elastic/charts
    • 使用 webpack externals 将 Kea 保留在 React Redux v7 上
  • 使用 ESLint 规则防止跨版本导入
  • 为什么混合 React Redux v7 和 v9 会破坏上下文
  • 迁移批次:哪些可以独立移动
  • 对其他大型单体仓库升级的启示
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档