典型的Web应用程序通常由共享数据的多个UI组件组成。通常,多个组件的任务是负责展示同一对象的不同属性。这个对象表示可随时更改的状态。在多个组件之间保持状态的一致性会是一场噩梦,特别是如果有多个通道用于更新同一个对象。

举个?,一个带有购物车的网站。在顶部,我们用一个UI组件显示购物车中的商品数量。我们还可以用另一个UI组件,显示购物车中商
品的总价。如果用户点击添加到购物车按钮,则这两个组件应立即更新当前的数据。如果用户从购物车中删除商品、更改数目、使用优惠券或者更改送货地点,则相关的UI组件都应该更新出正确的信息。
可以看到,随着功能范围的扩大,一个简单的购物车将会很难保持数据同步。

在这篇文章中,我将介绍Redux框架,它可以帮助你以简单易用的方式构建复杂项目并进行维护。为了使学习更容易,我们将使用一个简化的购物车项目来学习Redux的工作远离。你需要至少熟悉React库,因为你以后需要将其与Redux集成。

学习前提

在我们开始以前,确保你熟悉以下知识:

  • 功能性JavaScript
  • 面向对象JavaScript
  • JavaScript ES6 语法

同时,确保你的设备已经安装:

  • NodeJS
  • Yarn(或者npm)

什么是Redux

Redux是一个流行的JavaScript框架,为应用程序提供一个可预测的状态容器。Redux基于简化版本的Flux框架,Flux是Facebook开发的一个框架。在标准的MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格限制了数据只能在一个方向上流动。 见下图:

在Redux中,所有的数据(比如state)被保存在一个被称为store的容器中 → 在一个应用程序中只能有一个。store本质上是一个状态树,保存了所有对象的状态。任何UI组件都可以直接从store访问特定对象的状态。要通过本地或远程组件更改状态,需要分发一个action分发在这里意味着将可执行信息发送到store。当一个store接收到一个action,它将把这个action代理给相关的reducerreducer是一个纯函数,它可以查看之前的状态,执行一个action并且返回一个新的状态。

理解不变性(Immutability)

在我们开始实践之前,需要先了解JavaScript中的不变性意味着什么。在编码中,我们编写的代码一直在改变变量的值。这是可变性。但是可变性常常会导致意外的错误。如果代码只处理原始数据类型(numbers, strings, booleans),那么你不用担心。但是,如果在处理Arrays和Objects时,则需要小心执行可变操作。
接下来演示不变性

  • 打开终端并启动node(输入node)。
  • 创建一个数组,并将其赋值给另一个变量。
> let a = [1, 2, 3]
> let b = a
> b.push(8)
> b
[1, 2, 3, 8]
> a
[1, 2, 3, 8]

可以看到,更新数组b也会同时改变数组a。这是因为对象和数组是引用数据类型 → 这意味着这样的数据类型实际上并不保存值,而是存储指向存储单元的指针。
将a赋值给b,其实我们只是创建了第二个指向同一存储单元的指针。要解决这个问题,我们需要将引用的值复制到一个新的存储单元。在Javascript中,有三种不同的实现方式:

  1. 使用Immutable.js创建不可变的数据结构。
  2. 使用JavaScript库(如Underscore和Lodash)来执行不可变的操作。
  3. 使用ES6方法执行不可变操作。

本文将使用ES6方法,因为它已经在NodeJS环境中可用了,在终端中,执行以下操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = Object.assign([],a)
[ 1, 2, 3 ]
> b.push(8)
> b
[ 1, 2, 3, 8 ] // b output
> a
[ 1, 2, 3 ] // a output

在上面的代码中,修改数组b将不会影响数组a。我们使用Object.assign()创建了一个新的副本,由数组b指向。我们也可以使用操作符(...)执行不可变操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = [...a, 4, 5, 6]
[ 1, 2, 3, 4, 5, 6 ]
> a
[ 1, 2, 3 ]

