目录
实际操作中的工资单引擎
TypeScript
reducer 作为纯函数
测试 reducer 函数
整合所有内容
结论
首页 web前端 js教程 深入研究Redux

深入研究Redux

Feb 14, 2025 am 10:13 AM

A Deep Dive into Redux

核心要点

  • Redux 通过充当可预测的状态容器来简化现代应用程序中的状态管理,这对于维护应用程序在扩展时的稳定性至关重要。
  • TypeScript 集成通过强制类型安全来增强 Redux,这增加了一层可预测性,并通过简化重构来帮助维护大型代码库。
  • Redux 中的 reducer 被设计为纯函数,确保它不会产生副作用,从而增强了状态管理的可测试性和可靠性。
  • 使用 Jest 可以简化单元测试,Jest 与 TypeScript 无缝协作,用于测试 Redux 动作和 reducer,确保每个组件都能按预期工作。
  • 本文通过构建一个工资单引擎演示了 Redux 的实际实现,展示了 Redux 如何在实际应用程序场景中管理状态转换和处理副作用。

构建有状态的现代应用程序是一项复杂的任务。随着状态的改变,应用程序变得不可预测且难以维护。这就是 Redux 的用武之地。Redux 是一个轻量级的库,用于处理状态。可以把它想象成一个状态机。

在本文中,我将通过构建一个工资处理引擎来深入探讨 Redux 的状态容器。该应用程序将存储工资单以及所有额外内容,例如奖金和股票期权。我将使用纯 JavaScript 和 TypeScript 进行类型检查来保持解决方案的简洁性。由于 Redux 非常易于测试,我还将使用 Jest 来验证应用程序。

在本教程中,我假设您对 JavaScript、Node 和 npm 有一定的了解。

首先,您可以使用 npm 初始化此应用程序:

npm init
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

当询问测试命令时,请继续使用 jest。这意味着 npm t 将启动 Jest 并运行所有单元测试。主文件将是 index.js,以保持其简洁性。您可以随意回答 npm init 的其余问题。

我将使用 TypeScript 进行类型检查并确定数据模型。这有助于概念化我们正在尝试构建的内容。

要开始使用 TypeScript:

npm i typescript --save-dev
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

我将把开发工作流程中的一部分依赖项放在 devDependencies 中。这清楚地表明哪些依赖项是为开发人员准备的,哪些依赖项将用于生产环境。准备好 TypeScript 后,在 package.json 中添加一个启动脚本:

"start": "tsc && node .bin/index.js"
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

在 src 文件夹下创建一个 index.ts 文件。这将源文件与项目的其余部分分开。如果您执行 npm start,则解决方案将无法执行。这是因为您需要配置 TypeScript。

创建一个包含以下配置的 tsconfig.json 文件:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

我本可以将此配置放在 tsc 命令行参数中。例如,tsc src/index.ts --strict .... 但是将所有这些放在单独的文件中要清晰得多。请注意,package.json 中的启动脚本只需要一个 tsc 命令。

以下是一些合理的编译器选项,它们将为我们提供一个良好的起点,以及每个选项的含义:

  • strict:启用所有严格类型检查选项,即 --noImplicitAny、--strictNullChecks 等。
  • lib:编译中包含的库文件列表。
  • outDir:将输出重定向到此目录。
  • sourceMap:生成用于调试的源映射文件。
  • files:提供给编译器的输入文件。

因为我将使用 Jest 进行单元测试,所以我将继续添加它:

npm init
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

ts-jest 依赖项为测试框架添加了类型检查。一个需要注意的地方是在 package.json 中添加一个 jest 配置:

npm i typescript --save-dev
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

这使得测试框架能够拾取 TypeScript 文件并知道如何对其进行转换。一个不错的功能是,您在运行单元测试时可以进行类型检查。为了确保此项目已准备好,请创建一个 __tests__ 文件夹,其中包含一个 index.test.ts 文件。然后,进行健全性检查。例如:

"start": "tsc && node .bin/index.js"
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

现在执行 npm start 和 npm t 将不会出现任何错误。这告诉我们我们现在可以开始构建解决方案了。但在我们这样做之前,让我们将 Redux 添加到项目中:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

此依赖项将用于生产环境。因此,无需使用 --save-dev 包含它。如果您检查您的 package.json,它将位于 dependencies 中。

实际操作中的工资单引擎

工资单引擎将包含以下内容:工资、报销、奖金和股票期权。在 Redux 中,您不能直接更新状态。相反,会调度操作来通知存储任何新的更改。

因此,这留下了以下操作类型:

npm i jest ts-jest @types/jest @types/node --save-dev
登录后复制
登录后复制
登录后复制
登录后复制

