目录结构
+-- 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版本得到简化。
/* 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相等的常量。
/* 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),也强烈推荐这样去做。
/* 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的方法中出现过,只是不太常用。简单快速的用代码样例来回顾一下:
/* 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没有发生改变的组件子树。
/* 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了。
/* 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为例子:
/* 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的机会。
/* 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。
/* 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 = { '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调试,为你在开发过程中带来乐趣。
/* 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=([^&]+)/)), createStore ) }else { buildStore = compose(applyMiddleware(thunk), createStore) } export default function configureStore(initialState) { return buildStore(rootReducer, initialState); }
在你的代码中加上上面的两段代码,运行npm run debug命令,就可以用调试工具来管理你的项目了。
效果图:
https://github.com/matthew-sun/redux-example