我不会深入这个主题,但是这里还有一些额外的ES6功能,我们可以用它们执行不可变操作:

  • spread syntax - 用于追加操作
  • map function - 用于更新操作
  • filter function - 用于删除操作

配置Redux

配置Redux开发环境的最快方法是使用create-react-app工具。在开始之前,确保已经安装并更新了nodejsnpmyarn。我们生成一个redux-shopping-cart项目并安装Redux

create-react-app redux-shopping-cartcd redux-shopping-cart
yarn add redux # 或者npm install redux

首先,删除src文件夹中除index.js以外的所有文件。打开index.js,删除所有代码,键入以下内容:

import { createStore } from "redux";const reducer = function(state, action) {return state;
}const store = createStore(reducer);

让我解释一下上面的代码:

  1. 首先,我们从redux包中引入createStore()方法。
  2. 我们创建了一个名为reducer的方法。第一个参数state是当前保存在store中的数据,第二个参数action是一个容器,用于:

    • type - 一个简单的字符串常量,例如ADD, UPDATE, DELETE等。
    • payload - 用于更新状态的数据。
  3. 我们创建一个Redux存储区,它只能使用reducer作为参数来构造。存储在Redux存储区中的数据可以被直接访问,但只能通过提供的reducer进行更新。

注意到,我在第二点中所提到state。目前,state为undefined或null。要解决这个问题,需要分配一个默认的值给state,使其成为一个空数组:

const reducer = function(state=[], action) {return state;
}

让我们更进一步。目前我们创建的reducer是通用的。它的名字没有描述它的用途。那么我们如何使用多个reducer呢?我们将用到Redux包中提供的combineReducers函数。修改代码如下:

// src/index.jsimport { createStore } from "redux";
import { combineReducers } from 'redux';const productsReducer = function(state=[], action) {return state;
}const cartReducer = function(state=[], action) {return state;
}const allReducers = {products: productsReducer,shoppingCart: cartReducer
}const rootReducer = combineReducers(allReducers);let store = createStore(rootReducer);

在上面的代码中,我们将通用的reducer修改为productReducercartReducer。创建这两个空的reducer是为了展示如何在一个store中使用combineReducers函数组合多个reducer。

接下来,我们将为reducer定义一些测试数据。修改代码如下:

// src/index.js…const initialState = {cart: [{product: 'bread 700g',quantity: 2,unitCost: 90},{product: 'milk 500ml',quantity: 1,unitCost: 47}]
}const cartReducer = function(state=initialState, action) {return state;
}…let store = createStore(rootReducer);console.log("initial state: ", store.getState());

我们使用store.getState()在控制台中打印出当前的状态。你可以在终端中执行npm start或者yarn start来运行dev服务器。并在控制台中查看state

现在,我们的cartReducer什么也没做,但它应该在Redux的存储区中管理购物车商品的状态。我们需要定义添加、更新和删除商品的操作(action)。我们首先定义ADD_TO_CART的逻辑:

// src/index.js…const ADD_TO_CART = 'ADD_TO_CART';const cartReducer = function(state=initialState, action) {switch (action.type) {case ADD_TO_CART: {return {...state,cart: [...state.cart, action.payload]}}default:return state;}
}…

我们继续来分析一下代码。一个reducer需要处理不同的action类型,因此我们需要一个SWITCH语句。当一个ADD_TO_CART类型的action在应用程序中分发时,switch中的代码将处理它。
正如你所看到的,我们将action.payload中的数据与现有的state合并以创建一个新的state。

接下来,我们将定义一个action,作为store.dispatch()的一个参数。action是一个Javascript对象,有一个必须的type和可选的payload。我们在cartReducer函数后定义一个:

…
function addToCart(product, quantity, unitCost) {return {type: ADD_TO_CART,payload: { product, quantity, unitCost }}
}
…

在这里,我们定义了一个函数,返回一个JavaScript对象。在我们分发消息之前,我们添加一些代码,让我们能够监听store事件的更改。

…
let unsubscribe = store.subscribe(() =>console.log(store.getState())
);unsubscribe();

接下来,我们通过分发消息到store来向购物车中添加商品。将下面的代码添加在unsubscribe()之前:

…
store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

下面是整个index.js文件:

// src/index.jsimport { createStore } from "redux";
import { combineReducers } from 'redux';const productsReducer = function(state=[], action) {return state;
}const initialState = {cart: [{product: 'bread 700g',quantity: 2,unitCost: 90},{product: 'milk 500ml',quantity: 1,unitCost: 47}]
}const ADD_TO_CART = 'ADD_TO_CART';const cartReducer = function(state=initialState, action) {switch (action.type) {case ADD_TO_CART: {return {...state,cart: [...state.cart, action.payload]}}default:return state;}
}function addToCart(product, quantity, unitCost) {return {type: ADD_TO_CART,payload: {product,quantity,unitCost}}
}const allReducers = {products: productsReducer,shoppingCart: cartReducer
}const rootReducer = combineReducers(allReducers);let store = createStore(rootReducer);console.log("initial state: ", store.getState());let unsubscribe = store.subscribe(() =>console.log(store.getState())
);store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));unsubscribe();

保存代码后,Chrome会自动刷新。可以在控制台中确认新的商品已经添加了。

组织Redux代码

index.js中的代码逐渐变得冗杂。我把所有的代码都写在index.js中是为了起步时的简单易懂。接下来,我们来看一下如何组织Redux项目。首先,在src文件夹中创建一下文件和文件夹:

src/
├── actions
│ └── cart-actions.js
├── index.js
├── reducers
│ ├── cart-reducer.js
│ ├── index.js
│ └── products-reducer.js
└── store.js

然后,我们把index.js中的代码进行整理:

// src/actions/cart-actions.jsexport const ADD_TO_CART = 'ADD_TO_CART';export function addToCart(product, quantity, unitCost) {return {type: ADD_TO_CART,payload: { product, quantity, unitCost }}
}
// src/reducers/products-reducer.jsexport default function(state=[], action) {return state;
}
// src/reducers/cart-reducer.jsimport  { ADD_TO_CART }  from '../actions/cart-actions';const initialState = {cart: [{product: 'bread 700g',quantity: 2,unitCost: 90},{product: 'milk 500ml',quantity: 1,unitCost: 47}]
}export default function(state=initialState, action) {switch (action.type) {case ADD_TO_CART: {return {...state,cart: [...state.cart, action.payload]}}default:return state;}
}
// src/reducers/index.jsimport { combineReducers } from 'redux';
import productsReducer from './products-reducer';
import cartReducer from './cart-reducer';const allReducers = {products: productsReducer,shoppingCart: cartReducer
}const rootReducer = combineReducers(allReducers);export default rootReducer;
// src/store.jsimport { createStore } from "redux";
import rootReducer from './reducers';let store = createStore(rootReducer);export default store;
// src/index.jsimport store from './store.js';
import { addToCart }  from './actions/cart-actions';console.log("initial state: ", store.getState());let unsubscribe = store.subscribe(() =>console.log(store.getState())
);store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));unsubscribe();

整理完代码之后,程序依然会正常运行。现在我们来添加修改和删除购物车中商品的逻辑。修改cart-actions.jscart-reducer.js文件:

// src/reducers/cart-actions.js
…
export const UPDATE_CART = 'UPDATE_CART';
export const DELETE_FROM_CART = 'DELETE_FROM_CART';
…
export function updateCart(product, quantity, unitCost) {return {type: UPDATE_CART,payload: {product,quantity,unitCost}}
}export function deleteFromCart(product) {return {type: DELETE_FROM_CART,payload: {product}}
}
// src/reducers/cart-reducer.js
…
export default function(state=initialState, action) {switch (action.type) {case ADD_TO_CART: {return {...state,cart: [...state.cart, action.payload]}}case UPDATE_CART: {return {...state,cart: state.cart.map(item => item.product === action.payload.product ? action.payload : item)}}case DELETE_FROM_CART: {return {...state,cart: state.cart.filter(item => item.product !== action.payload.product)}}default:return state;}
}