PAY_DAY 操作类型可用于在发薪日发放支票并跟踪工资历史记录。这些操作类型在我们完善工资单引擎时指导其余的设计。它们捕获状态生命周期中的事件,例如设置基本工资金额。这些操作事件可以附加到任何内容,无论是点击事件还是数据更新。Redux 操作类型对于调度来自何处是抽象的。状态容器可以在客户端和/或服务器上运行。

TypeScript

使用类型理论,我将根据状态数据确定数据模型。对于每个工资单操作,例如操作类型和可选金额。金额是可选的,因为 PAY_DAY 不需要资金来处理工资单。我的意思是,它可以向客户收费,但现在先忽略它(也许在第二版中引入)。

例如,将其放在 src/index.ts 中:

"jest": {
  "preset": "ts-jest"
}
登录后复制
登录后复制
登录后复制
登录后复制

对于工资单状态,我们需要一个用于基本工资、奖金等的属性。我们还将使用此状态来维护工资历史记录。

此 TypeScript 接口应该可以做到:

npm init
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

对于每个属性,请注意 TypeScript 使用冒号指定类型。例如,: number。这确定了类型契约,并为类型检查器增加了可预测性。使用具有显式类型声明的类型系统可以增强 Redux。这是因为 Redux 状态容器是为可预测的行为而构建的。

这个想法并不疯狂或激进。《学习 Redux》第 1 章(仅限 SitePoint Premium 会员)对此进行了很好的解释。

随着应用程序的改变,类型检查增加了额外的可预测性。随着应用程序的扩展,类型理论也有助于简化大型代码段的重构。

现在使用类型概念化引擎有助于创建以下操作函数:

npm i typescript --save-dev
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

好的一点是,如果您尝试执行 processBasePay('abc'),类型检查器会向您发出警告。破坏类型契约会降低状态容器的可预测性。我使用像 PayrollAction 这样的单个操作契约来使工资处理器更可预测。请注意,金额通过 ES6 属性简写在操作对象中设置。更传统的方法是 amount: amount,这比较冗长。箭头函数,例如 () => ({}),是编写返回对象文字的函数的一种简洁方法。

reducer 作为纯函数

reducer 函数需要一个状态和一个操作参数。状态应该具有具有默认值的初始状态。那么,你能想象我们的初始状态可能是什么样子吗?我认为它需要从零开始,并带有一个空的工资历史记录列表。

例如:

"start": "tsc && node .bin/index.js"
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

类型检查器确保这些是属于此对象的正确值。有了初始状态,就开始创建 reducer 函数:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

Redux reducer 具有一个模式,其中所有操作类型都由 switch 语句处理。但在遍历所有 switch case 之前,我将创建一个可重用的局部变量:

npm i jest ts-jest @types/jest @types/node --save-dev
登录后复制
登录后复制
登录后复制
登录后复制

请注意,如果您不改变全局状态,则可以改变局部变量。我使用 let 运算符来传达此变量将来会发生变化。改变全局状态(例如状态或操作参数)会导致 reducer 不纯。这种函数式范式至关重要,因为 reducer 函数必须保持纯净。《JavaScript 从新手到忍者》第 11 章(仅限 SitePoint Premium 会员)对此进行了解释。

开始 reducer 的 switch 语句以处理第一个用例:

"jest": {
  "preset": "ts-jest"
}
登录后复制
登录后复制
登录后复制
登录后复制

我使用 ES6 rest 运算符来保持状态属性不变。例如,...state。您可以在新对象中的 rest 运算符之后覆盖任何属性。basePay 来自解构,这很像其他语言中的模式匹配。computeTotalPay 函数设置如下:

it('is true', () => {
  expect(true).toBe(true);
});
登录后复制
登录后复制
登录后复制

请注意,您会扣除 stockOptions,因为这笔钱将用于购买公司股票。假设您想处理报销:

npm init
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

由于金额是可选的,请确保它具有默认值以减少故障。这就是 TypeScript 的优势所在,因为类型检查器会发现此陷阱并向您发出警告。类型系统知道某些事实,因此它可以做出合理的假设。假设您想处理奖金:

npm i typescript --save-dev
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

此模式使 reducer 可读,因为它只维护状态。您获取操作的金额,计算总工资,并创建一个新的对象文字。处理股票期权没有什么不同:

"start": "tsc && node .bin/index.js"
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

对于在发薪日处理工资单,它需要抹去奖金和报销。这两个属性不会在每个工资单中保留在状态中。并且,向工资历史记录中添加一个条目。基本工资和股票期权可以保留在状态中,因为它们不会经常更改。考虑到这一点,这就是 PAY_DAY 的处理方式:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

在一个像 newPayHistory 这样的数组中,使用扩展运算符,它是 rest 的反义词。与收集对象中属性的 rest 不同,它会将项目展开。例如,[...payHistory]。尽管这两个运算符看起来很相似,但它们并不相同。仔细观察,因为这可能会出现在面试问题中。

