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

作者:来自 Elastic Walter Rafelsberger

Kibana 为 Redux Toolkit v2 设置了默认的软件包名称,并将 v1 放到了一个显式别名上,这颠倒了通常的迁移顺序。Webpack 外部依赖、yarn resolutions 和一条 ESLint 规则确保 React Redux v7 和 v9 互不干扰。

测试 Elastic 最前沿的开箱即用功能。深入了解我们在 Elasticsearch Labs 仓库 中的示例 notebooks,开始免费云试用,或者现在就在你的本地机器上试用 Elastic

我们将 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 这样规模的代码库中,这次升级有多么困难。一次之前的尝试采用了大爆炸式的方法,但当实际范围变得更加清晰后便陷入停滞。

那么,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,同时还提供了带有切片 reducer 注入功能的 combineSlices API,用于代码拆分。最后这一点对于 Kibana 的插件架构尤其有趣,因为延迟加载是其常态。

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

在深入了解解决方案之前,有必要先了解 Redux 在 Kibana 中的使用方式到底有多么多样化。对整个代码库进行的完整审计(记录在 #239863 中)发现了几个明显不同的阵营:

模式 插件和软件包 迁移需求
Redux Toolkit v1 Discover、Lens、Synthetics、Security Solution 完整的 v1 到 v2 迁移
Plain 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 与遗留的 Plain Redux 模式混合使用。

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

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

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

Security Solution data-table 软件包完全没有使用 RTK,而是使用 typescript-fsatypescript-fsa-reducers,其 reducer 嵌入 Security Solution 的主 store 中,因此也完全不属于 RTK 迁移范围。

Expressions 插件只从 react-redux 导入 shallowEqual,而 Monitoring 只导入类型。这些只需要交换别名即可。

要求每个团队同时进行迁移是完全不可行的。RTK v2 中的破坏性变更包括更严格的类型检查以及被移除的 API,例如 immer 中的 enableES5()、已经完全移除的 getDefaultMiddlewarePreloadedState、由 UnknownAction 替代的 AnyAction,以及 middleware 配置方式上的行为变化。

同时运行 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

scss 复制代码
`

1.  {
2.  "@reduxjs/toolkit": "2.12.0",
3.  "redux-toolkit-v1": "npm:@reduxjs/toolkit@1.9.7",
4.  "react-redux": "9.2.0",
5.  "react-redux-v7": "npm:react-redux@7.2.8"
6.  }

`Lobster AI

这是 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 导入最终会永久使用非标准名称,从长远来看导致代码库中一直存在非标准导入。

通过打包器提供两个版本

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

使用 yarn resolutions 固定 @elastic/ charts

然后是 @elastic/charts。它内部依赖 RTK v1,而且由于它是一个上游软件包,不能单独直接升级。yarn resolutions 将其嵌套依赖固定到 v1 版本:

scss 复制代码
`

1.  {
2.  "@elastic/charts/@reduxjs/toolkit": "npm:@reduxjs/toolkit@1.9.7"
3.  }

`Lobster AI

共享依赖 webpack 配置中的 NormalModuleReplacementPlugin 会检测 immer@reduxjs/toolkitreduxreact-reduxreselect 的导入是否来自 @elastic/charts 内部,如果是,则将解析重定向到嵌套的 v1 副本。这确保 @elastic/charts 解析到与其兼容的 v1 依赖集合。

使用 webpack 外部依赖让 Kea 保持在 React Redux v7

kea 库是另一个有趣的案例。它将 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 上下文。

webpack(kbn-optimizer)和 rspack(kbn-rspack-optimizer)配置都需要进行这些修改,同时提取了一个共享的 isKeaReactReduxImport helper,以保持逻辑一致。

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

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

该规则通过 .eslintrc.js 中的 override 限定在目前使用 v1 的约 36 个插件和软件包路径上。当某个团队完成迁移后,只需从 override 列表中删除自己的路径即可。这种简洁的自助式方式不需要任何协调。

rust 复制代码
`

1.  // .eslintrc.js (simplified)
2.  overrides: [{
3.    files: [
4.  'src/platform/plugins/shared/discover/**/*.{ts,tsx}',
5.  'src/platform/plugins/shared/workflows_management/**/*.{ts,tsx}',
6.  // ... 34 more paths
7.  ],
8.    rules: {
9.  '@kbn/imports/no_redux_toolkit_v2_imports': 'error',
10.    },
11.  }]

`Lobster AI![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

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

这一点值得特别指出,因为这是升级过程中一个很容易被忽略的故障模式。react-redux v9 和 v7 会创建独立的 React 上下文。如果一个组件树顶部使用 v9 的 <Provider>,但子组件调用的是来自 v7 的 useSelector(反之亦然),React-Redux 就无法找到匹配的上下文。在开发环境中,它会抛出错误,说明该组件必须使用匹配的 <Provider> 进行包装;在生产环境中,当 hook 访问 store 时,缺失的上下文会导致运行时错误。

ini 复制代码
`Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>`Lobster AI

这意味着每个插件都需要明确固定到其中一个版本。使用 react-redux 的共享软件包只能被相同版本的代码使用,因为两个版本无法混用。这一约束使得迁移天然需要以插件为单位,而不是以文件为单位。

迁移批次:哪些内容可以独立迁移

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

  • 批次 1:独立、自包含的 store。 kbn-coloringtransformtimelinesexpandable-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 配置的破坏性变更最为棘手;独立的 getDefaultMiddlewarePreloadedState 在 v2 中都已移除,而且它还有四个类型定义复杂的自定义 middleware 文件。

除了分批迁移之外:

  • 已弃用的功能可以继续使用 v1 别名。当功能被移除时,v1 导入会随着代码删除而消失,无需进行任何迁移工作。

  • Plain Redux v4 插件(Canvas、Maps 和其他插件)完全不属于 RTK 迁移范围。它们确实可以从现代化中受益,但这是另一个独立的工作。

  • Kea 插件 最终需要将 react-redux-v7 更新为 react-redux 别名,但不需要进行 RTK 迁移。更长期的问题,即是否继续使用 Kea,还是迁移到 RTK v2,是一个独立的决策。

  • 双版本方案会在迁移期间增加可量化的软件包体积开销。这是一种权衡,但在迁移期间是可以接受的。

对其他大型单体仓库升级的经验

ESLint 规则最终成为关键环节。如果没有自动化强制措施,带别名的导入会在几周内逐渐重新变回默认名称。有了它,迁移状态可以通过 override 中列出的路径清晰可见。截至最初的 PR,没有任何文件从 @reduxjs/toolkit v2 导入。所有 RTK 使用都通过 redux-toolkit-v1 别名进行。这就是迁移的起点。

准备工作也不仅限于导入路径。引用 react-redux 的 Jest mock 需要更新为 react-redux-v7,Storybook 预览、测试 helper 和环境类型声明也需要更新。多轮运行 node scripts/eslint_all_files --no-cache --fix 捕获了机械性的修改;剩余情况则需要手动修复。

如果你正在大型单体仓库中面对类似的主要依赖升级,那么可以考虑采用这种模式:让新版本使用默认名称,让旧版本使用显式别名。新代码自然会使用当前版本,而旧代码则会一直保持可见且可追踪,直到其数量降为零。

原文:Redux Toolkit v2 migration: 1,100 files, no code freeze | Elasticsearch Labs

相关推荐
Elasticsearch1 小时前
从 582 毫秒的延迟峰值追踪到负责该服务的团队:使用 Kibana Discover
elasticsearch
Jinkxs4 小时前
SkyWalking - 生产级部署:集成 Elasticsearch 作为后端存储
大数据·elasticsearch·skywalking
醉颜凉5 小时前
Elasticsearch服务器部署:从零到一完整启动+配置教程
大数据·服务器·elasticsearch
zhougl9966 小时前
Elasticsearch 7.x vs 8.x
大数据·elasticsearch·jenkins
wdfk_prog8 小时前
GitHub push 失败:如何扫描并清理 Git 历史中的大文件
git·elasticsearch·github
mqiqe8 小时前
AgentScope Java 2.0 技能仓库(Skill Repository)完全实战指南
java·开发语言·elasticsearch
wangchunyu11410 小时前
Elasticsearch 入门与实战:Spring Boot 3 + ES 8 从零搭建商品搜索服务
大数据·数据库·spring boot·elasticsearch
隔窗听雨眠10 小时前
ARM架构下Logstash与Elasticsearch集群部署完全指南:从环境适配到生产验证
arm开发·elasticsearch·架构
小芒果_0111 小时前
git常用命令速查
大数据·git·elasticsearch