使用Redux管理你的React應用

原文做者:http://www.cnblogs.com/Leo_wl/p/4780750.htmljavascript

 
由於redux和react的版本更新的比較頻繁,博客園這裏用的redux版本是1.0.1,若是你關心最新版本的使用技巧,歡迎來個人Github查看(https://github.com/matthew-sun/blog/issues/18) ,我會在這裏進行持續的更新和糾錯。

React是最好的前端庫,由於其發源於世界上最好的後端語言框架。 ---信仰css

4.0 will likely be the last major release. Use Redux instead. It's really great. —Flummox框架做者 acdliteAndrew Clarkhtml

爲何使用React還須要使用別的框架來搭配?

React的核心是使用組件定義界面的表現,是一個View層的前端庫,那麼在使用React的時候咱們一般還須要一套機制去管理組件與組件之間,組件與數據模型之間的通訊。前端

爲何使用Redux?

Facebook官方提出了FLUX思想管理數據流,同時也給出了本身的實現來管理React應用。但是當我打開FLUX的文檔時候,繁瑣的實現,又臭又長的文檔,實在難以讓我有使用它的慾望。幸虧,社區中和我有相似想法的不在少數,github上也涌現了一批關於實現FLUX的框架,比較出名的有Redux,Reflux,Flummoxjava

其中Redux的簡單和有趣的編程體驗是最吸引個人地方。react

  • 簡單。和其它的FLUX實現不同,Redux只有惟一的state樹,無論項目變的有多複雜,我也僅僅只須要管理一個State樹。可能你會有疑問,一個state樹就夠用了?這個state樹該有多大?彆着急,Redux中的Reducer機制能夠解決這個問題。git

  • 有趣。忙於迭代項目的你,體會編程帶來的趣味是有多久沒有體會到了?瞧下面這張圖,右邊那個調試工具是啥?整個應用的action和state都這麼被輕鬆的管理了?行爲還能被保存,刪除,回滾,重置?修改了代碼,頁面不刷新也能產生變化?別開玩笑了,不行,世界那麼大,讓我去試試!github

Redux DevTools

注:Redux開發調試工具:redux-devtools
React應用無刷新保存工具:hot-loaderajax

不明真相的羣衆,可能這裏須要我來安利一下Flux數據流的思想,看圖:
╔═════════╗ ╔════════╗ ╔═════════════════╗ ║ Actions ║──────>║ Stores ║──────>║ View Components ║ ╚═════════╝ ╚════════╝ ╚═════════════════╝ ^ │ └──────────────────────────────────────┘ 注意:圖片僅僅是FLUX思想,而不是Facebook的實現。

大體的過程是這樣的,View層不能直接對state進行操做,而須要依賴Actions派發指令來告知Store修改狀態,Store接收Actions指令後發生相應的改變,View層同時跟着Store的變化而變化。npm

舉個例子:A組件要使B組件發生變化。首先,A組件須要執行一個Action,告知綁定B組件的Store發生變化,Store接收到派發的指令後改變,那相應的B組件的視圖也就發生了改變。假如C,D,E,F組件綁定了和B組件相同的Store,那麼C,D,E,F也會跟着變化。

使用React和Redux開發一個小程序

爲了更好的描述怎麼樣使用Redux管理React應用,我作了一個Manage Items的小例子。你能夠在這裏找到所有的源代碼:https://github.com/matthew-sun/redux-example。

Manage Items

快速查看
1.git clone git@github.com:matthew-sun/redux-example.git
 
2.npm install && npm start
 
3.open localhost:3000

目錄結構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.
+-- app
|   +-- actions
|       +-- index.js
|   +-- components
|       +-- content.js
|       +-- footer.js
|       +-- searchBar.js
|   +-- constants
|       +-- ActionTypes.js
|   +-- containers
|       +-- App.js
|   +-- reducers
|       +-- index.js
|       +-- items.js
|       +-- filter.js
|   +-- utils
|   +-- configureStore.js
|   +-- index.js
+-- css
|   +-- pure.min.css
+-- index.html

Index.js

在入口文件中,咱們須要把App和redux創建起聯繫。Provider是react-redux提供的組件,它的做用是把store和視圖綁定在了一塊兒,這裏的Store就是那個惟一的State樹。當Store發生改變的時候,整個App就能夠做出對應的變化。{() => }是聲明瞭一個返回的函數傳進Provider的props.children裏,這個方法將會在React的 0.14版本獲得簡化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* app/index.js */
 
import React from 'react';
import { Provider } from 'react-redux';
import App from './containers/App';
import configureStore from './configureStore';
 
const store = configureStore();
 
React.render(
    <div>
        <Provider store={store}>
            {() => <App /> }
        </Provider>
    </div>,
    document.getElementById('app'));

Constants

keyMirror這個方法很是的有用,它能夠幫助咱們輕鬆建立與鍵值key相等的常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* app/constants/actionTypes.js */
 
import keyMirror from 'react/lib/keyMirror';
 
export default keyMirror({
    ADD_ITEM: null,
    DELETE_ITEM: null,
    DELETE_ALL: null,
    FILTER_ITEM: null
});
 
// 等於
// export const ADD_ITEM = 'ADD_ITEM';
// export const DELETE_ITEM = 'DELETE_ITEM';
// export const DELETE_ALL = 'DELETE_ALL';
// export const FILTER_ITEM = 'FILTER_ITEM';

Actions

Action向store派發指令,action 函數會返回一個帶有 type 屬性的 Javascript Plain Object,store將會根據不一樣的action.type來執行相應的方法。addItem函數的異步操做我使用了一點小技巧,使用redux-thunk中間件去改變dispatch,dispatch是在View層中用bindActionCreators綁定的。使用這個改變的dispatch咱們能夠向store發送異步的指令。好比說,能夠在action中放入向服務端的請求(ajax),也強烈推薦這樣去作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* app/actions/index.js */
 
import { ADD_ITEM, DELETE_ITEM, DELETE_ALL, FILTER_ITEM } from '../constants/actionTypes';
 
export function addItem(item) {
    return dispatch => {
       setTimeout(() => dispatch({type: ADD_ITEM}), 1000)
    }
}
export function deleteItem(item, e) {
    return {
       type: DELETE_ITEM,
       item
    }
}
export function deleteAll() {
    return {
       type: DELETE_ALL
    }
}
export function filterItem(e) {
    let filterItem = e.target.value;
    return {
       type: FILTER_ITEM,
       filterItem
    }
}

Reducers

Redux有且只有一個State狀態樹,爲了不這個狀態樹變得愈來愈複雜,Redux經過 Reducers來負責管理整個應用的State樹,而Reducers能夠被分紅一個個Reducer。

Reduce在javascript Array的方法中出現過,只是不太經常使用。簡單快速的用代碼樣例來回顧一下:

1
2
3
4
5
6
7
8
9
10
11
  /* Array.prototype.reduce */
 
var arr = [1,2,3,4];
var initialValue = 5;
var result = arr.reduce(function(previousValue, currentValue) {
    return previousValue + currentValue
}, initialValue)
console.log(result)
// 15
// 該回調函數的返回值爲累積結果,而且此返回值在下一次調用該回調函數時做爲參數提供。
// 整個函數執行的過程大體是這樣 ((((5+1)+2)+3)+4)

回到Redux中來看,整個的狀態就至關於從[初始狀態]merge一個[action.state]從而獲得一個新的狀態,隨着action的不斷傳入,不斷的獲得新的狀態的過程。(previousState, action) => newState,注意:任何狀況下都不要改變previousState,由於這樣View層在比較State的改變時只須要簡單比較便可,而避免了深度循環比較。Reducer的數據結構咱們能夠用immutable-js,這樣咱們在View層只須要react-immutable-render-mixin插件就能夠輕鬆的跳過更新那些state沒有發生改變的組件子樹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* app/reducers/items.js */
 
import Immutable from 'immutable';
import { ADD_ITEM, DELETE_ITEM, DELETE_ALL } from '../constants/actionTypes';
 
const initialItems = Immutable.List([1,2,3]);
 
export default function items(state = initialItems, action) {
    switch(action.type) {
        case ADD_ITEM:
            return state.push( state.size !=0 ? state.get(-1)+1 : 1 );
        case DELETE_ITEM:
            return state.delete( state.indexOf(action.item) );
        case DELETE_ALL:
            return state.clear();
        default:
            return state;
    }
}
鏈接reducers

Redux提供的combineReducers函數能夠幫助咱們把reducer組合在一塊兒,這樣咱們就能夠把Reducers拆分紅一個個小的Reducer來管理Store了。

1
2
3
4
5
6
7
8
9
10
11
12
/* app/reducers/index.js */
 
import { combineReducers } from 'redux';
import items from './items';
import filter from './filter';
 
const rootReducer = combineReducers({
  items,
  filter
});
 
export default rootReducer;

Middleware

在Redux中,Middleware 主要是負責改變Store中的dispatch方法,從而能處理不一樣類型的 action 輸入,獲得最終的 Javascript Plain Object 形式的 action 對象。

redux-thunk爲例子:

1
2
3
4
5
6
7
8
/* redux-thunk */ 
export default function thunkMiddleware({ dispatch, getState }) {
  return next =>
     action =>
       typeof action === ‘function’ ?
         action(dispatch, getState) :
         next(action);
}

當ThunkMiddleware 判斷action傳入的是一個函數,就會爲該thunk函數補齊dispatch和getState參數,不然,就調用next(action),給後續的Middleware(Middleware 插件能夠被綁定多個)獲得使用dispatch的機會。

1
2
3
4
5
6
7
8
9
10
/* app/configureStore.js */
 
import { compose, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
 
var buildStore = compose(applyMiddleware(thunk), createStore)
export default function configureStore(initialState) {
   return buildStore(rootReducer, initialState);
}

UI

智能組件和木偶組件,由於本文主要是介紹Redux,對這個感興趣的同窗能夠看一下這篇文章Smart and Dumb Components。本項目中在結構上會把智能組件放在containers中,木偶組件放於components中。

containers

智能組件,會經過react-redux函數提供的connect函數把state和actions轉換爲旗下木偶組件所須要的props。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* app/containers/App.js */
 
import React from 'react';
import SearchBar from '../components/searchBar';
import Content from '../components/content';
import Footer from '../components/footer';
import { connect } from 'react-redux';
import ImmutableRenderMixin from 'react-immutable-render-mixin';
import * as ItemsActions from '../actions';
import { bindActionCreators } from 'redux';
 
let App = React.createClass({
     mixins: [ImmutableRenderMixin],
     propTypes: {
         items: React.PropTypes.object,
         filter: React.PropTypes.string
     },
     render() {
         let styles = {
             width: '200px',
             margin: '30px auto 0'
         }
         const actions = this.props.actions;
         return (
             <div style={styles}>
                 <h2>Manage Items</h2>
                 <SearchBar filterItem={actions.filterItem}/>
                 <Content items={this.props.items} filter={this.props.filter} deleteItem={actions.deleteItem}/>
                 <Footer addItem={actions.addItem} deleteAll={actions.deleteAll}/>
             </div>
         )
     }
 })
 
export default connect(state => ({
     items: state.items,
     filter: state.filter
}), dispatch => ({
     actions: bindActionCreators(ItemsActions, dispatch)
}))(App);
components

木偶組件,各司其職,沒有什麼關於actions和stores的依賴,拿出項目中也可獨立使用,甚至能夠和別的actions,stores進行綁定。

  • SearchBar:查找Item。
  • Content:控制Items的顯示,刪除一個Item。
  • Footer:新增Item,刪除所有Item。

調試工具

使用redux-devtools調試,爲你在開發過程當中帶來樂趣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* app/index.js */
 
function renderDevTools(store) {
  if (__DEBUG__) {
    let {DevTools, DebugPanel, LogMonitor} = require('redux-devtools/lib/react');
    return (
      <DebugPanel top right bottom>
        <DevTools store={store} monitor={LogMonitor} />
      </DebugPanel>
    );
  }else {
    return null;
  }
}
 
React.render(
    <div>
        <Provider store={store}>
            {() => <App /> }
        </Provider>
        {renderDevTools(store)}
    </div>,
  document.getElementById('app'));
/* app/configureStore.js */
 
var buildStore;
if(__DEBUG__) {
  buildStore = compose(
    applyMiddleware(thunk),
    require('redux-devtools').devTools(),
    require('redux-devtools').persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)),
    createStore
  )
}else {
  buildStore = compose(applyMiddleware(thunk), createStore)
}
 
export default function configureStore(initialState) {
  return buildStore(rootReducer, initialState);
}

在你的代碼中加上上面的兩段代碼,運行npm run debug命令,就能夠用調試工具來管理你的項目了。

延伸閱讀

寫在最後

剛接觸到Redux和React技術的時候,我幾乎是夜夜難以入眠的,技術革新帶來的新的思想老是不斷的刺激着個人大腦。很是建議你也能來試試Redux,體會我在開發中獲得的這種幸福感。

相關文章
相關標籤/搜索