Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用Redux管理你的React应用 #18

Open
matthew-sun opened this issue Aug 30, 2015 · 72 comments
Open

使用Redux管理你的React应用 #18

matthew-sun opened this issue Aug 30, 2015 · 72 comments

Comments

@matthew-sun
Copy link
Owner

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应用无刷新保存工具:react-transform

不明真相的群众,可能这里需要我来安利一下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

目录结构

.
+-- 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
+-- scss
|   +-- pure.scss
+-- index.html

Index.js

在入口文件中,我们需要把App和redux建立起联系。Provider是react-redux提供的组件,它的作用是把store和视图绑定在了一起,这里的Store就是那个唯一的State树。当Store发生改变的时候,整个App就可以作出对应的变化。这里的会传进Provider的props.children里。

/* 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 'fbjs/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) {
  const store = buildStore(rootReducer, initialState);
  return store;
}

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 = {
             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调试,为你在开发过程中带来乐趣。

/* 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 */

import { compose, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

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) {
  const store = buildStore(rootReducer, initialState);

  if(module.hot) {
    module.hot.accept('./reducers', () => {
      store.replaceReducer(require('./reducers'));
    });
  }

  return store;
}

在你的代码中加上上面的两段代码,运行npm run debug命令,就可以用调试工具来管理你的项目了。

延伸阅读

写在最后

刚接触到Redux和React技术的时候,我几乎是夜夜难以入眠的,技术革新带来的新的思想总是不断的刺激着我的大脑。非常建议你也能来试试Redux,体会我在开发中得到的这种幸福感。

如果有任何想要了解的,欢迎来我的github和我一起互动交流。​

@luqin
Copy link

luqin commented Sep 21, 2015

这么好的文章没推广啊

@denvey
Copy link

denvey commented Sep 27, 2015

真的非常好

@matthew-sun
Copy link
Owner Author

@denvey 谢谢,写这篇文章的初衷就是想通过用最简单的方式阐明redux的思想及用法。

@wing-kai
Copy link

wing-kai commented Dec 8, 2015

项目运行不了……

@matthew-sun
Copy link
Owner Author

@wing-kai 刚测试了一下,可以运行的,看看有没有npm install。如果还有什么问题的话,可以加我QQ,527551750。

@wing-kai
Copy link

wing-kai commented Dec 8, 2015

@matthew-sun
npm install 的时候提示

npm WARN EPEERINVALID redux--example@1.0.1 requires a peer of node-sass@^3.3.3 but none was installed.
npm WARN EPEERINVALID sass-loader@2.0.1 requires a peer of node-sass@^3.2.0 but none was installed.

运行的时候

ERROR in Cannot find module 'node-sass'

我的npm版本是3.3.12

@matthew-sun
Copy link
Owner Author

@wing-kai 试一下用cnpm

@smilingpoplar
Copy link

赞,终于有个能看懂的redux例子了

@anxsec
Copy link

anxsec commented Dec 16, 2015

为了更好的描述怎么样使用Redux管理React应用,我做了一个Manage Items的小例子。你可以在这里找到全部的源代码
此句后面跟的链接结尾多了个句号,导致点击打开显示not found.

@matthew-sun
Copy link
Owner Author

@anxsec 赞,已改~

@zhongning7924
Copy link

@matthew-sun 我的出现了和 @wing-kai一样的问题,怎么解决的啊?

@matthew-sun
Copy link
Owner Author

@zhongning7924 你的node版本是多少呢,我的是4.x

@zhongning7924
Copy link

@matthew-sun 我的是5.x,window系统。

@wing-kai
Copy link

@zhongning7924 翻墙安装

或者克隆这个仓库 git clone https://github.com/sass/node-sass.git

@Bigerfe
Copy link

Bigerfe commented Jan 4, 2016

为啥老是安装失败啊;npm install 老是失败。

@matthew-sun
Copy link
Owner Author

@hichaozjp node版本确认是4.x,npm安装确认用的是cnpm吗

@luqin
Copy link

luqin commented Jan 4, 2016

@hichaozjp 可以试试 node-sass 安装失败解决方案

@JoeQian2lfy
Copy link

