典型的Web应用程序通常由几个共享数据的UI组件组成。 通常,多个组件的任务是显示同一对象的不同属性。 该对象表示可以随时更改的状态。 在多个组件之间保持状态一致可能是一场噩梦,尤其是当有多个通道用于更新同一对象时。
以带有购物车的网站为例。 在顶部,我们有一个UI组件,显示购物车中的物品数量。 我们还可以使用另一个UI组件来显示购物车中商品的总费用。 如果用户单击“ 添加到购物车”按钮,则这两个组件均应立即以正确的数字进行更新。 如果用户决定从购物车中删除物品,更改数量,添加保护计划,使用优惠券或更改运输位置,则相关的UI组件应更新以显示正确的信息。 如您所见,一个简单的购物车会随着其功能范围的扩大而迅速变得难以保持同步 。
在本指南中,我将向您介绍一个称为Redux的框架,该框架可以帮助您以易于扩展和维护的方式构建复杂的项目。 为了简化学习,我们将使用简化的购物车项目来了解Redux的工作方式。 您至少需要熟悉React库,因为稍后需要将其与Redux集成。
先决条件
在开始之前,请确保您熟悉以下主题:
另外,请确保在计算机上进行以下设置:
您可以在GitHub上访问本教程中使用的全部代码。
什么是Redux
Redux是一种流行的JavaScript框架,为应用程序提供了可预测的状态容器。 Redux基于Flux(Facebook开发的框架)的简化版本。 与标准MVC框架不同,在标准MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格允许数据仅在一个方向流动。 请参见下图:
图1:Redux流程图
在Redux中,所有数据(即状态 )都保存在称为store的容器中。 应用程序中只能有其中之一。 该存储实质上是一棵状态树,其中保留了所有对象的状态。 任何UI组件都可以直接从商店访问特定对象的状态。 要从本地或远程组件更改状态,需要调度一个动作 。 在这种情况下调度意味着向商店发送可操作的信息。 商店收到action
,会将其委托给相关的reducer 。 reducer
只是一个纯函数,它查看先前的状态,执行一个操作并返回一个新的状态。 为了看到所有这些,我们需要开始编码。
首先了解不变性
在开始之前,我需要您首先了解不变性在JavaScript中的含义。 根据牛津英语词典,不变性意味着不可改变 。 在编程中,我们编写的代码始终会更改变量的值。 这称为可变性 。 我们这样做的方式通常会在我们的项目中导致意外的错误。 如果您的代码仅处理原始数据类型(数字,字符串,布尔值),则无需担心。 但是,如果您使用数组和对象,则对它们执行可变操作可能会导致意外错误。 为了演示这一点,请打开您的终端并启动Node交互式shell:
node
接下来,让我们创建一个数组,然后将其分配给另一个变量:
> let a = [1,2,3]
> let b = a
> b.push(9)
> console.log(b)
[ 1, 2, 3, 9 ] // b output
> console.log(a)
[ 1, 2, 3, 9 ] // a output
如您所见,更新array b
也会导致array a
也发生变化。 发生这种情况是因为对象和数组是已知的引用数据类型 ,这意味着此类数据类型本身实际上并不保存值,而是指向存储值的内存位置的指针。 通过将a
分配给b
,我们仅创建了引用相同位置的第二个指针。 要解决此问题,我们需要将引用的值复制到新位置。 在JavaScript中,可以通过三种不同的方法来实现此目的:
- 使用Immutable.js创建的不可变数据结构
- 使用JavaScript库,如下划线和Lodash执行一成不变的操作
- 使用本机ES6函数执行不变的操作。
在本文中,我们将使用ES6方式,因为它已经在NodeJS环境中可用。 在您的NodeJS
终端中,执行以下操作:
> a = [1,2,3] // reset a
[ 1, 2, 3 ]
> b = Object.assign([],a) // copy array a to b
[ 1, 2, 3 ]
> b.push(8)
> console.log(b)
[ 1, 2, 3, 8 ] // b output
> console.log(a)
[ 1, 2, 3 ] // a output
在上面的代码示例中,现在可以修改数组b
而不影响数组a
。 我们使用Object.assign()创建了变量b
现在指向的值的新副本。 我们还可以使用rest operator(...)
来执行不变的操作,如下所示:
> a = [1,2,3]
[ 1, 2, 3 ]
> b = [...a, 4, 5, 6]
[ 1, 2, 3, 4, 5, 6 ]
> a
[ 1, 2, 3 ]
其余运算符也可以处理对象文字! 我不会深入探讨这个主题,但是这里有一些我们将用来执行不可变操作的ES6其他功能:
如果我链接的文档没有用,请不要担心,因为您将看到它们在实际中的用法。 让我们开始编码!
设置Redux
设置Redux开发环境的最快方法是使用create-react-app
工具。 在开始之前,请确保您已经安装并更新了nodejs
, npm
和yarn
。 让我们通过生成redux-shopping-cart
项目并安装Redux软件包来设置Redux项目:
create-react-app redux-shopping-cart
cd redux-shopping-cart
yarn add redux # or npm install redux
删除src
文件夹中除index.js
之外的所有文件。 打开文件并清除所有现有代码。 输入以下内容:
import { createStore } from "redux";
const reducer = function(state, action) {
return state;
}
const store = createStore(reducer);
让我解释一下以上代码的作用:
- 第一个陈述 。 我们从Redux包中导入了
createStore()
函数。 - 第二条陈述 。 我们创建一个称为reducer的空函数。 第一个参数
state
是存储在存储中的当前数据。 第二个参数action
是一个容器,用于:- 类型 —一个简单的字符串常量,例如
ADD
,UPDATE
,DELETE
等。 - 有效负载 —用于更新状态的数据
- 类型 —一个简单的字符串常量,例如
- 第三条陈述 。 我们创建一个Redux存储,只能使用reducer作为参数来构造它。 可以直接访问Redux存储中保存的数据,但是只能通过提供的reducer进行更新。
您可能已经注意到我提到了当前数据,就好像它已经存在一样。 当前,我们的state
是undefined或null。 为了解决这个问题,只需为默认状态分配一个默认值,使其成为一个空数组即可:
const reducer = function(state=[], action) {
return state;
}
现在,让我们开始实践。 我们创建的减速器是通用的。 它的名字没有描述它的作用。 然后是我们如何使用多个异径管的问题。 答案是使用Redux软件包提供的combineReducers
函数。 如下更新代码:
// src/index.js
…
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重命名为cartReducer
。 我还创建了一个名为productsReducer
的新空减速器,以向您展示如何使用combineReducers
函数在单个商店中组合多个减速器。
接下来,我们将研究如何为减速器定义一些测试数据。 如下更新代码:
// 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
来运行开发服务器。 然后按Ctrl+Shift+I
在Chrome中打开检查器标签,以便查看控制台标签。
图2:Redux初始状态
目前,我们的cartReducer
不执行任何操作,但是应该管理Redux商店中我们购物车项目的状态。 我们需要定义用于添加,更新和删除购物车项目的操作。 让我们从定义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;
}
}
…
花时间分析和理解代码。 减速器应该处理不同的操作类型,因此需要SWITCH
语句。 当将类型ADD_TO_CART
操作分派到应用程序中的任何位置时,此处定义的代码将对其进行处理。 如您所见,我们正在使用action.payload
提供的信息来合并到现有状态,以创建新状态。
接下来,我们将定义一个action
,它是store.dispatch()
的参数所需要的。 动作只是必须具有type
和可选有效负载的JavaScript对象。 让我们继续在cartReducer
函数之后定义一个:
…
function addToCart(product, quantity, unitCost) {
return {
type: ADD_TO_CART,
payload: { product, quantity, unitCost }
}
}
…
在这里,我们定义了一个返回纯JavaScript对象的函数。 没有什么花哨。 在分派之前,让我们添加一些代码,使我们能够侦听存储事件的更改。 将此代码放在console.log()
语句之后:
…
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
unsubscribe();
接下来,让我们通过向商店分派动作来将几个商品添加到购物车中。 将此代码unsubscribe()
之前:
…
store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));
为了澄清起见,我将在下面说明进行上述所有更改后如何显示整个代码:
// src/index.js
import { 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应该会自动刷新。 检查控制台选项卡,以确认已添加新项目:
图3:分派的Redux操作
组织Redux代码
index.js
文件迅速变大了。 这不是Redux代码的编写方式。 我只是为了向您展示Redux是多么简单。 让我们看看应该如何组织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.js
export 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.js
export default function(state=[], action) {
return state;
}
// src/reducers/cart-reducer.js
import { 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.js
import { 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.js
import { createStore } from "redux";
import rootReducer from './reducers';
let store = createStore(rootReducer);
export default store;
// src/index.js
import 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-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
}
}
}
接下来,如下更新cart-reducer.js
:
// 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
调度UPDATE_CART
和DELETE_FROM_CART
操作:
// src/index.js
…
// Update Cart
store.dispatch(updateCart('Flour 1kg', 5, 110));
// Delete from Cart
store.dispatch(deleteFromCart('Coffee 500gm'));
…
保存所有更改后,浏览器应自动刷新。 检查控制台选项卡以确认结果:
图4:Redux更新和删除操作
如已确认的那样,将1公斤面粉的数量从2更新为5,同时将500克咖啡从购物车中删除。
使用Redux工具进行调试
现在,如果我们在代码中犯了一个错误,我们如何调试Redux项目?
Redux附带了许多第三方调试工具,我们可以使用它们来分析代码行为并修复错误。 可能最受欢迎的工具是时间旅行工具 ,也称为redux-devtools-extension 。 设置过程分为三个步骤。 首先,转到您的Chrome浏览器并安装Redux Devtools扩展程序 。
图5:Redux DevTools Chrome扩展
接下来,转到运行Redux应用程序的终端,然后按Ctrl+C
停止开发服务器。 接下来,使用npm或yarn安装redux-devtools-extension软件包。 就个人而言,我更喜欢Yarn,因为有一个yarn.lock
文件,我想保持更新。
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面板:
图6:Redux DevTools菜单
就我而言,我选择了“到底部”选项。 随意尝试其他选择。
图7:Redux DevTools面板
如您所见,Redux Devtool非常出色。 您可以在action,state和diff方法之间切换。 在左侧面板上选择操作,然后观察状态树的变化。 您也可以使用滑块播放操作序列。 您甚至可以直接从该工具调度! 请查阅文档以了解更多有关如何进一步根据需要自定义该工具的信息。
与React集成
在本教程的开始,我提到Redux与React确实搭配得很好。 好了,您只需要执行几个步骤即可设置集成。 首先,停止开发服务器,因为我们需要安装react-redux软件包,这是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存储和动作链接。 另外,您需要使用Express或Feathers之类的框架来设置API。 该API将使我们的应用程序可以访问数据库服务。
在Redux中,我们还需要安装其他软件包(例如axios
以通过Redux操作执行API请求。 然后,我们的React组件状态将由Redux处理,确保所有组件都与数据库API同步。 要了解有关如何完成所有这些操作的更多信息,请查看我的其他教程“ 使用React,Redux和FeathersJS构建CRUD应用程序 ”。
摘要
希望本指南对Redux有所帮助。 但是,还有很多东西需要您学习。 例如,您需要学习如何处理异步操作,身份验证,日志记录,处理表单等。 既然您了解了Redux的全部含义,那么您将发现尝试其他类似的框架(例如Flux ,Alt.js或Mobx)更加容易 。 如果您觉得Redux适合您,我强烈建议您使用以下教程,以帮助您获得有关Redux的更多经验: