学习前提

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

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

什么是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)。
  • 创建一个数组,并将其赋值给另一个变量。
  1. > let a = [1, 2, 3]
  2. > let b = a
  3. > b.push(8)
  4. > b
  5. [1, 2, 3, 8]
  6. > a
  7. [1, 2, 3, 8]

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

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

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

  1. > a = [1,2,3]
  2. [ 1, 2, 3 ]
  3. > b = Object.assign([],a)
  4. [ 1, 2, 3 ]
  5. > b.push(8)
  6. > b
  7. [ 1, 2, 3, 8 ] // b output
  8. > a
  9. [ 1, 2, 3 ] // a output

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

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

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

配置Redux

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

  1. create-react-app redux-shopping-cart
  2. cd redux-shopping-cart
  3. yarn add redux # 或者npm install redux

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

  1. import { createStore } from "redux";
  2. const reducer = function(state, action) {
  3. return state;
  4. }
  5. 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,使其成为一个空数组:

  1. const reducer = function(state=[], action) {
  2. return state;
  3. }

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

  1. // src/index.js
  2. import { createStore } from "redux";
  3. import { combineReducers } from 'redux';
  4. const productsReducer = function(state=[], action) {
  5. return state;
  6. }
  7. const cartReducer = function(state=[], action) {
  8. return state;
  9. }
  10. const allReducers = {
  11. products: productsReducer,
  12. shoppingCart: cartReducer
  13. }
  14. const rootReducer = combineReducers(allReducers);
  15. let store = createStore(rootReducer);

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

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

  1. // src/index.js
  2. const initialState = {
  3. cart: [
  4. {
  5. product: 'bread 700g',
  6. quantity: 2,
  7. unitCost: 90
  8. },
  9. {
  10. product: 'milk 500ml',
  11. quantity: 1,
  12. unitCost: 47
  13. }
  14. ]
  15. }
  16. const cartReducer = function(state=initialState, action) {
  17. return state;
  18. }
  19. let store = createStore(rootReducer);
  20. console.log("initial state: ", store.getState());

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

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

  1. // src/index.js
  2. const ADD_TO_CART = 'ADD_TO_CART';
  3. const cartReducer = function(state=initialState, action) {
  4. switch (action.type) {
  5. case ADD_TO_CART: {
  6. return {
  7. ...state,
  8. cart: [...state.cart, action.payload]
  9. }
  10. }
  11. default:
  12. return state;
  13. }
  14. }

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

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


  1. function addToCart(product, quantity, unitCost) {
  2. return {
  3. type: ADD_TO_CART,
  4. payload: { product, quantity, unitCost }
  5. }
  6. }

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


  1. let unsubscribe = store.subscribe(() =>
  2. console.log(store.getState())
  3. );
  4. unsubscribe();

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


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