最后,我们在index.js中分发这两个action

// src/index.js
…
// Update Cart
store.dispatch(updateCart('Flour 1kg', 5, 110));// Delete from Cart
store.dispatch(deleteFromCart('Coffee 500gm'));
…

保存完代码之后,可以在浏览器的控制台中检查修改和删除的结果。

使用Redux工具调试

如果我们的代码出错了,应该如何调试呢?

Redux拥有很多第三方的调试工具,可用于分析代码和修复bug。最受欢迎的是time-travelling tool,即redux-devtools-extension。设置它只需要三个步骤。

  • 首先,在Chrome中安装Redux Devtools扩展。
  • 然后,在运行Redux应用程序的终端里使用Ctrl+C停止服务器。并用npm或yarn安装redux-devtools-extension包。
yarn add redux-devtools-extension
  • 一旦安装完成,我们对store.js稍作修改:
// src/store.js
import { createStore } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers';const store = createStore(rootReducer, composeWithDevTools());export default store;

我们还可以把src/index.js中日志相关的代码删除掉。返回Chrome,右键单击该工具的图标,打开Redux DevTools面板:

可以看到,Redux Devtools很强大。你可以在actionstatediff(方法差异)之间切换。选择左侧面板上的不同action,观察状态树的变化。你还可以通过进度条来播放actions序列。甚至可以通过工具直接分发操作信息。具体的请查看文档。

集成React

在本文开头,我提到Redux可以很方便的与React集成。只需要简单的几步。

  • 首先,停止服务器,并安装react-redux包:
yarn add react-redux
  • 接下来,在index.js中加入React代码。我们还将使用Provider类将React应用程序包装在Redux容器中:
// src/index.js
…
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';const App = <h1>Redux Shopping Cart</h1>;ReactDOM.render(<Provider store={store}>{ App }</Provider> ,document.getElementById('root')
);
…

目前,已经完成了集成的第一部分。可以启动服务器以查看效果。第二部分涉及到使用刚刚安装的react-redux包中的几个方法。通过这些方法将React组件与Redux的storeaction相关联。此外,还可以使用Express和Feathers这样的框架来设置API。API将为我们的应用程序提供对数据库服务的访问。

在Redux中,我们还可以安装其他一些包,比如axios等。我们React组件的state将由Redux处理,确保所有组件与数据库API的同步。想要更进一步的学习,请看Build a CRUD App Using React, Redux and FeathersJS。

总结

我希望本文能对你有所帮助。当然,还有很多相关的内容需要学习。例如,处理异步操作、身份验证、日志记录等。如果觉得Redux适合你,可以看看以下几篇文章:

  • Redux State Management in Vanilla JavaScript
  • Redux Logging in Production with LogRocket
  • Build a CRUD App Using React, Redux and FeathersJS
  • Dealing with Asynchronous APIs in Server-rendered React

