Redux入門教程(快速上手)

典型的Web應用程序一般由共享數據的多個UI組件組成。一般,多個組件的任務是負責展現同一對象的不一樣屬性。這個對象表示可隨時更改的狀態。在多個組件之間保持狀態的一致性會是一場噩夢,特別是若是有多個通道用於更新同一個對象。

舉個?,一個帶有購物車的網站。在頂部,咱們用一個UI組件顯示購物車中的商品數量。咱們還能夠用另外一個UI組件,顯示購物車中商
品的總價。若是用戶點擊添加到購物車按鈕,則這兩個組件應當即更新當前的數據。若是用戶從購物車中刪除商品、更改數目、使用優惠券或者更改送貨地點,則相關的UI組件都應該更新出正確的信息。
能夠看到,隨着功能範圍的擴大,一個簡單的購物車將會很難保持數據同步。javascript

在這篇文章中,我將介紹Redux框架,它能夠幫助你以簡單易用的方式構建複雜項目並進行維護。爲了使學習更容易,咱們將使用一個簡化的購物車項目來學習Redux的工做遠離。你須要至少熟悉React庫,由於你之後須要將其與Redux集成。java

學習前提

在咱們開始之前,確保你熟悉如下知識:node

同時,確保你的設備已經安裝:react

什麼是Redux

Redux是一個流行的JavaScript框架,爲應用程序提供一個可預測的狀態容器。Redux基於簡化版本的Flux框架,Flux是Facebook開發的一個框架。在標準的MVC框架中,數據能夠在UI組件和存儲之間雙向流動,而Redux嚴格限制了數據只能在一個方向上流動。 見下圖:ios

圖片描述

在Redux中,全部的數據(好比state)被保存在一個被稱爲store的容器中 → 在一個應用程序中只能有一個。store本質上是一個狀態樹,保存了全部對象的狀態。任何UI組件均可以直接從store訪問特定對象的狀態。要經過本地或遠程組件更改狀態,須要分發一個action分發在這裏意味着將可執行信息發送到store。當一個store接收到一個action,它將把這個action代理給相關的reducerreducer是一個純函數,它能夠查看以前的狀態,執行一個action而且返回一個新的狀態。git

理解不變性(Immutability)

在咱們開始實踐以前,須要先了解JavaScript中的不變性意味着什麼。在編碼中,咱們編寫的代碼一直在改變變量的值。這是可變性。可是可變性經常會致使意外的錯誤。若是代碼只處理原始數據類型(numbers, strings, booleans),那麼你不用擔憂。可是,若是在處理Arrays和Objects時,則須要當心執行可變操做。
接下來演示不變性github

  • 打開終端並啓動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中,有三種不一樣的實現方式:web

  1. 使用Immutable.js建立不可變的數據結構。
  2. 使用JavaScript庫(如UnderscoreLodash)來執行不可變的操做。
  3. 使用ES6方法執行不可變操做。

本文將使用ES6方法,由於它已經在NodeJS環境中可用了,在終端中,執行如下操做:chrome

> 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功能,咱們能夠用它們執行不可變操做:

配置Redux

配置Redux開發環境的最快方法是使用create-react-app工具。在開始以前,確保已經安裝並更新了nodejsnpmyarn。咱們生成一個redux-shopping-cart項目並安裝Redux

create-react-app redux-shopping-cart

cd 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.js

import { 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.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會自動刷新。能夠在控制檯中確認新的商品已經添加了。

圖片描述

組織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.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-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很強大。你能夠在action, statediff(方法差別)之間切換。選擇左側面板上的不一樣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相關聯。此外,還可使用ExpressFeathers這樣的框架來設置API。API將爲咱們的應用程序提供對數據庫服務的訪問。

感謝網友整理了本文的相關代碼,如須要,請移步這裏

在Redux中,咱們還能夠安裝其餘一些包,好比axios等。咱們React組件的state將由Redux處理,確保全部組件與數據庫API的同步。想要更進一步的學習,請看Build a CRUD App Using React, Redux and FeathersJS

總結

我但願本文能對你有所幫助。固然,還有不少相關的內容須要學習。例如,處理異步操做、身份驗證、日誌記錄等。若是以爲Redux適合你,能夠看看如下幾篇文章:

這篇文章是看到比較簡明的Redux教程。固然也是翻譯過來噠,文中提到了不少延伸文章,我還在一個個學習當中,遇到不錯的依然會翻譯給你們的。

?喜歡的話記得收藏哦!

相關文章
相關標籤/搜索