对 payHistory 使用 pop() 不会改变状态。为什么?因为 slice() 返回一个全新的数组。JavaScript 中的数组是通过引用复制的。将数组分配给新变量不会更改底层对象。因此,在处理这些类型的对象时必须小心。

因为 lastPayHistory 有可能未定义,所以我使用穷人的空值合并来将其初始化为零。请注意 (o && o.property) || 0 模式用于合并。JavaScript 或甚至 TypeScript 的未来版本可能会有一种更优雅的方法来做到这一点。

每个 Redux reducer 都必须定义一个默认分支。为了确保状态不会变得未定义:

npm i jest ts-jest @types/jest @types/node --save-dev
登录后复制
登录后复制
登录后复制
登录后复制

测试 reducer 函数

编写纯函数的众多好处之一是它们易于测试。单元测试是指您必须期望可预测的行为的测试,您可以将所有测试作为构建的一部分自动化。在 __tests__/index.test.ts 中,取消虚拟测试并导入所有感兴趣的函数:

"jest": {
  "preset": "ts-jest"
}
登录后复制
登录后复制
登录后复制
登录后复制

请注意,所有函数都设置为导出,因此您可以导入它们。对于基本工资,启动工资单引擎 reducer 并对其进行测试:

it('is true', () => {
  expect(true).toBe(true);
});
登录后复制
登录后复制
登录后复制

Redux 将初始状态设置为未定义。因此,在 reducer 函数中提供默认值始终是一个好主意。处理报销怎么样?

npm i redux --save
登录后复制
登录后复制

处理奖金的模式与此相同:

const BASE_PAY = 'BASE_PAY';
const REIMBURSEMENT = 'REIMBURSEMENT';
const BONUS = 'BONUS';
const STOCK_OPTIONS = 'STOCK_OPTIONS';
const PAY_DAY = 'PAY_DAY';
登录后复制
登录后复制

对于股票期权:

interface PayrollAction {
  type: string;
  amount?: number;
}
登录后复制
登录后复制

请注意,当 stockOptions 大于 totalPay 时,totalPay 必须保持不变。由于这家假设的公司是合乎道德的,它不想从员工那里拿钱。如果您运行此测试,请注意 totalPay 设置为 -10,因为 stockOptions 会被扣除。这就是我们测试代码的原因!让我们修复计算总工资的地方:

npm init
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

如果员工赚的钱不够买公司股票,请继续跳过扣除。另外,确保它将 stockOptions 重置为零:

npm i typescript --save-dev
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

该修复程序确定了 newStockOptions 中他们是否有足够的钱。有了这个,单元测试通过,代码健全且有意义。我们可以测试有足够的钱进行扣除的积极用例:

"start": "tsc && node .bin/index.js"
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

对于发薪日,请使用多个状态进行测试,并确保一次性交易不会持续存在:

{
  "compilerOptions": {
    "strict": true,
    "lib": ["esnext", "dom"],
    "outDir": ".bin",
    "sourceMap": true
  },
  "files": [
    "src/index"
  ]
}
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

请注意,我如何调整 oldState 以验证奖金并将报销重置为零。

reducer 中的默认分支怎么样?

npm i jest ts-jest @types/jest @types/node --save-dev
登录后复制
登录后复制
登录后复制
登录后复制

Redux 在开始时设置了一个像 INIT_ACTION 这样的操作类型。我们只关心我们的 reducer 是否设置了一些初始状态。

整合所有内容

此时,您可能会开始怀疑 Redux 是否更像是一种设计模式。如果您回答它既是模式又是轻量级库,那么您是对的。在 index.ts 中,导入 Redux:

"jest": {
  "preset": "ts-jest"
}
登录后复制
登录后复制
登录后复制
登录后复制

下一个代码示例可以围绕此 if 语句包装。这是一个权宜之计,因此单元测试不会泄漏到集成测试中:

it('is true', () => {
  expect(true).toBe(true);
});
登录后复制
登录后复制
登录后复制

我不建议在实际项目中这样做。模块可以放在单独的文件中以隔离组件。这使其更易于阅读,并且不会泄漏问题。单元测试也受益于模块独立运行的事实。

使用 payrollEngineReducer 启动 Redux 存储:

npm i redux --save
登录后复制
登录后复制

每个 store.subscribe() 都返回一个后续的 unsubscribe() 函数,该函数可用于清理。它会在通过存储调度操作时取消订阅回调。在这里,我使用 store.getState() 将当前状态输出到控制台。

假设这位员工赚了 300,有 50 的报销,100 的奖金,以及 15 用于公司股票:

const BASE_PAY = 'BASE_PAY';
const REIMBURSEMENT = 'REIMBURSEMENT';
const BONUS = 'BONUS';
const STOCK_OPTIONS = 'STOCK_OPTIONS';
const PAY_DAY = 'PAY_DAY';
登录后复制
登录后复制

