TypeScript 起死回生!Redux 5 官宣使用 TS 完全重构
大家好,这里是大家的林语冰。
前阵子“Ruby On Rails”之父 D.H.H. 官宣 Turbo 8 弃用 TS,一时“大型项目弃用 TS”/“TS 已死”闹得沸沸扬扬。
不久前,Redux 团队官宣 Redux 5 正式升级,亮点之一就是使用 TS 完全重构。就统计学意义而言,Redux 的 GitHub stars 高达 60_100
(截至本文发布),比 up 主的女粉还多,大约是 Turbo 8 的 10 倍,重新定义了什么叫做“大型项目”。
反正 up 主是“类型体操”都整不明白的 JS 爱好者,我们就先抛开 TS 不谈,来讲讲 Redux。
Redux 在前端生态的状态管理库领域赫赫有名或臭名昭著,有名当然是大数据显示其女粉超多,臭名源于一个悖论 —— 为什么 Redux 源码有且仅有几百行,而 Redux 相关文章却有一大坨,最骚的是,还没讲明白 Redux 是什么鬼物!
这是因为,Redux 大约是前端状态管理的“第一性原理”(更底层的可能是 Flux/Event Sourcing),所有后 Redux 时代的状态管理库,多多少少都受到其影响。
所以 Redux 是前端必知必会的一个库,但一大坨前端人却敬而远之,比如“我是 Vue 爱好者我不需要学 Redux”的思想钢印,其实来源于“Redux 能且仅能在 React 中使用”的认知偏差。
如果 Redux 能且仅能在 React 中使用,那 React-Redux 可以当场退役了。Redux 的“文艺复兴”可以让我们更愉快地玩弄 Vuex/Pinia、MobX/zustand/Jotai 等,这才是其精髓所在。
话说回来,以前 up 主的思想钢印是大型项目才需要 TS,现在 Redux 的源码有且仅有几百行,也使用 TS 重构,某种意义上也说明 TS/静态类型方案真正实现全民普及。
关于“Redux 的跨框架论文”和“reactive
之后不再需要 Pinia 的质疑”,我们日后再说。本期《前端翻译计划》共享的是 Redux 的版本升级公告,强烈建议和官方文档的迁移指南梦幻联动,以及关注 RTK 的进展。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 Redux v5.0.0。
Redux 5 升级速览
Redux 5 的重大升级包括但不限于:
- 将代码库完全转换为 TS
- 更新打包,更好地兼容 ESM/CJS,构建输出现代化
action.type
必须是字符串- 继续将
createStore
标记为已弃用 - 弃用
AnyAction
类型,取而代之的是UnknownAction
类型 - 移除
PreloadedState
类型,取而代之的是Reducer
类型的新泛型参数
此版本是所有 Redux 软件包主版本的一部分,涵盖破坏性更新:
- RTK(Redux Toolkit,工具集)2.0
- Redux core(核心包)5.0
- React-Redux 9.0
- Reselect 5.0
- Redux Thunk 3.0
Redux core、Reselect 和 Redux Thunk 作为 RTK 的一部分,RTK 用户无需手动升级它们 —— 您在升级到 RTK 2.0 时自动获取。(如果您尚未使用 RTK,请立即开始迁移现存的旧版 Redux 代码以使用 RTK!)
# RTK
pnpm add @reduxjs/toolkit
# 单独安装核心包
pnpm add redux
关于 TS 的变更
我们已经放弃了对 TS 4.6 及更早版本的支持,我们的支持矩阵现在是 TS 4.7+。
使用 TS 完全重构
2019 年,我们开始由社区驱动将 Redux 代码库转换为 TS。虽然但是,由于担心与现存生态系统可能存在兼容性问题(以及我们的普遍惯性),TS 转换的代码在存储库中放置了几年,未使用且未发布。
Redux core v5 现在是基于 TS 转换的源码构建。理论上,这在运行时行为和类型上应该与 Redux 4.x 构建几乎相同,但某些更改很可能会导致类型问题。
AnyAction
已弃用,取而代之的是 UnknownAction
Redux TS 类型始终导出 AnyAction
类型,该类型被定义为具有 {type: string}
并将任何其他字段视为 any
。这使得编写诸如 console.log(action.whatever)
之类的用法变得很容易,但不幸的是,这没有提供任何有意义的类型安全。
我们现在导出 UnknownAction
类型,它将除 action.type
之外的所有字段视为 unknown
。这鼓励用户编写类型守卫来检查 action
对象并断言其特定的 TS 类型。在这些检查中,您可以访问具有更好类型安全性的字段。
UnknownAction
现在是 Redux 源码中需要 action
对象的默认位置。
AnyAction
出于兼容性考虑仍然存在,但已被标记为已弃用。
请注意,RTK的 action
创建器有一个 .match()
方法,该方法充当有用的类型守卫:
if (todoAdded.match(someUnknownAction)) {
// action 现在被类型注解为 PayloadAction<Todo>
}
您还可以使用新的 isAction
工具来检查未知值是否是某种 action
对象。
Middleware
类型已更改:中间件 action
和 next
的类型为 unknown
以前,next
参数的类型为传递的 D
类型参数,action
的类型为从调度类型中提取的 Action
。这些都不是一个安全的假设:
next
会被类型注解以获取所有调度扩展,包括链中较早且不再适用的扩展。- 就技术而言,
next
类型注解为redux 基本库实现的默认Dispatch
是最安全的,但这会导致next(action)
出错(因为我们无法保证action
实际上是一个Action
) —— 它不会考虑任何后续中间件,这些中间件除了在看到特定action
时所给出的action
之外返回任何内容。
- 就技术而言,
action
不一定是已知的action
,它实际上可以是任何东西 - 举个栗子,thunk
是一个没有.type
属性的函数(因此AnyAction
是不准确的)
我们已将 next
更改为 (action: unknown) => unknown
(这是准确的,我们不知道 next
期望或将返回什么),并更改了 action
(如上所述,这是准确的)。
为了安全地与 action
参数内的值或访问字段进行交互,您必须首先执行类型守卫检查来缩小类型范围,比如 isAction(action)
或 someActionCreator.match(action)
。
这个新类型与 Redux v4 Middleware
类型不兼容,因此如果包的中间件说它不兼容,请检查它从哪个版本的 Redux 获取类型!
移除 PreloadedState
类型,取而代之的是 Reducer
泛型
我们对 TS 类型进行了调整,以提高类型安全性和行为。
首先,Reducer
类型现在有一个 PreloadedState
可能的泛型:
type Reducer<S, A extends Action, PreloadedState = S> = (
state: S | PreloadedState | undefined,
action: A
) => S
为什么需要进行此更改?当 store
首次由 createStore/configureStore
创建时,初始状态设置为作为 preloadedState
参数(或若没有通过,则是 undefined
)。这意味着,首次调用 reducer
时,会使用 preloadedState
进行调用。首次调用后,reducer
始终会传递当前状态(即 S
)。
对于大多数普通的 reducer
,S | undefined
准确地描述了 preloadedState
可以传入的内容。虽然但是,combineReducers
函数允许预加载 Partial<S> | undefined
状态。
解决方案是使用一个单独的泛型来表示 reducer
接受的预加载状态。这样 createStore
就可以使用该泛型作为其 preloadedState
参数。
以前,这是由 $CombinedState
类型处理的,但这使事情复杂化,并导致了某些用户报告的问题。这完全消除了对 $CombinedState
的需要。
此更改确实涵盖某些破坏性更新,但总体而言不会对用户域中的用户升级产生巨大影响:
Reducer
、ReducersMapObject
和createStore/configureStore
类型/函数采用额外的PreloadedState
泛型,默认为S
。- 移除
combineReducers
的重载,取而代之的是采用ReducersMapObject
作为其通用参数的单个函数定义。对于这些更改,移除重载是必要的,因为有时会选择错误的重载。 - 明确列出
reducer
泛型的增强器将需要添加第三个泛型。
isAction
谓词
我们最近向 RTK 添加了一个 isAction
谓词,然后意识到它更适合 Redux 核心库。这可以用在任何有可能是 Redux action
对象的值的地方,并且您需要检查它是否实际上是一个 action
。这对于与更新的 Redux 中间件 TS 类型一起使用特别有用,其中默认值现在是 unknown
,并且您需要使用类型守卫来告诉 TS 当前值实际上是一个 action
。
Action 类型必须是字符串
我们始终明示用户 action
和 state
必须是可序列化的,并且 action.type
应该是一个字符串。这既是为了确保 action
是可序列化的,也是为了在 Redux DevTools(开发者工具)中提供可读的 action
历史记录。
store.dispatch(action)
现在强行要求 action.type
必须是字符串,如果不是字符串就会报错,就像如果 action
不是普通对象一样,它会报错。
实际上,99.99% 的情况下这问题不大,且不会对用户(尤其是使用 RTK 和 createSlice
的用户)产生任何影响,但可能有某些旧版 Redux 代码库选择使用 Symbols
作为 action
的类型。
createStore
标记为已弃用
在 Redux 4.2.0 中,我们将原始的 createStore
方法标记为 @deprecated
。严格而言,这不是一个破坏性更新,也不是 Redux 5.0 的新内容,但为了完整起见,我们在这里记录它。
此弃用只是一条视觉删除线,旨在鼓励用户将其 App 从旧版 Redux 模式迁移到现代 RTK API。
弃用会导致导入和使用时出现视觉删除线,比如 ,但不会出现运行时错误或警告。createStore
createStore
将无期打工,且永远不会被移除。虽然但是,今天我们希望所有 Redux 用户都使用 RTK 来实现所有 Redux 逻辑。
要解决此问题,有以下三个选项:
- 强烈建议切换到 RTK 和
configureStore
- 当做无事发生。这只是一条视觉删除线,它不会影响代码的行为方式。请无视它。
- 切换到使用现在导出的
legacy_createStore
API,这是完全相同的函数,但没有@deprecated
标记。最简单的选择是执行别名导入重命名,比如import { legacy_createStore as createStore } from 'redux'
ESM/CJS 封装兼容性
Redux v5 和 RTK 2.0 版本的最大主题是尝试实现“真正的”ESM 包发布兼容性,同时仍支持已发布包中的 CJS。
主要构建产物现在是 ESM 文件 dist/redux.mjs
。大多数构建工具应该能够识别这一点。还有一个 CJS 工件和名为 redux.legacy-esm.js
的 ESM 文件的第二副本,用于支持 Webpack 4(它无法识别 package.json
中的 exports
字段)。此外,所有构建产物现在都位于已发布包中的 ./dist/
目录下。
现代化的构建输出
我们现在发布了针对 ES2020 的现代 JS 语法,包括但不限于 ?.
可选链运算符、{...}
对象扩展运算符和其他现代语法。
构建工具
我们现在正在使用 github.com/egoist/tsup 打包构建。我们现在还包括 ESM 和 CJS 产物的源码映射。
弃用 UMD 版本
Redux 始终附带 UMD 构建产物。这些主要用于作为脚本标签直接导入,比如在 CodePen 或无打包构建环境中。
我们已经从已发布的包中删除了这些构建产物,因为这些用例今天看起来相当罕见。
现在包中有一个 redux.browser.mjs
文件,可以从 CDN(比如 Unpkg)加载。
内部监听器实现
Redux store
始终使用数组来跟踪侦听器回调,并使用 listeners.findIndex
来移除取消订阅时的侦听器。正如我们在 React-Redux 中发现的那样,当许多侦听器同时取消订阅时,可能会出现性能问题。
在 React-Redux 中,我们使用更复杂的链表方法修复了这个问题。在这里,我们将 listeners
更新为存储在 Map
中,这比数组具有更好的移除性能。
实际上,这不应该产生任何实际效果,因为 React-Redux 在 <Provider>
中设置了订阅,并且所有嵌套组件都会订阅该订阅。虽然但是,很高兴在这里修复它。
我们还导出了 Redux 代码库中多年的 isPlainObject
工具。
您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~
转载自:https://juejin.cn/post/7311707089498390547