react+redux实战(-)——基本流程
工程框架可以使用项目构建工具yeoman构建,使用方法可自行搜索。我们使用的是react-webpack,到使用iconfont 的时候会可能报如下错误
可以将cfg文件夹下的default.js中的loaders部分中.(png|jpg|gif|woff|woff2|eot|ttf|svg)后面加上(\?[a-z0-9=\.]+)即可以解决,下面步入正题。
React部分
这部分应该就大都是react的基础了,稍微看过react文档就可以写出简单静态页面。一个js文件中的基本结构如下:
class Person extends React.Component {
constructor (props) {
super(props);
this.state = { smiling: false };
}
handleClick(){
this.setState({smiling: !this.state.smiling});
};
componentWillMount () {
// add event listeners (Flux Store, WebSocket, document, etc.)
},
componentDidMount () {
// React.getDOMNode()
},
componentWillUnmount () {
// remove event listeners (Flux Store, WebSocket, document, etc.)
},
smilingMessage () {
return (this.state.smiling) ? "is smiling" : "";
}
render () {
return (
<div onClick={this.handleClick.bind(this)}>
{this.props.name} {this.smilingMessage.bind(this)}
</div>
);
},
}
Person.defaultProps = {
name: 'Guest'
};
Person.propTypes = {
name: React.PropTypes.string
};
export default Person;
使用react最重要的应该是考虑如何安排组件
比如想要实现一个新闻列表,首先观察其由哪些组件构成。
显而易见,列表页包含两种组件那么我们就需要创建一个容器组件,也就是ArticleList,其中包含两种展示组件GridArticle和GridKeyArticle
class ArticleList extends React.component{
render() {
const { articles, isFetching } = this.props;
const articleNodes = [];
articles.forEach(function(article, i){
if(article.genre == 1){
//将数据key和article作为属性传递到子组件中
articleNodes.push(<GridArticle key={i} article={article} />);
}else{
articleNodes.push(<GridKeyArticle key={i} article={article} />);
}
});
return (
<div className="com-article-list">
{articleNodes}
</div>
);
}
}
//子组件 GridArticle
class GridArticle extends React.Component {
render() {
//取出从父组件拿到的数据,这样方便分配和管理
const { article } = this.props;
return (
<Link to="/articles/1" className="com-grid-article clearfix" >
<div className="grid-article-left">
<h2>{article.title}</h2>
<div className="ribbon">
<span>{article.source}</span>
<span className="iconfont icon-praise">{article.praise_count}</span>
<span className="iconfont icon-message">{article.comment_count}</span>
<span className="smart-date">{Utils.smartDate(article.publish_time)}</span>
</div>
</div>
<div className="grid-article-right">
<img src={article.banner_pic} className="imgcover"/>
</div>
</Link>
);
}
}
向组件中传入数据时记得要加上key值,类似id,作为该组件的唯一标识。我们只需要将对应数据传入组件,在容器中引用组件变得再简单不过,这时候你应该开始好奇了,这些props数据从哪里来。
react应用中最重要的部分应该就是数据的流动和处理,这个我们选择了使用redux来帮助我们进行数据管理。
Redux部分
回顾基本流程:view直接触发dispatch;将action发送到reducer中后,根节点上会更新state,进而改变全局view。在整个redux流程中,action只是充当了一个类似于topic之类的角色,reducer会根据这个topic确定需要如何返回新的数据,一个reducer就对应着一个字段(理解这个是理解redux应用中数据结构的关键);数据的结构处理也从store中移到了reducer中。
说到这儿,不得不想一下这个问题:
Question:redux中的state和react中的state是什么关系?
Answer:没丁点关系。详细的可以参考这篇文章。redux中的state指的是全局状态树,简单点可以理解为一个数据库。其中的props就是数据库中的部分数据,这部分是需要作为属性传递到相应的组件中去的。而react中介绍的state就是一个在本组件中临时存储的状态变量。在使用了redux的应用中,就彻底抛弃react中的state吧,否则会污染全局状态,发生不可控的错误。
redux完整流程一般应用中都会有多个reducer,redux推荐使用combineReducers方法将不同的reducer合并成一个最终的reducer,然后对这个reducer调用creatStore。合并后的reducer可以调用各个子reducer,并把它们的结果合并成一个state对象,state中的数据结构就是各个子reducer的集合,为了后边传递数据方便,我们可以给子reducer分配个key。
import { combineReducers } from 'redux';
import articleReducer from './article.js';
import articlesReducer from './articles.js';
import commentsReducer from './comments.js';
const rootReducer = combineReducers({
//为子reducer设置了key值之后,可以在容器组件中比较轻易的绑定该部分对应的数据,下文会再次提到这点
article:articleReducer,
articles:articlesReducer,
comments:commentsReducer
});
export default rootReducer;
//所以这时我们的state树的一级子树中有article,articles,comments三个字段。
//这样我们可以在对应容器中只传入该部分数据,
//减少了传递的数据大小和数据操作换乱导致错误的风险。
createStore方法接收reducer函数和初始化的数据(currentState),并将这2个参数并保存在store中。createStore时传入的reducer方法会在store的dispatch被调用的时候被调用,接收store中的state和action,根据业务逻辑修改store中的state;
对于action和reducer部分,可以使用工具redux-actions,这样在Redux中也能使用Flux标准编写action,这样代码会简洁不少。下面是一个fetch请求的action和reducer代码:
//actions/articles.js
import { createActions } from 'redux-actions';
export const { fetchArticles } = createActions({
FETCH_ARTICLES: async (timestamp = '20160829') => {
try {
let response = await fetch(`/interfaces/articles/articlemore/${timestamp}.json`);
let articles = await response.json();
return { timestamp, articles }
} catch (err) {
console.log(err);
}
}
});
//reducer/articles.js
import { handleActions } from 'redux-actions';
import { FETCH_ARTICLES } from '../actions/index.js';
export default handleActions({
FETCH_ARTICLES: (state, action) => {
let payload = action.payload;
return Object.assign({}, state, {
[payload.timestamp]: {
isFecting: false,
articles: payload.articles
}
});
}
}, {});
PS: fetch 是未来异步的主要方式,可以看看这篇fetch文档。
除此之外,注册store,并向其中传入合并之后的rootReducer和initialState,redux的工具就已基本建立完成。当然了,在redux中使用中间件也是比较常用的功能,我们可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等,它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,更替的说是执行原本的dispatch(action)之前,类似这样middleware1(middleware2(middleware3(store.dispatch)))(action)
,所以我们的store部分就类似这样:
import { createStore, applyMiddleware } from 'redux';
//异步操作
import promiseMiddleware from 'redux-promise';
//记录日志
import createLogger from 'redux-logger';
import rootReducer from '../reducers/index.js';
const loggerMiddleware = createLogger();
const createStoreWithMiddleware = applyMiddleware(
promiseMiddleware,
loggerMiddleware
)(createStore);
export default function store(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
那么我们我们在页面中如何使用这个redux呢?
先利用react-redux提供的Provider方法注入store将我们的react应用和redux连接起来:
//在根目录下使用provider包裹即可
import React from 'react';
import { Provider } from 'react-redux';
import App from './containers/App';
import store from './stores/index';
React.render(
<div>
<Provider store={store()}>
<App />
</Provider>
</div>,
document.getElementById('app'));
然后呢?上文提到的props那些变量和方法从哪儿导入,数据从如何获取?
这个需要使用react-redux提供的connect方法将我们需要的state中的数据(还可以加上actions中的方法)绑定到props中,在需要绑定数据的容器中:
//将全局的state映射到组件的props,相当于从store获取数据,
//一种reducer就对应数据库中的一个字段,所以根据需要传入的数据引用对应的reducer
function mapStateToProps(state,ownParams){
//比如获取对应id或者时间戳等参数的文章,可以通过ownParams参数传进来,为了方便,以下例子中都使用的是变量。
const { articles } = state;
const {
isFetchting,
articles: articles
} = articles['20160829'] || {
isFetchting: true,
articles: []
};
return {
page: 1,
articles: articles,
isFetchting: isFetchting
}
}
export default connect(mapStateToProps)(ArticleList);
这多说几句:
在上面的例子代码中把articlesReducer的内容拆开了,看起来很low的样子。其实也可以把整个articles对象作为一个整体传递,在render函数中接收后再单独使用,这儿就会变得美观了。
function mapStateToProps(state){
return{
articlesList:state.articles[20160829]
//这儿最远只能取到reducer字段
}
}
但是有一点要注意:仍以上面代码为例,数据articles(改个名便于区分)叫做articlesList,其结构中包括isFetching和articles两个字段,如果我们在render函数中直接使用articlesList.articles.forEach(xxxxxx),很有可能就会报错,说这个articles不存在。明明它就是articlesList中的字段啊,怎么会不存在呢?这种基本都是因为该部分数据比如articlesList还没获取到。那就又有疑问了,我们在ComponentWillMount中已经提前dispatch(fetchArticles)了啊,注意!我们的请求都是异步的,所以在render执行的时候我们的数据还没获取到,所以为了程序的顺利进行,我们需要一点保护措施:
//如果articlesList获取到了,再使用其子数据
if(articlesList){
articlesList.articles.forEach(xxxx);
}
//所以我们的代码虽然看起来ugly一点,但是不会发生因为某个值undefined而导致程序报错的错误
我们继续主线:这样在该容器中即可通过props拿到articles等数据,这样也就可以顺利的进行向后续组件中传递数据了。然而我们这只是相当于绑定了该组件是对应数据库中的这一部分的数据,如何获取呢?在组件渲染之后(或者之前,但是这样的话需要保证获取这些数据的动作及reducer操作与dom结构无关)去dispatch(fetchArticles)
componentWillMount(){
const { dispatch } = this.props;
dispatch(fetchArticles());
}
再次总结流程
那么在ArticleList的最终代码就是:
'use strict';
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { fetchArticles } from '../../actions/index.js';
import GridArticle from '../../components/grid-article/index.js'
import GridKeyArticle from '../../components/grid-key-article/index.js'
require('./index.less');
class ArticleList extends React.Component {
constructor(props){
super(props)
}
componentWillMount(){
const { dispatch } = this.props;
dispatch(fetchArticles());
}
render() {
const { articles, isFetching } = this.props;
const articleNodes = [];
articles.forEach(function(article, i){
if(article.genre == 1){
articleNodes.push(<GridArticle key={i} article={article} />);
}else{
articleNodes.push(<GridKeyArticle key={i} article={article} />);
}
});
return (
<div className="com-article-list">
{articleNodes}
</div>
);
}
}
ArticleList.displayName = 'ArticleList';
ArticleList.propTypes = {
page: PropTypes.number,
articles: PropTypes.array,
isFetchting: PropTypes.bool,
dispatch: PropTypes.func
};
ArticleList.defaultProps = {};
// 将全局的state映射到组件的props,相当于从store获取数据
function mapStateToProps(state){
const { articlesReducer } = state;
const {
isFetchting,
articles: articles
} = articlesReducer['20160829'] || {
isFetchting: true,
articles: []
};
return {
page: 1,
articles: articles,
isFetchting: isFetchting
}
}
export default connect(mapStateToProps)(ArticleList);
思考:mapStateToProps方法与触发action关系
mapStateToProps方法是容器中配合redux提供的connect方法使用的
既然mapStateToProps是将全局的state映射到组件的props,也就是相当于这个组件从store获取了数据,那么为什么还要使用dispatch这个fetch请求数据的action呢?就觉得两者都使用就仿佛说话累赘了么?
在这个页面容器中,我们同样写了这样的代码:
componentWillMount(){
const { dispatch } = this.props;
dispatch(fetchArticles());
}
最后我的理解是:mapPropsToState方法是告诉reducer你要请求的部分数据去哪儿找,但是你得触发这个去找的动作,这个给你指明的方向才会有意义啊。所以在容器页面中,这两者不可或缺,dispatch(fetchArticles)执行,对应的reducer按着方向去获取相应的数据。
写在最后:使用webpack-dev-server,提升开发效率
众所周知,webpack提供了强大的模块打包功能,同样,热加载功能在开发阶段也是我们的得力助手,使用webpack-dev-server启动一个web服务器,监听文件修改,可通知浏览器自动刷新。网上关于webpack的使用很多,这儿我就不多介绍了,关键是我也讲不清(害羞脸)。。。还要学习啊!!!