为了使其更有趣,再进行 50 的报销并处理另一张工资单:

interface PayrollAction {
  type: string;
  amount?: number;
}
登录后复制
登录后复制

最后,运行另一张工资单并取消订阅 Redux 存储:

interface PayStubState {
  basePay: number;
  reimbursement: number;
  bonus: number;
  stockOptions: number;
  totalPay: number;
  payHistory: Array<PayHistoryState>;
}

interface PayHistoryState {
  totalPay: number;
  totalCompensation: number;
}
登录后复制

最终结果如下所示:

export const processBasePay = (amount: number): PayrollAction =>
  ({type: BASE_PAY, amount});
export const processReimbursement = (amount: number): PayrollAction =>
  ({type: REIMBURSEMENT, amount});
export const processBonus = (amount: number): PayrollAction =>
  ({type: BONUS, amount});
export const processStockOptions = (amount: number): PayrollAction =>
  ({type: STOCK_OPTIONS, amount});
export const processPayDay = (): PayrollAction =>
  ({type: PAY_DAY});
登录后复制

如所示,Redux 维护状态、改变状态并在一个简洁的小包中通知订阅者。可以将 Redux 想象成一个状态机,它是状态数据的真实来源。所有这些都采用了编码的最佳实践,例如健全的函数式范式。

结论

Redux 为复杂的状态管理问题提供了一个简单的解决方案。它依赖于函数式范式来减少不可预测性。因为 reducer 是纯函数,所以单元测试非常容易。我决定使用 Jest,但是任何支持基本断言的测试框架都可以工作。

TypeScript 使用类型理论增加了额外的保护层。将类型检查与函数式编程结合起来,您将获得几乎不会中断的健全代码。最重要的是,TypeScript 在增加价值的同时不会妨碍工作。如果您注意到,一旦类型契约到位,几乎没有额外的编码。类型检查器会完成其余的工作。像任何好工具一样,TypeScript 在保持不可见的同时自动化编码纪律。TypeScript 吠叫声很大,但咬起来很轻。

如果您想试用此项目(我希望您这样做),您可以在 GitHub 上找到本文的源代码。

以上是深入研究Redux的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

前端热敏纸小票打印遇到乱码问题怎么办? 前端热敏纸小票打印遇到乱码问题怎么办? Apr 04, 2025 pm 02:42 PM

前端热敏纸小票打印的常见问题与解决方案在前端开发中,小票打印是一个常见的需求。然而,很多开发者在实...

神秘的JavaScript:它的作用以及为什么重要 神秘的JavaScript:它的作用以及为什么重要 Apr 09, 2025 am 12:07 AM

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

谁得到更多的Python或JavaScript? 谁得到更多的Python或JavaScript? Apr 04, 2025 am 12:09 AM

Python和JavaScript开发者的薪资没有绝对的高低,具体取决于技能和行业需求。1.Python在数据科学和机器学习领域可能薪资更高。2.JavaScript在前端和全栈开发中需求大,薪资也可观。3.影响因素包括经验、地理位置、公司规模和特定技能。

JavaScript难以学习吗? JavaScript难以学习吗? Apr 03, 2025 am 12:20 AM

学习JavaScript不难,但有挑战。1)理解基础概念如变量、数据类型、函数等。2)掌握异步编程,通过事件循环实现。3)使用DOM操作和Promise处理异步请求。4)避免常见错误,使用调试技巧。5)优化性能,遵循最佳实践。

如何实现视差滚动和元素动画效果,像资生堂官网那样?
或者:
怎样才能像资生堂官网一样,实现页面滚动伴随的动画效果? 如何实现视差滚动和元素动画效果,像资生堂官网那样? 或者: 怎样才能像资生堂官网一样,实现页面滚动伴随的动画效果? Apr 04, 2025 pm 05:36 PM

实现视差滚动和元素动画效果的探讨本文将探讨如何实现类似资生堂官网(https://www.shiseido.co.jp/sb/wonderland/)中�...

JavaScript的演变:当前的趋势和未来前景 JavaScript的演变:当前的趋势和未来前景 Apr 10, 2025 am 09:33 AM

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

如何使用JavaScript将具有相同ID的数组元素合并到一个对象中? 如何使用JavaScript将具有相同ID的数组元素合并到一个对象中? Apr 04, 2025 pm 05:09 PM

如何在JavaScript中将具有相同ID的数组元素合并到一个对象中?在处理数据时,我们常常会遇到需要将具有相同ID�...

console.log输出结果差异:两次调用为何不同? console.log输出结果差异:两次调用为何不同? Apr 04, 2025 pm 05:12 PM

深入探讨console.log输出差异的根源本文将分析一段代码中console.log函数输出结果的差异,并解释其背后的原因。�...

See all articles