您好,我是刚学react的菜渣,
请问configureStore.js 是用作什么的?
我在网上找到的其他教程都没有这个东西并直接像下面类似的代码

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './app.jsx';

ReactDOM.render(
    <Provider store={store}>
        <App></App>
    </Provider>,
    document.querySelector("#root")
);

但是我这么写以后就报错,store is not define
希望您能抽空讲解下,万分感谢

@matthew-sun
Copy link
Owner Author

@JoeQian2lfy
可以这样理解,redux构建了一个管理react应用的唯一state树,configureStore的功能就是生成这个state树,它承担了应用中间件和合并reducer的功能。
你在网上找的其他教程,可能是因为没有用redux,所以没有这个东西。去掉那句话的话,因为store没有定义,自然会报错了。

@JoeQian2lfy
Copy link

@matthew-sun 感谢,我翻了下redux的中文文档已经有了初步认识了。
主要之前步子跨太大没好好了解redux

@Galen-Yip
Copy link

@matthew-sun �本来想提提issue的,但免得打乱了你的文章- - 。。。
我这里问个问题吧 为啥actions里面只有ADD_ITEM中要写dispatch呢 ?
感觉上不合理吧?这里应该只是actionCreator,并不涉及dispatch的操作吧??

@matthew-sun
Copy link
Owner Author

@Galen-Yip 写ADD_ITEM的目的是用setTimeout模拟异步操作,这里是需要用dispatch派发的

@Galen-Yip
Copy link

@matthew-sun 这里是actionCreator,应该是只做return action
dispatch在bindActionCreators已经做了,要实现setTimeout的话 可以这样吧?

export function addItem() {
    setTimeout(() => {
        type: ADD_ITEM
    },1000)
}

@matthew-sun
Copy link
Owner Author

@Galen-Yip dispatch是作为参数注入到action里,按照你的方法改会报这个错误,你可以试试
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.

@Galen-Yip
Copy link

@matthew-sun 我明白你的意思,但bindActionCreator传入了dispatch,应该是已经自动去做了dispatch了。按分层而言,理应不应该在action那里面去dispatch吧? 你觉得呢?

@Galen-Yip
Copy link

@matthew-sun 再请教一个问题 https://github.com/matthew-sun/redux-example/blob/master/public/app/containers/App.js
这个里面return之前,console.log(this.props.action) 为什么是空的???但确实是传进了jsx里

@matthew-sun
Copy link
Owner Author

@Galen-Yip 我刚刚打了一下不是空的

@wuwb
Copy link

wuwb commented Apr 10, 2016

@laixstx 看下 webpack 吧

@matthew-sun
Copy link
Owner Author

@laixstx 用webpack --config webpack.config.min.js命令可以打包文件在本地,然后你再发布到生产环境上面

@Gala-1024
Copy link

好文章。谢谢

@wuwb
Copy link

wuwb commented May 12, 2016

@WillBean 现在可以试试 cross-env,在 package.json 中增加

script: {
    start: cross-env DEBUG=true node server.js
}

然后安装 cross-env

nap i cross-env --save-dev

最后运行

nam run start

关于 cross-env 见 cross-env

@Gala-1024
Copy link

请问如果initialItems的数据,我想通过AJAX拿下来后再渲染要怎么处理呢?

@wing-kai
Copy link

@cgygd createStore的第二个参数就是设置Store的初始值,不过要注意,这是设置整个Store树的。

http://cn.redux.js.org/docs/api/createStore.html

const Store = compose(
    applyMiddleware(thunk, logger)
)(createStore)(rootReducer, initialState);

const Root = (
    <Provider store={Store}>
        <Body />
    </Provider>
);

@chapsticks
Copy link

我想咨询楼主一个问题,这种技术能不能够应用在erp系统的开发中,对数据的展示,增删改查,数据的即时性等等

@OxygenWang
Copy link

例子如果升级支持Node 6,并且在Windows10 下也可以跑就好了。
现在npm install报:
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.0.12

@veetine
Copy link

veetine commented Jun 3, 2016

写的很棒,支持一下!!

@oldqian
Copy link

oldqian commented Jun 15, 2016

