Redux管理你的React應用


使用Redux管理你的React應用

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

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

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

為什么使用React還需要使用別的框架來搭配?

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

為什么使用Redux?

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

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

  • 簡單。和其它的FLUX實現不一樣,Redux只有唯一的state樹,不管項目變的有多復雜,我也僅僅只需要管理一個State樹。可能你會有疑問,一個state樹就夠用了?這個state樹該有多大?別着急,Redux中的Reducer機制可以解決這個問題。

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

Redux DevTools

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

不明真相的群眾,可能這里需要我來安利一下Flux數據流的思想,看圖:
  ╔═════════╗       ╔════════╗       ╔═════════════════╗
  ║ Actions ║──────>║ Stores ║──────>║ View Components ║
  ╚═════════╝       ╚════════╝       ╚═════════════════╝
       ^                                      │
       └──────────────────────────────────────┘

  注意:圖片僅僅是FLUX思想,而不是Facebook的實現。

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

舉個例子: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,體會我在開發中得到的這種幸福感。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM