一、简介
在上一篇的Redux文章中,详细介绍了Redux的概念和综合利用,开发者可以通过Redux的State管理系统管理整个应用程序的数据流,依靠功能完备的Store来分发Action,进而渲染和更新组件的UI。在我们之前的文章介绍中,根组件是保存State的组件,一般的Web开发,都是将State数据作为属性,从根组件向下传递给子组件,当子组件触发事件时,数据再通过回调函数的属性沿着组件树向上回到了根组件。这种数据在组件树中向上和向下传递的过程增加了程序的复杂性,于是乎,类似Redux的库就是为此问题而生,它通过直接从子组件分发Action来达到更新应用程序State的目的,摒弃了通过双向函数绑定实现组件树的数据传递的方式。本章的目的就是讲如何把Redux中构建的Store和UI组件整合起来,即React Redux应用。现在对比一下传统的数据流在组件树中的传递方式和采用Redux管理State的方式,流程图和代码表示大概分别如下:
图一 图二
图一对应的代码:根组件将state数据作为子组件的属性向下传递。
//根组件文件:App.js
import React, { Component } from 'react'; import NameList from './redux/component/NameList' import NameForm from "./redux/component/NameForm"; export default class App extends Component { constructor(props){ super(props); //初始化state数据 this.state = { names:[ { "name_id":"1", "name":"张三", }, { "name_id":"2", "name":"李四", } ], subjects:[ { "subject_id":"1", "subject":"数学", }, { "subject_id":"2", "subject":"语文", } ], scores:[ { "score_id":"1", "score": 90, }, { "score_id":"2", "score": 95, } ] }; //绑定事件 this.addName = this.addName.bind(this); this.deleteName = this.deleteName.bind(this); this.updateName = this.updateName.bind(this); } addName(new_name_id, name){ this.setState({ ... }); }; deleteName(name_id){ this.setState({ ... }); }; updateName(name_id, name){ this.setState({ ... }); }; render() { const {names} = this.state; const {addName,deleteName,updateName} = this; return ( <div className="app"> <NameList names={names}/> <NameForm onAddName={addName} onDeleteName={deleteName} onUpdateName={updateName}/> </div> ) } }
图二对应的代码:根组件和子组件都是通过store获取state数据。
//入口文件:index.js //------------- React Redux 显示传递Store-------------------// import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './index.css'; //通过类方法创建store import storeFactory from "./redux/store_factory"; const store = storeFactory(); const render = () => ReactDOM.render( <App store={store}/>, document.getElementById('root') ); store.subscribe(render); render(); //根组件文件:App.js import React from 'react'; import NameList from './redux/component/NameList' import NameForm from "./redux/component/NameForm"; const App = ({store}) => <div className="app"> <NameList store={store}/> <NameForm store={store}/> </div>; export default App;
二、传统方式的应用
上面图一代码举例了传统的数据流在组件树中的传递方式,从中大略地可以看到State数据在根组件和子组件间双向流动的关系。具体的实现交互,完整代码如下:
App.js
import React, { Component } from 'react'; import NameList from './redux/component/NameList' import NameForm from "./redux/component/NameForm"; //---------------------- 传统方式 -----------------------------------// export default class App extends Component { constructor(props){ super(props); //初始化state数据 this.state = { names:[ { "name_id":"1", "name":"张三", }, { "name_id":"2", "name":"李四", } ], subjects:[ { "subject_id":"1", "subject":"数学", }, { "subject_id":"2", "subject":"语文", } ], scores:[ { "score_id":"1", "score": 90, }, { "score_id":"2", "score": 95, } ] }; //绑定事件 this.addName = this.addName.bind(this); this.deleteName = this.deleteName.bind(this); this.updateName = this.updateName.bind(this); } addName(new_name_id, name){ this.setState({ names:[...this.state.names, {"name_id":new_name_id,"name":name}], subjects:this.state.subjects, scores:this.state.scores }); }; deleteName(name_id){ this.setState({ names:this.state.names.filter(item => item.name_id !== name_id), subjects:this.state.subjects, scores:this.state.scores }); }; updateName(name_id, name){ this.setState({ names:this.state.names.map(item => (item.name_id === name_id) ? ({...item, name}) : item), subjects:this.state.subjects, scores:this.state.scores }); }; render() { const {names} = this.state; const {addName,deleteName,updateName} = this; return ( <div className="app"> <NameList names={names}/> <NameForm onAddName={addName} onDeleteName={deleteName} onUpdateName={updateName}/> </div> ) } }
NameList.js
import React, { Component } from 'react'; export default class NameList extends Component{ render(){ const {names} = this.props; return ( <div> <table border="1" cellPadding="5" cellSpacing="0" bgcolor="F2F2F2" width="50%"> <tbody> <tr id="infoTr"> <td>ID</td> <td>姓名</td> </tr> </tbody> { names.map((item,key) => ( <tbody key={key}> <tr id="infoTr"> <td>{item.name_id}</td> <td>{item.name}</td> </tr> </tbody> )) } </table> </div> ); } }
NameForm.js
import React, { Component} from 'react'; import PropTypes from 'react-dom' export default class NameForm extends Component{ static propsTypes = { onAddName: PropTypes.func, onDeleteName: PropTypes.func, onUpdateName: PropTypes.func }; addName(){ this.props.onAddName("3","王五"); }; deleteName(){ this.props.onDeleteName("1"); }; updateName(){ this.props.onUpdateName("2","赵六"); }; render(){ const divStyle = {marginTop:10}; const spanStyle = {padding:10,margin:10}; return ( <div style={divStyle}> <span style={spanStyle}><button onClick={this.addName.bind(this)}>添加</button></span> <span style={spanStyle}><button onClick={this.deleteName.bind(this)}>删除</button></span> <span style={spanStyle}><button onClick={this.updateName.bind(this)}>更新</button></span> </div> ); } }
演示结果
三、React Redux的应用
1、显式传递Store
在React Redux中,将Store集成到UI中最合乎逻辑的做法是显式地将它作为属性在组件树中向下传递。上面图二代码中传递Store的方式就是显式传递的。这种方法很简单,对于只包含少量嵌套组件的小型应用程序效果非常好。如代码所示,APP组件作为根组件,它通过属性获取了Store,然后显式地向下传递给了子组件NameList组件和NameForm组件。此时,所有子组件可以使用store.getState从store中获取State数据了,并且可以使用store.dispatch将Action分发到Store。在子组件中可以引入之前创建的Action生成器。具体代码如下所示:
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './index.css'; //通过类方法创建store import storeFactory from "./redux/store_factory"; const store = storeFactory(); const render = () => ReactDOM.render( <App store={store}/>, document.getElementById('root') ); store.subscribe(render); render();
App.js
import React, { Component } from 'react'; import NameList from './redux/component/NameList' import NameForm from "./redux/component/NameForm"; const App = ( {store}) => <div className="app"> <NameList store={store}/> <NameForm store={store}/> </div>; export default App;
NameList.js
import React, { Component } from 'react'; const NameList = ({store}) => { //通过store获取state数据 const {names} = store.getState(); return ( <div> <table border="1" cellPadding="5" cellSpacing="0" bgcolor="F2F2F2" width="50%"> <tbody> <tr id="infoTr"> <td>ID</td> <td>姓名</td> </tr> </tbody> { names.map((item,key) => ( <tbody key={key}> <tr id="infoTr"> <td>{item.name_id}</td> <td>{item.name}</td> </tr> </tbody> )) } </table> </div> ) }; export default NameList;
NameForm.js
import React, { Component} from 'react'; //导入前篇文章中写的action生成器 import {addName,deleteName,updateName} from "../action_builder"; const NameForm = ({store}) => { const add_name = e => { e.preventDefault(); store.dispatch(addName("3","王五")) }; const delete_name = e => { e.preventDefault(); store.dispatch(deleteName("2")) }; const update_name = e => { e.preventDefault(); store.dispatch(updateName("1","赵六")) }; const divStyle = {marginTop:10}; const spanStyle = {padding:10,margin:10}; return ( <div style={divStyle}> <span style={spanStyle}><button onClick={add_name}>添加</button></span> <span style={spanStyle}><button onClick={delete_name}>删除</button></span> <span style={spanStyle}><button onClick={update_name}>更新</button></span> </div> ); }; export default NameForm;
演示结果:
2、通过上下文传递Store
在组件间除了可以显式地传递Store外,还有其他获取Store的方式,例如通过上下文的方式传递Store。它允许在组件树中不显式地向下传递属性的情况下将变量传递给组件。任意子组件都可以访问这些上下文变量。一般的操作是index文件保持基本的渲染入口,同时传递store时去掉订阅,然后需要在根组件App中保存上下文,并监听Store以便及时刷新UI。注意,需要在组件中引入类型验证来确定上下文类型,否则组件无法从上下文中获取Store,若在使用'react'中的PropTypes时出现depredcated,说明这种导入方式已经过期,推荐的正确安装如下:
//1.安装PropTypes npm install -S prop-types //2.导入PropTypes import PropTypes from 'prop-types'; //而不是这样做: import { PropTypes } from 'react';
在根组件中完成了相关的上下文设置后,在内嵌子组件时就不用给子组件传递props或者store了,子组件完全自己可以从上下文中获取store。具体完整的代码示例如下所示:
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './index.css'; import storeFactory from "./redux/store_factory"; //通过类方法创建store const store = storeFactory(); ReactDOM.render( <App store={store}/>, document.getElementById('root') );
App.js
import React, { Component } from 'react'; import NameList from './redux/component/NameList' import NameForm from "./redux/component/NameForm"; import PropTypes from 'prop-types'; export default class App extends Component{ //从上下文获取store getChildContext(){ return { store: this.props.store } } //添加订阅任务 componentWillMount() { this.unsubscribe = this.props.store.subscribe( () => this.forceUpdate() ) } //退订监听者 componentWillUnmount() { this.unsubscribe(); } //内嵌子组件的数据不用再由根组件向下传递 render() { return ( <div className="app"> <NameList /> <NameForm /> </div> ) } } //属性类型验证 App.propTypes = { store: PropTypes.object.isRequired }; //上下文类型验证 App.childContextTypes = { store: PropTypes.object.isRequired }; //注意类型验证必不可少
NameList.js
import React, { Component } from 'react'; import PropTypes from "prop-types"; //上下文会排在props后面,作为第二个参数传递给无状态函数式组件。 //我们可以通过对象解构直接从参数中的对象获取Store。为了使用Store,我们可以在NameList示例上定义contextTypes 。 //这是为了告知React该组件将会使用的上下文变量类型。这是一个必需的步骤。如果没有它,用于将无法从上下文中读取Store。 const NameList = (props, {store}) => { //通过store获取state数据 const {names} = store.getState(); return ( <div> <table border="1" cellPadding="5" cellSpacing="0" bgcolor="F2F2F2" width="50%"> <tbody> <tr id="infoTr"> <td>ID</td> <td>姓名</td> </tr> </tbody> { names.map((item,key) => ( <tbody key={key}> <tr id="infoTr"> <td>{item.name_id}</td> <td>{item.name}</td> </tr> </tbody> )) } </table> </div> ) }; //上下文中store类型验证 NameList.contextTypes = { store: PropTypes.object }; export default NameList;
NameForm.js
import React, { Component} from 'react'; import {addName,deleteName,updateName} from "../action_builder"; import PropTypes from "prop-types"; //同样地,NameForm组件也必须定义contextTypes。然后才能从上下文中获取store。 export default class NameForm extends Component{ constructor(props){ super(props); this.addName = this.addName.bind(this); this.deleteName = this.deleteName.bind(this); this.updateName = this.updateName.bind(this); } addName(){ const {store} = this.context; //从上下文中获取store store.dispatch(addName("3","陈七")) }; deleteName(){ const {store} = this.context; //从上下文中获取store store.dispatch(deleteName("2")) }; updateName(){ const {store} = this.context; //从上下文中获取store store.dispatch(updateName("3","李八")) }; render(){ const divStyle = {marginTop:10}; const spanStyle = {padding:10,margin:10}; return ( <div style={divStyle}> <span style={spanStyle}><button onClick={this.addName}>添加</button></span> <span style={spanStyle}><button onClick={this.deleteName}>删除</button></span> <span style={spanStyle}><button onClick={this.updateName}>更新</button></span> </div> ); } } //上下文中store类型验证 NameForm.contextTypes = { store: PropTypes.object };
演示结果:
3、表现层和容器组件
在上面的示例中,我们既采用了显式地的方式传递Store,还采用了上下文的方式传递Store,然后通过Store来获取State数据。这些组件都是直接和Redux的Store交互来渲染UI元素的。其实,开发者还可以将Store和渲染UI组件的关系进行脱离,进一步来优化应用程序的架构。在上面示例中的NameList、NameForm都是表现层组件,它们是直接负责渲染UI元素的组件,并没有和任何数据结构紧密地耦合在一起。它们只是通过属性接收数据,并且通过回调函数将数据回传到父组件。可以说,它们就是纯粹地聚焦于UI,可以达到不同数据亦可复用的目的。在新型架构中,表现层作为其中的一部分,同时还有一个部分就是容器组件。容器组件就是将表现层和数据关联起来的组件,容器组件将通过上下文访问Store,负责通过Store管理交互。容器组件通过映射属性到State和将回调函数属性传递给Store的dispatch方法,以此完成渲染表现层组件的工作。也就是说,容器组件聚焦于数据,并将表现层与其关联。这种架构的优势是明显的,表现层组件和容器组件都可复用,易替换,易测试。现在我们在同一个文件中给NameList和NameForm定义容器组件,当然App组件大致会保持原样,仍然在上下文对象中定义Store,以便子组件可以访问它,它唯一变动的地方就是嵌入的子组件从表现层组件修改为容器组件。如下所示:
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './index.css'; import storeFactory from "./redux/store_factory"; //通过类方法创建store const store = storeFactory(); ReactDOM.render( <App store={store}/>, document.getElementById('root') );
App.js
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { NameListProvider, NameFormProvider } from './redux/component/NameProvider' //此时App的子组件不再是NameList和NameForm表现层组件 //而是都是容器组件NameListProvider、NameFormProvider export default class App extends Component{ //从上下文获取store getChildContext(){ return { store: this.props.store } } //添加订阅任务 componentWillMount() { this.unsubscribe = this.props.store.subscribe( () => this.forceUpdate() ) } //退订监听者 componentWillUnmount() { this.unsubscribe(); } render() { return ( <div className="app"> <NameListProvider /> <NameFormProvider /> </div> ) } } //属性中store类型验证 App.propTypes = { store: PropTypes.object.isRequired }; //上下文中store类型验证 App.childContextTypes = { store: PropTypes.object.isRequired };
NameList.js
import React, { Component } from 'react'; export default class NameList extends Component{ render(){ const {names} = this.props; return ( <div> <table border="1" cellPadding="5" cellSpacing="0" bgcolor="F2F2F2" width="50%"> <tbody> <tr id="infoTr"> <td>ID</td> <td>姓名</td> </tr> </tbody> { names.map((item,key) => ( <tbody key={key}> <tr id="infoTr"> <td>{item.name_id}</td> <td>{item.name}</td> </tr> </tbody> )) } </table> </div> ); } }
NameForm.js
import React, { Component} from 'react'; import PropTypes from "prop-types"; //按钮事件中,直接调用容器组件传递过来的属性函数 //这些属性函数会在Provider组件中调用dispatch函数分发action export default class NameForm extends Component{ static propsTypes = { onAddName: PropTypes.func, onDeleteName: PropTypes.func, onUpdateName: PropTypes.func }; render(){ const divStyle = {marginTop:10}; const spanStyle = {padding:10,margin:10}; const {onAddName,onDeleteName,onUpdateName} = this.props; return ( <div style={divStyle}> <span style={spanStyle}><button onClick={() => onAddName()}>添加</button></span> <span style={spanStyle}><button onClick={() => onDeleteName()}>删除</button></span> <span style={spanStyle}><button onClick={() => onUpdateName()}>更新</button></span> </div> ); } }
NameProvider.js
import NameList from './NameList' import NameForm from "./NameForm"; import PropTypes from "prop-types"; import React, { Component } from 'react'; import {addName,deleteName,updateName} from "../action_builder"; //姓名列表容器组件 export const NameListProvider = (props, {store}) => { const {names} = store.getState(); return <NameList names={names}/> }; NameListProvider.contextTypes = { store: PropTypes.object }; //姓名操作容器列表 //在回调函数中调用dispatch函数分发action export const NameFormProvider = (props, {store}) => { return ( <NameForm onAddName={ () => store.dispatch(addName("4","猪八戒")) } onDeleteName={ () => store.dispatch(deleteName("1")) } onUpdateName={ ()=> store.dispatch(updateName("2","孙悟空")) } /> ) }; NameFormProvider.contextTypes = { store: PropTypes.object };
演示结果:
4、React Redux的Provider容器组件和connect函数
通过将UI组件和容器分离的方式将它们与数据连接是一个不错的优化方案,但是,这对于小型的项目、概念验证性或者原型项目来说是大材小用了。React Redux是一个脚本库,它包含的一些工具可以显著降低显式通过上下文传递Store的复杂性。React Redux为开发者提供了一个默认的Provider容器组件,开发者通过它在上下文中配置属于自己的Store,它可以包装任意的React元素,该元素的所有子元素都将能够通过上下文访问Store,该组件去掉了类型验证这一操作,实现更简单了。也即是说在Provider中配置一次Store后,它会把Store作为属性进行传递,它的子组件皆可访问。 React Redux还为开发者提供了另一种配合Provider快速创建容器组件的方式,即connect函数。connect函数是一个高阶函数,它既可以通过将Redux的Store中当前State映射为表现层组件的属性来创建容器组件,还可以将Store的dispatch函数映射成回调函数。具体的代码如下所示:
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './index.css'; import storeFactory from "./redux/store_factory"; import {Provider} from 'react-redux' //通过类方法创建store //直接把Store作为容器组件Provider的属性,向下传递 //App以及它的所有组件均可访问 const store = storeFactory(); ReactDOM.render( <Provider store={store}> <App/> </Provider>, document.getElementById('root') );
App.js
import React, { Component } from 'react'; import { NameListProvider, NameFormProvider } from './redux/component/NameProvider' //去掉了类型验证这步操作 const App = () => <div className="app"> <NameListProvider /> <NameFormProvider /> </div>; export default App;
NameList.js
import React, { Component } from 'react'; export default class NameList extends Component{ render(){ const {names} = this.props; return ( <div> <table border="1" cellPadding="5" cellSpacing="0" bgcolor="F2F2F2" width="50%"> <tbody> <tr id="infoTr"> <td>ID</td> <td>姓名</td> </tr> </tbody> { names.map((item,key) => ( <tbody key={key}> <tr id="infoTr"> <td>{item.name_id}</td> <td>{item.name}</td> </tr> </tbody> )) } </table> </div> ); } }
NameForm.js
import React, { Component} from 'react'; import PropTypes from "prop-types"; export default class NameForm extends Component{ static propsTypes = { onAddName: PropTypes.func, onDeleteName: PropTypes.func, onUpdateName: PropTypes.func }; render(){ const divStyle = {marginTop:10}; const spanStyle = {padding:10,margin:10}; const {onAddName,onDeleteName,onUpdateName} = this.props; return ( <div style={divStyle}> <span style={spanStyle}><button onClick={() => onAddName()}>添加</button></span> <span style={spanStyle}><button onClick={() => onDeleteName()}>删除</button></span> <span style={spanStyle}><button onClick={() => onUpdateName()}>更新</button></span> </div> ); } }
NameProvider.js
import NameList from './NameList' import NameForm from "./NameForm"; import React, { Component } from 'react'; import {addName,deleteName,updateName} from "../action_builder"; import {connect} from "react-redux"; // connect函数: function connect(mapStateToProps, mapDispatchToProps, mergeProps, _ref2){...} // connect函数是一个高阶函数,它的返回值是一个表现层组件,该组件被一个容器组件包装,可以通过属性发送数据。 // connect函数接收函数作为参数,第1个参数是传递State的函数,第2个参数是dispatch函数, 第三个是合并属性的函数。根据需要,传入参数。 // connect函数与Provider一起协同工作。Provider将Store添加到上下文对象中,connect函数创建组件访问Store,开发者无需过多关心上下文对象。 //姓名列表容器组件 export const NameListProvider = connect( state => ({ names:[...state.names] }), null )(NameList); //姓名操作容器列表 export const NameFormProvider = connect( null, dispatch => ({ onAddName(){ dispatch(addName("4","XXX")); }, onDeleteName(){ dispatch(deleteName("1")); }, onUpdateName(){ dispatch(updateName("2","YYY")); } }), )(NameForm);
演示结果
四、总结
在本文中,介绍了将Redux连接到React的多种方法。既有显式地将Store当做属性沿着组件树向下传递给了子组件,也有通过上下文对象将Store直接传递给了需要访问它的组件。既有通过容器组件将Store的功能从表现层组件中剥离出来,也有通过react-redux库的上下文对象和容器组件Provider帮助开发者快速地将Store和表现层连接起来。不论哪一种方式,都为Web应用程序的开发提供了极大的方便,也为架构的优化提供了优秀的方案。