下面是整个index.js文件:

  1. // src/index.js
  2. import { createStore } from "redux";
  3. import { combineReducers } from 'redux';
  4. const productsReducer = function(state=[], action) {
  5. return state;
  6. }
  7. const initialState = {
  8. cart: [
  9. {
  10. product: 'bread 700g',
  11. quantity: 2,
  12. unitCost: 90
  13. },
  14. {
  15. product: 'milk 500ml',
  16. quantity: 1,
  17. unitCost: 47
  18. }
  19. ]
  20. }
  21. const ADD_TO_CART = 'ADD_TO_CART';
  22. const cartReducer = function(state=initialState, action) {
  23. switch (action.type) {
  24. case ADD_TO_CART: {
  25. return {
  26. ...state,
  27. cart: [...state.cart, action.payload]
  28. }
  29. }
  30. default:
  31. return state;
  32. }
  33. }
  34. function addToCart(product, quantity, unitCost) {
  35. return {
  36. type: ADD_TO_CART,
  37. payload: {
  38. product,
  39. quantity,
  40. unitCost
  41. }
  42. }
  43. }
  44. const allReducers = {
  45. products: productsReducer,
  46. shoppingCart: cartReducer
  47. }
  48. const rootReducer = combineReducers(allReducers);
  49. let store = createStore(rootReducer);
  50. console.log("initial state: ", store.getState());
  51. let unsubscribe = store.subscribe(() =>
  52. console.log(store.getState())
  53. );
  54. store.dispatch(addToCart('Coffee 500gm', 1, 250));
  55. store.dispatch(addToCart('Flour 1kg', 2, 110));
  56. store.dispatch(addToCart('Juice 2L', 1, 250));
  57. 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中的代码进行整理:

  1. // src/actions/cart-actions.js
  2. export const ADD_TO_CART = 'ADD_TO_CART';
  3. export function addToCart(product, quantity, unitCost) {
  4. return {
  5. type: ADD_TO_CART,
  6. payload: { product, quantity, unitCost }
  7. }
  8. }
  1. // src/reducers/products-reducer.js
  2. export default function(state=[], action) {
  3. return state;
  4. }
  1. // src/reducers/cart-reducer.js
  2. import { ADD_TO_CART } from '../actions/cart-actions';
  3. const initialState = {
  4. cart: [
  5. {
  6. product: 'bread 700g',
  7. quantity: 2,
  8. unitCost: 90
  9. },
  10. {
  11. product: 'milk 500ml',
  12. quantity: 1,
  13. unitCost: 47
  14. }
  15. ]
  16. }
  17. export default function(state=initialState, action) {
  18. switch (action.type) {
  19. case ADD_TO_CART: {
  20. return {
  21. ...state,
  22. cart: [...state.cart, action.payload]
  23. }
  24. }
  25. default:
  26. return state;
  27. }
  28. }
  1. // src/reducers/index.js
  2. import { combineReducers } from 'redux';
  3. import productsReducer from './products-reducer';
  4. import cartReducer from './cart-reducer';
  5. const allReducers = {
  6. products: productsReducer,
  7. shoppingCart: cartReducer
  8. }
  9. const rootReducer = combineReducers(allReducers);
  10. export default rootReducer;
  1. // src/store.js
  2. import { createStore } from "redux";
  3. import rootReducer from './reducers';
  4. let store = createStore(rootReducer);
  5. export default store;
  1. // src/index.js
  2. import store from './store.js';
  3. import { addToCart } from './actions/cart-actions';
  4. console.log("initial state: ", store.getState());
  5. let unsubscribe = store.subscribe(() =>
  6. console.log(store.getState())
  7. );
  8. store.dispatch(addToCart('Coffee 500gm', 1, 250));
  9. store.dispatch(addToCart('Flour 1kg', 2, 110));
  10. store.dispatch(addToCart('Juice 2L', 1, 250));
  11. unsubscribe();

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

  1. // src/reducers/cart-actions.js

  2. export const UPDATE_CART = 'UPDATE_CART';
  3. export const DELETE_FROM_CART = 'DELETE_FROM_CART';

  4. export function updateCart(product, quantity, unitCost) {
  5. return {
  6. type: UPDATE_CART,
  7. payload: {
  8. product,
  9. quantity,
  10. unitCost
  11. }
  12. }
  13. }
  14. export function deleteFromCart(product) {
  15. return {
  16. type: DELETE_FROM_CART,
  17. payload: {
  18. product
  19. }
  20. }
  21. }
  1. // src/reducers/cart-reducer.js

  2. export default function(state=initialState, action) {
  3. switch (action.type) {
  4. case ADD_TO_CART: {
  5. return {
  6. ...state,
  7. cart: [...state.cart, action.payload]
  8. }
  9. }
  10. case UPDATE_CART: {
  11. return {
  12. ...state,
  13. cart: state.cart.map(item => item.product === action.payload.product ? action.payload : item)
  14. }
  15. }
  16. case DELETE_FROM_CART: {
  17. return {
  18. ...state,
  19. cart: state.cart.filter(item => item.product !== action.payload.product)
  20. }
  21. }
  22. default:
  23. return state;
  24. }
  25. }

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

  1. // src/index.js

  2. // Update Cart
  3. store.dispatch(updateCart('Flour 1kg', 5, 110));
  4. // Delete from Cart
  5. 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包。
  1. yarn add redux-devtools-extension
  • 一旦安装完成,我们对store.js稍作修改:
  1. // src/store.js
  2. import { createStore } from "redux";
  3. import { composeWithDevTools } from 'redux-devtools-extension';
  4. import rootReducer from './reducers';
  5. const store = createStore(rootReducer, composeWithDevTools());
  6. export default store;

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

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

集成React

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

  • 首先,停止服务器,并安装react-redux包:
  1. yarn add react-redux
  • 接下来,在index.js中加入React代码。我们还将使用Provider类将React应用程序包装在Redux容器中:
  1. // src/index.js

  2. import React from 'react';
  3. import ReactDOM from 'react-dom';
  4. import { Provider } from 'react-redux';
  5. const App = <h1>Redux Shopping Cart</h1>;
  6. ReactDOM.render(
  7. <Provider store={store}>
  8. { App }
  9. </Provider> ,
  10. document.getElementById('root')
  11. );

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

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

总结

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

[转] Redux入门教程(快速上手)的更多相关文章

  1. Flask入门和快速上手

    目录 Flask入门和快速上手 python三大主流框架对比 Flask安装 依赖 可选依赖 创建flask项目 flask最小应用--hello word 非法导入名称 调试模式 路由 唯一的 UR ...

  2. Redux 入门教程

    Redux 入门教程(三):React-Redux 的用法(53@2016.09.21) Redux 入门教程(二):中间件与异步操作(32@2016.09.20) Redux 入门教程(一):基本用 ...

  3. Spring Boot2 快速入门教程-到上手

    Spring Boot2 教程合集 入门 纯 Java 代码搭建 SSM 环境 创建一个 Spring Boot 项目的三种方法 理解 Spring Boot 项目中的 parent 基础配置 配置文 ...

  4. Redux 入门教程(二):中间件与异步操作

    上一篇文章,介绍了 Redux 的基本做法:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染. 但是,一个关键问题没有解决:异步操作怎么办?Action 发出以后, ...

  5. Redux 入门教程(三):React-Redux 的用法

    为了方便使用,Redux 的作者封装了一个 React 专用的库 React-Redux,本文主要介绍它. 这个库是可以选用的.实际项目中,你应该权衡一下,是直接使用 Redux,还是使用 React ...

  6. Redux 入门教程(一):基本用法

    转自http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_one_basic_usages.html(仅供个人学习使用) 首先明确一点, ...

  7. pc/移动端(手机端)浏览器的直播rtmp hls(适合入门者快速上手)

    一.直播概述 关于直播,大概的过程是:推流端——>源站——>客户端拉流,用媒介播放 客户端所谓的拉流就是一个播放的地址url,会有多种类型的流: 视频直播服务目前支持三种直播协议,分别是R ...

  8. AFNnetworking快速教程,官方入门教程译

    AFNnetworking快速教程,官方入门教程译 分类: IOS2013-12-15 20:29 12489人阅读 评论(5) 收藏 举报 afnetworkingjsonios入门教程快速教程 A ...

  9. Redux 入门到高级教程

    Redux 是 JavaScript 状态容器,提供可预测化的状态管理. (如果你需要一个 WordPress 框架,请查看 Redux Framework.) Redux 除了和 React 一起用 ...

随机推荐

  1. union的特性,去重与不去重

    转载:https://blog.csdn.net/kingmax54212008/article/details/33762921 union的特性,去重与不去重 集合操作有 并,交,差 3种运算. ...

  2. PLSQL:orecal,tnsname简介

    导入ORACLE遇到很多问题,学了好多,其中很长时间花在网络配置上,刚开始学,具体原因不知道,先把搜集到的好文章存下来,以后慢慢研究. 监听配置文件             为了使得外部进程 如 CA ...

  3. 转 spring注解式参数校验

    转自: https://blog.csdn.net/jinzhencs/article/details/51682830 转自: https://blog.csdn.net/zalan01408980 ...

  4. BZOJ 3669 魔法森林

    LCT维护生成树 先按照a的权值把边排序,离线维护b的最小生成树. 将a排序后,依次动态加边,我们只需要关注b的值.要保证1-n花费最少,两点间的b值肯定是越小越好,所以我们可以考虑以b为关键字维护最 ...

  5. Euler Circuit UVA - 10735(混合图输出路径)

    就是求混合图是否存在欧拉回路 如果存在则输出一组路径 (我就说嘛 咱的代码怎么可能错.....最后的输出格式竟然w了一天 我都没发现) 解析: 对于无向边定向建边放到网络流图中add(u, v, 1) ...

  6. position:sticky

    使用sticky定位可以简洁的实现固定功能 例如,左右布局页面,左侧菜单,右侧内容,内容区域滚动时,不希望菜单区域滚动,而是固定不动 以往要实现这个功能,需要使用fixed定位菜单,菜单脱离文档流,布 ...

  7. [powershell] 批量重命名,修改文件名中的部分字符串

    实例:替换一个目录下所有的字幕文件从720p到1080p ls $Path -Recurse |ForEach-Object{Rename-Item $_.FullName $_.FullName.R ...

  8. 联想的笔记本有隐藏分区 导致无法安装win10 eufi启动 报错:windows无法更新计算机的启动配置。无法安装

    联想的笔记本都带着类似一键还原等的系统恢复软件,这些软件往往是将出厂设置备份在单 独的一个分区,此分区默认为隐藏,在 Windows 的磁盘管理中可以看到.打开磁盘管理器 的方法是右击计算机——管理, ...

  9. 20175221 2018-2019-2 《Java程序设计》第一周学习总结

    20175221 2018-2019-2 <Java程序设计>第一周学习总结 教材学习内容总结 本周通过观看书本配套视频,学到了如解释器,编译器等一些简单概念. 还懂得了java的一些简单 ...

  10. python dic字典使用

    #!/usr/bin/env python -*-''' 字典的基本组成及用法: dict={key:value} dict[key]=value 字典是无序的. key值是唯一属性,一对一,几个ke ...