如何用redux架构react项目

猪小花1号2018-09-13 11:21

作者:牟金涛


Facebook官方提出了FLUX思想管理数据流,同时也给出了自己的实现来管理React应用。在这之后github上也涌现了一批关于实现FLUX的框架,其中包括了redux。而和其它的FLUX相比redux更为简单,Redux只有唯一的state树,不管项目变的有多复杂,开发者只需要维护一个state树就可以了。让我们简单的了解一下redux的实现,如下:

Redux近似于flux,秉承了flux单向数据流的概念。Redux的区别在于redux将多个store变成了一个大的store集合,同时将dispatch合并到了Store中,并引入了reducer及中间件。Store在绑定actions函数之后,actions调用返回数据经由中间件交给reducer,经reducer处理后更改store中对应的数据(该数据的命名按照传入combineReducers中的对象属性名进行命名),而数据更改之后store将变化分发给所有注册监听的对象。

Redux并不一定要配合react使用,但是react作为一种view层的框架却很适合于使用redux作为架构。借助reduxreact-redux模块,我们能很容易的将reactredux结合在一起。

下面我们通过一个demo来解析说明如何将redux与react搭配使用,demo地址:一个简单的redux demo


目录结构

开发目录按照redux分为action,components,constants,page,reducers五个目录,基本对应redux架构的组成,我们抽取评论组件做解析


Constants

constants/comment.js

该文件中定义了comment的操作(因为demo相对简单仅为评论定义了评论删除与评论添加)

export const  ADD_COMMENT = 'ADD_COMMENT';
export const  DELETE_COMMENT = 'DELETE_COMMENT';

Actions

actions/comment.js

该文件中声明了评论的操作方法及评论对应的数据操作,对应于reduxactions部分

import  {ADD_COMMENT, DELETE_COMMENT } from "js/constants/comment.js"
/**options数据结构{
*	id:"评论id",
*	content:评论内容,
*	author:评论作者
*	date:评论时间
*}
**/

const getDateTime=()=>{
	let nowTime =new Date();
	Return nowTime.getFullYear()+"-"+(nowTime.getMonth()+1)+"-"+nowTime.getDate()+"   	"+nowTime.getHours()+":"+nowTime.getMinutes()
}

export const addItem = (options)=>{
	let {id,content,author}=options,date=getDateTime();
	return  dispatch => {
       setTimeout(() => dispatch({
       	type:ADD_COMMENT,
       	date,id,content,author
       }), 100)
    }
}

export const deleteItem = (id)=>{
	return {
		type:DELETE_COMMENT,
		id:id
	}
}

Actions实际上是一个数据的操作函数,返回处理后的数据及数据处理的类型。类型来着于constants,其可以看做是actionsreducer直接的类型约定。(setTimeout模拟ajax异步操作,正常actions返回一个对象,但为了实现异步引入了redux-thunk。)。

组件reducer

/reducer/components/comment.js

import  {ADD_COMMENT, DELETE_COMMENT } from "js/constants/comment.js";

const actionToItem=(action)=>{
	let {id,content,author,date}=action;
	return {
		id:id,
		content:content,
		author:author,
		date:date
	}
}

const delItem = (state,action)=>{
	const noSameId=(elem,index)=>{
		if(elem.id==action.id){
			return false;
		}else{
			return true;
		}
	}

	return state.filter(noSameId)
}

const Comment=(state=[{id:"1",content:"我是第一条评论",author:"用户1",date:"2016-8-11 17:46"}],action)=>{
	switch (action.type){
		case ADD_COMMENT:
			state.push(actionToItem(action));
			return state.slice(0);
		case DELETE_COMMENT:
			return delItem(state,action);
		default:
			return state;
	}
}

export default Comment

Reducer的作用其实可以看做是一个中间件,其对actions中的返回的数据进行处理,根据类型的不同进行不同的处理,最终改变store中该reducer对应的数据。这里有一点需要注意的是redux要求的数据存储改变是新数据覆盖旧数据而不是更改原有旧数据,所以redux必须是返回新的对象。如果是对原state进行更改返回,store并不会触发state的变更事件。


页面reducer

/reducer/page/index.js

import { combineReducers } from 'redux';
import comments from "../components/comment.js";
import user from "../components/user.js";
import article from "../components/article.js";

const rootReducer = combineReducers({
  comments,user,article
});
export default rootReducer;

该模块相当于为每一个页面生成一个对应需要的Reducer的集合。如demo中需要评论,用户和文章这些数据则引入了其相应的reducers


react-redux

至此实际上redux的基本布局就完成了,actions调用触发store的事件reducer接受更改的数据并更新到store上,combineReducers将一个页面需要的reducers绑定到一起。接下来只需要生成store并将store绑定到react组件中同时将actions绑定给redux;为此我们将使用react-redux来实现reactredux的结合。

React-redux包含了Providerconnect两个部分

 /page/index.js

import React from 'react';
import { render } from 'react-dom'
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import BaseStyle from 'less/base.less';
import MStyle from 'less/moudle.less';
import PageStyle from 'less/page/index.less';

import {createStore,applyMiddleware} from 'redux';
import rootReducer from 'js/reducers/page/index.js';

import Comment from 'js/components/comment/comment.js';
import Article from 'js/components/articleContent/articleContent.js';
import UserBar from 'js/components/userBar/UserBar.js';

//生产store
let store = createStore(rootReducer,applyMiddleware(thunk));

render( <div>
<Provider store={store}>
            <UserBar/>
        </Provider>
        <Provider store={store}>
            <Article/>
        </Provider>
        <Provider store={store}>
            <Comment/>
        </Provider>
     </div>,document.getElementById('app'));


该模块为应用的入口文件,Index中用createStore实例化store,并用provider绑定了react的组件。Provider的实现其实很简单

export default class Provider extends Component {
  getChildContext() {
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  render() {
    const { children } = this.props
    return Children.only(children)
  }
}

从其源码中我们可以看出provider是将store传递给了react组件做了封装。实际上一些早期的redux的实现并没有使用react-redux而是直接以<Comment store={store}>这种方式去实现store的传递。

react组件

/components/comment/comment.js

import img from "./commen_bg.jpg"
import style from "./comment.less";
import React from "react";
import CommentInput from "../commentInput/commentInput.js";
import CommentList from "../commentList/commentList.js";
import { connect } from 'react-redux';
import * as ComponentActions from 'js/action/comment.js';
import { bindActionCreators } from 'redux';

class Comment extends React.Component {
  constructor(props){
    super(props);
  }
  addItemToList(options){
    var {id,name}=options.author;
    this.props.actions.addItem({
      id:id,
      author:name,
      content:options.content
    });
  }
  render() {
   var actions=this.props.actions;
    return (
     <div className="comment-wrap">
   <CommentInput author={this.props.author} commentContext="" addComment={this.addItemToList.bind(this)}/>
     <CommentList comments={this.props.comments}/>
     </div>
)
  }
}
export default connect(state => ({
comments:state.comments,
  author:state.user
}), dispatch => ({
     actions: bindActionCreators(ComponentActions, dispatch)
}))(Comment);


该模块就是评论的组件部分代码,对于组件有木偶组件及智能组件的说法,而实际上就是redux组件及普通组件的区别。这里的CommentInputCommentList就是普通的组件可以用于任何的react项目,而comment则是redux组件,其通过react-reduxconnect以及redux bindActionCreators来实现componentstore的绑定,及actionsredux的绑定,这种组件只适合于redux的项目。实际上这个组件可以看做是react componentredux的适配。


react-redux原理

那么react-redux connect是如何使用及如何实现的?其接受mapStateToProps, mapDispatchToProps, mergeProps, options这三个参数

mapStateToProps是一个函数,返回值表示的是需要mergepropsstate。默认值为() => ({})

mapDispatchToProps可以是一个函数,返回值表示的是需要mergepropsactionCreators,这里的actionCreator应该是已经被包装了dispatch,即绑定actionsstore中,推荐使用reduxbindActionCreators函数。

mergeProps用于自定义merge流程。

options共有两个开关:pure代表是否打开优化,默认为truewithRef用来给包装在里面的组件一个ref,可以通过getWrappedInstance方法来获取这个ref,默认为false

connect的实现要比Provider稍微复杂一些,我从源码中摘出了几段关键的源码来简单的看一下其到底是如何实现的


// node_modules/react-redux/src/components/connect.js  
//197行
 trySubscribe() {
        if (shouldSubscribe && !this.unsubscribe) {
          this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
          this.handleChange()
        }
      }
/// 239行
andleChange() {
        if (!this.unsubscribe) {
          return
        }

        const storeState = this.store.getState()
        const prevStoreState = this.state.storeState
        if (pure && prevStoreState === storeState) {
          return
        }

        if (pure && !this.doStatePropsDependOnOwnProps) {
          const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
          if (!haveStatePropsChanged) {
            return
          }
          if (haveStatePropsChanged === errorObject) {
            this.statePropsPrecalculationError = errorObject.value
          }
          this.haveStatePropsBeenPrecalculated = true
        }

        this.hasStoreStateChanged = true
        this.setState({ storeState })
      }

通过这几段代码我们就能看出来connect的实现,是函数本身实现一个react component对传入组件进行封装,注册一个对传入store的监听,每当store触发更改的时候调用setState更改组件的store,并将需要传入组件的属性合并到其props中进行重新render。当然实际上会做的更多,比如参数中的pure,其控制是否对组件进行优化,如果优化的话就会在store state改变的时候对mapStateToProps, mapDispatchToProps中我们所依赖的部分与store中的进行比较也就是脏检测,只有在我们依赖的部分改变的时候才会去render

至此一个简单的基于reduxreact项目的架构就完成了,我们最后用一张流程图再重新梳理一遍



本文来自网易实践者社区,经作者牟金涛授权发布