Redux入门教程详解(快速上手)相关推荐

  1. python入门教程pdf-Python入门教程详解.pdf

    您所在位置:网站首页 > 海量文档 &nbsp>&nbsp计算机&nbsp>&nbspPython Python入门教程详解.pdf132页 本文档一 ...

  2. vue 单相绑定_Vuejs第一篇之入门教程详解(单向绑定、双向绑定、列表渲染、响应函数)...

    Vuejs第一篇之入门教程详解(单向绑定.双向绑定.列表渲染.响应函数) 2018-12-30 什么是组件? 组件(Component)是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素 ...

  3. k8s入门教程详解(一)

    文章目录 Kubernetes 入门教程详解(一) 一. Kubernetes 概述 1. K8S 发展历史由来 2.K8S官网 2.K8S 是什么 3. K8s 优势及特点 3.1 K8S优势 3. ...

  4. 计算机入门新人必学,电脑新手入门教程 让你快速上手

    随着互联网时代的快速发展,电脑发展也相当的成熟,新手学电脑可以快速入门,是每个新手梦寐以求的事情,但是不会所有人都可以快速上手,最近很多用户咨询我关于零基础学电脑先学什么,或者是电脑新手入门必学什么东 ...

  5. MindManager2020版本入门教程详解

    MindManager是一款创造.管理和交流思想的思维导图软件,其直观清晰的可视化界面和强大的功能可以快速捕捉.组织和共享思维.想法.资源和项目进程等等.MindManager新手入门教程专为新手用户 ...

  6. Python入门教程详解

    Python入门教程 目录 1. 简介 2. 安装 3. 基本语法 4. 数据类型 5. 条件语句 6. 循环 7. 函数 8. 模块 9. 异常处理 10. 文件输入输出 1. 简介 Python是 ...

  7. keyshot9怎么导入材质_keyShot 9如何使用?keyshot 入门教程详解

    KeyShot 9 Pro for Mac是应用在Mac上的一款3D渲染和动画制作软件,从科学上准确的材料和环境预设到高级材料编辑和动画,创建交互式产品视觉效果或销售和营销图像从未如此简单. macw ...

  8. 2022年最新C#入门教程详解

    C# 简介 C# 是一个面向对象的编程语言,它是由微软(Microsoft)开发的,由 Ecma 和 ISO 核准认可的.C# 是专为公共语言基础结构(CLI)设计的.CLI 由可执行代码和运行时环境 ...

  9. KeyShot 9如何使用?keyshot 入门教程详解

    KeyShot 9 Pro for Mac是应用在Mac上的一款3D渲染和动画制作软件,从科学上准确的材料和环境预设到高级材料编辑和动画,创建交互式产品视觉效果或销售和营销图像从未如此简单. macw ...

最新文章

  1. vue在created调用点击方法_vue中created、mounted等方法整理
  2. .NET 4.0 任务(Task)
  3. 绕过mysql的id:32933 BUG 实现order by limit 正常取数据.
  4. java基础day11---空指针异常----引用类型--自定义类型赋值--封装
  5. django-元选项
  6. php获取cpu编码,PHP下通过exec获得计算机的唯一标识[CPU,网卡 MAC地址]
  7. mysql not in 性能_SQL中Execpt和not in 性能区别
  8. 敏捷开发团队管理系列之一:序言与出发点
  9. 深入理解jsonp跨域请求原理
  10. LuaForUnity2:Lua基本数据类型与符号
  11. Topcoder SRM 601 div1题解
  12. Linux与Xshell:登陆服务器与后台执行程序
  13. 电脑网络安全_网络安全月 | 戳视频!帮你的电脑远离“黑客”
  14. laravel-admin模型表格和详情展示关联数据
  15. STM32串口DMA方式接收数据。类似环形FIFO。超省CPU资源!
  16. js alert弹窗函数
  17. 现代x64的APIC中断控制器架构
  18. 在Windows下编写的代码,实时在Linux下编译
  19. 数字证书的创建与使用(采用java)
  20. 简单聊聊 Ironic 是如何管理裸金属的

热门文章

  1. 除了不雅察旁边车道的车辆行驶状况
  2. 即时函数(Immediate Functions)
  3. 解决谷歌Chorm浏览器上面的地址栏搜索内容直接跳转百度页面的问题
  4. mingw手工编译wxWidgets遇到的奇怪问题,及解决方案
  5. 思科2960导出系统
  6. 面向开发者的 Android 10 —— Android 10 亮点
  7. 2017工作总结及2018工作计划
  8. 计算机网络——(9)网络管理与网络安全
  9. 如何使用ChatGPT写论文?
  10. 卷积神经网络CNN(7)—— 限速交通标志分类