写的很好,第一遍看的时候很多不懂的地方,研究了很多天,回过头来看,觉得很精彩,很详细,已经在研究中,希望能借此可以真正做项目

@oldqian
Copy link

oldqian commented Jun 15, 2016

楼主之前说action里面写上ajax请求,可是如果我初始化的时候,就需要从服务器获取数据呢,在action里面如何去做呢?我看楼主初始化目前给的是一个固定的List,我这边想从数据库去获取值 @matthew-sun 麻烦楼主能抽空给我个答复

@matthew-sun
Copy link
Owner Author

matthew-sun commented Jun 16, 2016

@ToNiQian 可以在App初始化的时候,在componentDidMount里调用你的获取数据Action

@vzhen
Copy link

vzhen commented Jun 23, 2016

@matthew-sun 我看了很多文章, 还是搞不清redux 或者直接说flux这个东西, 能不能用一个纯react vs react-redux 的例子来做解释。
另外一个问题是,我用firebase为我的数据库。 如果使用了redux或者不使用的差别在什么地方?

@oldqian
Copy link

oldqian commented Jun 27, 2016

npm run dev 会报错
image
楼主有没遇到这样的问题呢 @matthew-sun

@wuwb
Copy link

wuwb commented Jun 28, 2016

@ToNiQian webpack command not found,安装一下

@wuwb
Copy link

wuwb commented Jun 28, 2016

@ToNiQian 例子项目已经非常老了,很多已经过时了,推荐看下对应的各个官方文档

@oldqian
Copy link

oldqian commented Jun 28, 2016

@eaielo webpack本来就已经安装了,npm run start就不会报错,有点蒙圈

@skyFi
Copy link

skyFi commented Aug 10, 2016

很棒!

@yangbin1994
Copy link

谢谢!

@Aaaaash
Copy link

Aaaaash commented Aug 18, 2016

谢谢大神!

@ca7pe
Copy link

ca7pe commented Sep 7, 2016

ERROR in Cannot find module 'node-sass'
 @ ./public/scss/pure.scss 4:14-121 13:2-17:4 14:20-127

package.json里面没有这个包的配置,需要我额外安装最后才正常运行。是否需要补一下?

@matthew-sun
Copy link
Owner Author

@ca7pe 好的,我补一下哈,3q

@yellowfrogCN
Copy link

为什么npm start后会出现这样的问题?

D:\redux-example-master>npm start

redux--example@1.0.1 start D:\redux-example-master
node server.js

events.js:141
throw er; // Unhandled 'error' event
^

Error: listen EADDRINUSE 127.0.0.1:3000
at Object.exports._errnoException (util.js:907:11)
at exports._exceptionWithHostPort (util.js:930:20)
at Server._listen2 (net.js:1250:14)
at listen (net.js:1286:10)
at net.js:1395:9
at GetAddrInfoReqWrap.asyncCallback as callback
at GetAddrInfoReqWrap.onlookup as oncomplete
npm ERR! Windows_NT 6.1.7600
npm ERR! argv "C:\Program Files\nodejs\node.exe" "C:\Program Files\nodejs
node_modules\npm\bin\npm-cli.js" "start"
npm ERR! node v4.5.0
npm ERR! npm v2.15.9
npm ERR! code ELIFECYCLE
npm ERR! redux--example@1.0.1 start: node server.js
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the redux--example@1.0.1 start script 'node server.js'.
npm ERR! This is most likely a problem with the redux--example package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR! node server.js
npm ERR! You can get information on how to open an issue for this project with:
npm ERR! npm bugs redux--example
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!
npm ERR! npm owner ls redux--example
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR! D:\redux-example-master\npm-debug.log

@wuwb
Copy link

wuwb commented Sep 17, 2016

@yellowfrogCN this example is out of date, recommend to read the redux official document

@jonashao
Copy link

看过这篇文章之后,第一次跑通了redux。

@zhw2590582
Copy link

项目运行不了

@jonashao
Copy link

@zhw2590582 根据这篇文章我创建了一个项目,可以试一下。simple-redux-example

@81777268
Copy link

你好 请问这个filter的作用是什么啊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests