典型的Web應用程序一般由共享數據的多個UI組件組成。一般,多個組件的任務是負責展現同一對象的不一樣屬性。這個對象表示可隨時更改的狀態。在多個組件之間保持狀態的一致性會是一場噩夢,特別是若是有多個通道用於更新同一個對象。
舉個?,一個帶有購物車的網站。在頂部,咱們用一個UI組件顯示購物車中的商品數量。咱們還能夠用另外一個UI組件,顯示購物車中商
品的總價。若是用戶點擊添加到購物車
按鈕,則這兩個組件應當即更新當前的數據。若是用戶從購物車中刪除商品、更改數目、使用優惠券或者更改送貨地點,則相關的UI組件都應該更新出正確的信息。
能夠看到,隨着功能範圍的擴大,一個簡單的購物車將會很難保持數據同步。javascript
在這篇文章中,我將介紹Redux框架,它能夠幫助你以簡單易用的方式構建複雜項目並進行維護。爲了使學習更容易,咱們將使用一個簡化的購物車項目
來學習Redux的工做遠離。你須要至少熟悉React庫,由於你之後須要將其與Redux集成。java
在咱們開始之前,確保你熟悉如下知識:node
同時,確保你的設備已經安裝:react
Redux是一個流行的JavaScript框架,爲應用程序提供一個可預測的狀態容器。Redux基於簡化版本的Flux框架,Flux是Facebook開發的一個框架。在標準的MVC框架中,數據能夠在UI組件和存儲之間雙向流動,而Redux嚴格限制了數據只能在一個方向上流動。 見下圖:ios
在Redux中,全部的數據(好比state)被保存在一個被稱爲store
的容器中 → 在一個應用程序中只能有一個。store
本質上是一個狀態樹,保存了全部對象的狀態。任何UI組件均可以直接從store
訪問特定對象的狀態。要經過本地或遠程組件更改狀態,須要分發一個action
。分發在這裏意味着將可執行信息發送到store
。當一個store
接收到一個action
,它將把這個action
代理給相關的reducer
。reducer
是一個純函數,它能夠查看以前的狀態,執行一個action
而且返回一個新的狀態。git
在咱們開始實踐以前,須要先了解JavaScript中的不變性
意味着什麼。在編碼中,咱們編寫的代碼一直在改變變量的值。這是可變性
。可是可變性
經常會致使意外的錯誤。若是代碼只處理原始數據類型(numbers, strings, booleans),那麼你不用擔憂。可是,若是在處理Arrays和Objects時,則須要當心執行可變操做。
接下來演示不變性
:github
> 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
本文將使用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開發環境的最快方法是使用create-react-app
工具。在開始以前,確保已經安裝並更新了nodejs
,npm
和yarn
。咱們生成一個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);
讓我解釋一下上面的代碼:
redux
包中引入createStore()
方法。咱們建立了一個名爲reducer的方法。第一個參數state
是當前保存在store
中的數據,第二個參數action
是一個容器,用於:
type
- 一個簡單的字符串常量,例如ADD, UPDATE, DELETE等。payload
- 用於更新狀態的數據。注意到,我在第二點中所提到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修改成productReducer
和cartReducer
。建立這兩個空的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會自動刷新。能夠在控制檯中確認新的商品已經添加了。
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.js
和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 } } }
// 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擁有不少第三方的調試工具,可用於分析代碼和修復bug。最受歡迎的是time-travelling tool,即redux-devtools-extension。設置它只須要三個步驟。
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
, state
和diff(方法差別)
之間切換。選擇左側面板上的不一樣action
,觀察狀態樹的變化。你還能夠經過進度條來播放actions
序列。甚至能夠經過工具直接分發操做信息。具體的請查看文檔。
在本文開頭,我提到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的store
和action
相關聯。此外,還可使用Express和Feathers這樣的框架來設置API。API將爲咱們的應用程序提供對數據庫服務的訪問。
感謝網友整理了本文的相關代碼,如須要,請移步這裏。
在Redux中,咱們還能夠安裝其餘一些包,好比axios
等。咱們React組件的state
將由Redux處理,確保全部組件與數據庫API的同步。想要更進一步的學習,請看Build a CRUD App Using React, Redux and FeathersJS。
我但願本文能對你有所幫助。固然,還有不少相關的內容須要學習。例如,處理異步操做、身份驗證、日誌記錄等。若是以爲Redux適合你,能夠看看如下幾篇文章:
這篇文章是看到比較簡明的Redux教程。固然也是翻譯過來噠,文中提到了不少延伸文章,我還在一個個學習當中,遇到不錯的依然會翻譯給你們的。?喜歡的話記得收藏哦!