原文地址html
本文从属于笔者的React入门与最佳实践系列,推荐阅读GUI应用程序架构的十年变迁:MVC,MVP,MVVM,Unidirectional,Clean前端
React组件一个很大的特性在于其拥有本身完整的生命周期,所以咱们能够将React组件视做可自运行的小型系统,它拥有本身的内部状态、输入与输出。react
对于React组件而言,其输入的来源就是Props,咱们会用以下方式向某个React组件传入数据:git
// Title.jsx class Title extends React.Component { render() { return <h1>{ this.props.text }</h1>; } }; Title.propTypes = { text: React.PropTypes.string }; Title.defaultProps = { text: 'Hello world' }; // App.jsx class App extends React.Component { render() { return <Title text='Hello React' />; } };
text
是Text
组件本身的输入域,父组件App
在使用子组件Title
时候应该提供text
属性值。除了标准的属性名以外,咱们还会用到以下两个设置:github
propTypes:用于定义Props的类型,这有助于追踪运行时误设置的Prop值。web
defaultProps:定义Props的默认值,这个在开发时颇有帮助segmentfault
Props中还有一个特殊的属性props.children
能够容许咱们使用子组件:后端
class Title extends React.Component { render() { return ( <h1> { this.props.text } { this.props.children } </h1> ); } }; class App extends React.Component { render() { return ( <Title text='Hello React'> <span>community</span> </Title> ); } };
注意,若是咱们不主动在Title
组件的render
函数中设置{this.props.children}
,那么span
标签是不会被渲染出来的。除了Props以外,另外一个隐性的组件的输入便是context
,整个React组件树会拥有一个context
对象,它能够被树中挂载的每一个组件所访问到,关于此部分更多的内容请参考依赖注入这一章节。架构
组件最明显的输出就是渲染后的HTML文本,便是React组件渲染结果的可视化展现。固然,部分包含了逻辑的组件也可能发送或者触发某些Action或者Event。app
class Title extends React.Component { render() { return ( <h1> <a onClick={ this.props.logoClicked }> <img src='path/to/logo.png' /> </a> </h1> ); } }; class App extends React.Component { render() { return <Title logoClicked={ this.logoClicked } />; } logoClicked() { console.log('logo clicked'); } };
在App
组件中咱们向Title
组件传入了能够从Title
调用的回调函数,在logoClicked
函数中咱们能够设置或者修改须要传回父组件的数据。须要注意的是,React并无提供能够访问子组件状态的API,换言之,咱们不能使用this.props.children[0].state
或者相似的方法。正确的从子组件中获取数据的方法应该是在Props中传入回调函数,而这种隔离也有助于咱们定义更加清晰的API而且促进了所谓单向数据流。
React最大的特性之一便是其强大的组件的可组合性,实际上除了React以外,笔者并不知道还有哪一个框架可以提供如此简单易用的方式来建立与组合各式各样的组件。本章咱们会一块儿讨论些经常使用的组合技巧,咱们以一个简单的例子来进行讲解。假设在咱们的应用中有一个页首栏目,而且其中放置了导航栏。咱们建立了三个独立的React组件:App
,Header
以及Navigation
。将这三个组件依次嵌套组合,能够获得如下的代码:
<App> <Header> <Navigation> ... </Navigation> </Header> </App>
而在JSX中组合这些组件的方式就是在须要的时候引用它们:
// app.jsx import Header from './Header.jsx'; export default class App extends React.Component { render() { return <Header />; } } // Header.jsx import Navigation from './Navigation.jsx'; export default class Header extends React.Component { render() { return <header><Navigation /></header>; } } // Navigation.jsx export default class Navigation extends React.Component { render() { return (<nav> ... </nav>); } }
不过这种方式却可能存在如下的问题:
咱们将App
当作各个组件间的链接线,也是整个应用的入口,所以在App
中进行各个独立组件的组合是个不错的方法。不过Header
元素中可能包含像图标、搜索栏或者Slogan这样的元素。而若是咱们须要另外一个不包含Navigation
功能的Header
组件时,像上面这种直接将Navigation
组件硬编码进入Header
的方式就会难于修改。
这种硬编码的方式会难以测试,若是咱们在Header
中加入一些自定义的业务逻辑代码,那么在测试的时候当咱们要建立Header
实例时,由于其依赖于其余组件而致使了这种依赖层次过深(这里不包含Shallow Rendering这种仅渲染父组件而不渲染嵌套的子组件方式)。
children
APIReact为咱们提供了this.props.children
来容许父组件访问其子组件,这种方式有助于保证咱们的Header
独立而且不须要与其余组件解耦合。
// App.jsx export default class App extends React.Component { render() { return ( <Header> <Navigation /> </Header> ); } } // Header.jsx export default class Header extends React.Component { render() { return <header>{ this.props.children }</header>; } };
这种方式也有助于测试,咱们能够选择输入空白的div
元素,从而将要测试的目标元素隔离开来而专一于咱们须要测试的部分。
React组件能够接受Props做为输入,咱们也能够选择将须要封装的组件以Props方式传入:
// App.jsx class App extends React.Component { render() { var title = <h1>Hello there!</h1>; return ( <Header title={ title }> <Navigation /> </Header> ); } }; // Header.jsx export default class Header extends React.Component { render() { return ( <header> { this.props.title } <hr /> { this.props.children } </header> ); } };
这种方式在咱们须要对传入的待组合组件进行一些修正时很是适用。
Higher-Order Components模式看上去很是相似于装饰器模式,它会用于包裹某个组件而后为其添加一些新的功能。这里展现一个简单的用于构造Higher-Order Component的函数:
var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} /> ) } }; export default enhanceComponent;
一般状况下咱们会构建一个工厂函数,接收原始的组件而后返回一个所谓的加强或者包裹后的版本,譬如:
var OriginalComponent = () => <p>Hello world.</p>; class App extends React.Component { render() { return React.createElement(enhanceComponent(OriginalComponent)); } };
通常来讲,高阶组件的首要工做就是渲染原始的组件,咱们常常也会将Props与State传递进去,将这两个属性传递进去会有助于咱们创建一个数据代理。HOC模式容许咱们控制组件的输入,即将须要传入的数据以Props传递进去。譬如咱们须要为原始组件添加一些配置:
var config = require('path/to/configuration'); var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ config.appTitle } /> ) } };
这里对于configuration
的细节实现会被隐藏到高阶组件中,原始组件只须要了解从Props中获取到title
变量而后渲染到界面上。原始组件并不会关心变量存于何地,从何而来,这种模式最大的优点在于咱们可以以独立的模式对该组件进行测试,而且能够很是方便地对该组件进行Mocking。在HOC模式下咱们的原始组件会变成这样子:
var OriginalComponent = (props) => <p>{ props.title }</p>;
咱们写的大部分组件与模块都会包含一些依赖,合适的依赖管理有助于建立良好可维护的项目结构。而所谓的依赖注入技术正是解决这个问题的经常使用技巧,不管是在Java仍是其余应用程序中,依赖注入都受到了普遍的使用。而React中对于依赖注入的须要也是显而易见的,让咱们假设有以下的应用树结构:
// Title.jsx export default function Title(props) { return <h1>{ props.title }</h1>; } // Header.jsx import Title from './Title.jsx'; export default function Header() { return ( <header> <Title /> </header> ); } // App.jsx import Header from './Header.jsx'; class App extends React.Component { constructor(props) { super(props); this.state = { title: 'React in patterns' }; } render() { return <Header />; } };
title
这个变量的值是在App
组件中被定义好的,咱们须要将其传入到Title
组件中。最直接的方法就是将其从App
组件传入到Header
组件,而后再由Header
组件传入到Title
组件中。这种方法在这里描述的简单的仅有三个组件的应用中仍是很是清晰可维护的,不过随着项目功能与复杂度的增长,这种层次化的传值方式会致使不少的组件要去考虑它们并不须要的属性。在上文所讲的HOC模式中咱们已经使用了数据注入的方式,这里咱们使用一样的技术来注入title
变量:
// enhance.jsx var title = 'React in patterns'; var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ title } /> ) } }; export default enhanceComponent; // Header.jsx import enhance from './enhance.jsx'; import Title from './Title.jsx'; var EnhancedTitle = enhance(Title); export default function Header() { return ( <header> <EnhancedTitle /> </header> ); }
在上文这种HOC模式中,title
变量被包含在了一个隐藏的中间层中,咱们将其做为Props值传入到原始的Title
变量中而且获得一个新的组件。这种方式思想是不错,不过仍是只解决了部分问题。如今咱们能够不去显式地将title
变量传递到Title
组件中便可以达到一样的enhance.jsx
效果。
React为咱们提供了context
的概念,context
是贯穿于整个React组件树容许每一个组件访问的对象。有点像所谓的Event Bus,一个简单的例子以下所示:
// a place where we'll define the context var context = { title: 'React in patterns' }; class App extends React.Component { getChildContext() { return context; } ... }; App.childContextTypes = { title: React.PropTypes.string }; // a place where we need data class Inject extends React.Component { render() { var title = this.context.title; ... } } Inject.contextTypes = { title: React.PropTypes.string };
注意,咱们要使用context对象必需要经过childContextTypes
与contextTypes
指明其构成。若是在context
对象中未指明这些那么context
会被设置为空,这可能会添加些额外的代码。所以咱们最好不要将context
当作一个简单的object对象而为其设置一些封装方法:
// dependencies.js export default { data: {}, get(key) { return this.data[key]; }, register(key, value) { this.data[key] = value; } }
这样,咱们的App
组件会被改形成这样子:
import dependencies from './dependencies'; dependencies.register('title', 'React in patterns'); class App extends React.Component { getChildContext() { return dependencies; } render() { return <Header />; } }; App.childContextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func };
而在Title
组件中,咱们须要进行以下设置:
// Title.jsx export default class Title extends React.Component { render() { return <h1>{ this.context.get('title') }</h1> } } Title.contextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func };
固然咱们不但愿在每次要使用contextTypes
的时候都须要显式地声明一下,咱们能够将这些声明细节包含在一个高阶组件中。
// Title.jsx import wire from './wire'; function Title(props) { return <h1>{ props.title }</h1>; } export default wire(Title, ['title'], function resolve(title) { return { title }; });
这里的wire
函数的第一个参数是React组件对象,第二个参数是一系列须要注入的依赖值,注意,这些依赖值务必已经调用过register
函数。最后一个参数则是所谓的映射函数,它接收存储在context
中的某个原始值而后返回React Props中须要的值。由于在这个例子里context
中存储的值与Title
组件中须要的值都是title
变量,所以咱们直接返回便可。不过在真实的应用中多是一个数据集合、配置等等。
export default function wire(Component, dependencies, mapper) { class Inject extends React.Component { render() { var resolved = dependencies.map(this.context.get.bind(this.context)); var props = mapper(...resolved); return React.createElement(Component, props); } } Inject.contextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func }; return Inject; };
这里的Inject就是某个能够访问context
的高阶组件,而mapper
就是用于接收context
中的数据并将其转化为组件所须要的Props的函数。实际上如今大部分的依赖注入的解决方案都是基于context
,我以为了解这种方式的底层原理仍是颇有意义的。譬如如今流行的Redux
,其核心的connect
函数与Provider
组件都是基于context
。
单向数据流是React中主要的数据驱动模式,其核心概念在于组件并不会修改它们接收到的数据,它们只是负责接收新的数据然后从新渲染到界面上或者发出某些Action以触发某些专门的业务代码来修改数据存储中的数据。咱们先设置一个包含一个按钮的Switcher
组件,当咱们点击该按钮时会触发某个flag
变量的改变:
class Switcher extends React.Component { constructor(props) { super(props); this.state = { flag: false }; this._onButtonClick = e => this.setState({ flag: !this.state.flag }); } render() { return ( <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; // ... and we render it class App extends React.Component { render() { return <Switcher />; } };
此时咱们将全部的数据放置到组件内,换言之,Switcher
是惟一的包含咱们flag
变量的地方,咱们来尝试下将这些数据托管于专门的Store中:
var Store = { _flag: false, set: function(value) { this._flag = value; }, get: function() { return this._flag; } }; class Switcher extends React.Component { constructor(props) { super(props); this.state = { flag: false }; this._onButtonClick = e => { this.setState({ flag: !this.state.flag }, () => { this.props.onChange(this.state.flag); }); } } render() { return ( <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; class App extends React.Component { render() { return <Switcher onChange={ Store.set.bind(Store) } />; } };
这里的Store
对象是一个简单的单例对象,能够帮助咱们设置与获取_flag
属性值。而经过将getter
函数传递到组件内,能够容许咱们在Store
外部修改这些变量,此时咱们的应用工做流大概是这样的:
User's input | Switcher -------> Store
假设咱们已经将flag
值保存到某个后端服务中,咱们须要为该组件设置一个合适的初始状态。此时就会存在一个问题在于同一份数据保存在了两个地方,对于UI与Store
分别保存了各自独立的关于flag
的数据状态,咱们等于在Store
与Switcher
之间创建了双向的数据流:Store ---> Switcher
与Switcher ---> Store
// ... in App component <Switcher value={ Store.get() } onChange={ Store.set.bind(Store) } /> // ... in Switcher component constructor(props) { super(props); this.state = { flag: this.props.value }; ...
此时咱们的数据流向变成了:
User's input | Switcher <-------> Store ^ | | | | | | v Service communicating with our backend
在这种双向数据流下,若是咱们在外部改变了Store
中的状态以后,咱们须要将改变以后的最新值更新到Switcher
中,这样也在无形之间增长了应用的复杂度。而单向数据流则是解决了这个问题,它强制在全局只保留一个状态存储,一般是存放在Store中。在单向数据流下,咱们须要添加一些订阅Store中状态改变的响应函数:
var Store = { _handlers: [], _flag: '', onChange: function(handler) { this._handlers.push(handler); }, set: function(value) { this._flag = value; this._handlers.forEach(handler => handler()) }, get: function() { return this._flag; } };
而后咱们在App
组件中设置了钩子函数,这样每次Store
改变其值的时候咱们都会强制从新渲染:
class App extends React.Component { constructor(props) { super(props); Store.onChange(this.forceUpdate.bind(this)); } render() { return ( <div> <Switcher value={ Store.get() } onChange={ Store.set.bind(Store) } /> </div> ); } };
注意,这里使用的forceUpdate
并非一个推荐的用法,咱们一般会使用HOC模式来进行重渲染,这里使用forceUpdate
只是用于演示说明。在基于上述的改造,咱们就不须要在组件中继续保留内部状态:
class Switcher extends React.Component { constructor(props) { super(props); this._onButtonClick = e => { this.props.onChange(!this.props.value); } } render() { return ( <button onClick={ this._onButtonClick }> { this.props.value ? 'lights on' : 'lights off' } </button> ); } };
这种模式的优点在于会将咱们的组件改造为简单的Store
中数据的呈现,此时才是真正无状态的View。咱们能够以彻底声明式的方式来编写组件,而将应用中复杂的业务逻辑放置到单独的地方。此时咱们应用程序的流图变成了:
Service communicating with our backend ^ | v Store <----- | | v | Switcher ----> ^ | | User input
在这种单向数据流中咱们再也不须要同步系统中的多个部分,这种单向数据流的概念并不只仅适用于基于React的应用。
关于Flux的简单了解能够参考笔者的GUI应用程序架构的十年变迁:MVC,MVP,MVVM,Unidirectional,Clean
Flux是用于构建用户交互界面的架构模式,最先由Facebook在f8大会上提出,自此以后,不少的公司开始尝试这种概念而且貌似这是个很不错的构建前端应用的模式。Flux常常和React一块儿搭配使用,笔者自己在平常的工做中也是使用React+Flux的搭配,给本身带来了很大的遍历。
Flux中最主要的角色为Dispatcher,它是整个系统中全部的Events的中转站。Dispatcher负责接收咱们称之为Actions的消息通知而且将其转发给全部的Stores。每一个Store实例自己来决定是否对该Action感兴趣而且是否相应地改变其内部的状态。当咱们将Flux与熟知的MVC相比较,你就会发现Store在某些意义上很相似于Model,两者都是用于存放状态与状态中的改变。而在系统中,除了View层的用户交互可能触发Actions以外,其余的相似于Service层也可能触发Actions,譬如在某个HTTP请求完成以后,请求模块也会发出相应类型的Action来触发Store中对于状态的变动。
而在Flux中有个最大的陷阱就是对于数据流的破坏,咱们能够在Views中访问Store中的数据,可是咱们不该该在Views中修改任何Store的内部状态,全部对于状态的修改都应该经过Actions进行。做者在这里介绍了其维护的某个Flux变种的项目fluxiny。
大部分状况下咱们在系统中只须要单个的Dispatcher,它是相似于粘合剂的角色将系统的其余部分有机结合在一块儿。Dispatcher通常而言有两个输入:Actions与Stores。其中Actions须要被直接转发给Stores,所以咱们并不须要记录Actions的对象,而Stores的引用则须要保存在Dispatcher中。基于这个考虑,咱们能够编写一个简单的Dispatcher:
var Dispatcher = function () { return { _stores: [], register: function (store) { this._stores.push({ store: store }); }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action); }); } } } };
在上述实现中咱们会发现,每一个传入的Store
对象都应该拥有一个update
方法,所以咱们在进行Store的注册时也要来检测该方法是否存在:
register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { this._stores.push({ store: store }); } }
在完成了对于Store的注册以后,下一步咱们就是须要将View与Store关联起来,从而在Store发生改变的时候可以触发View的重渲染:
不少flux的实现中都会使用以下的辅助函数:
Framework.attachToStore(view, store);
不过做者并非很喜欢这种方式,这样这样会要求View中须要调用某个具体的API,换言之,在View中就须要了解到Store的实现细节,而使得View与Store又陷入了紧耦合的境地。当开发者打算切换到其余的Flux框架时就不得不修改每一个View中的相对应的API,那又会增长项目的复杂度。另外一种可选的方式就是使用React mixins
:
var View = React.createClass({ mixins: [Framework.attachToStore(store)] ... });
使用mixin
是个不错的修改现有的React 组件而不影响其原有代码的方式,不过这种方式的缺陷在于它不可以以一种Predictable的方式去修改组件,用户的可控性较低。还有一种方式就是使用React context
,这种方式容许咱们将值跨层次地传递给React组件树中的组件而不须要了解它们处于组件树中的哪一个层级。这种方式和mixins可能有相同的问题,开发者并不知道该数据从何而来。
做者最终选用的方式便是上面说起到的Higher-Order Components模式,它创建了一个包裹函数来对现有组件进行从新打包处理:
function attachToStore(Component, store, consumer) { const Wrapper = React.createClass({ getInitialState() { return consumer(this.props, store); }, componentDidMount() { store.onChangeEvent(this._handleStoreChange); }, componentWillUnmount() { store.offChangeEvent(this._handleStoreChange); }, _handleStoreChange() { if (this.isMounted()) { this.setState(consumer(this.props, store)); } }, render() { return <Component {...this.props} {...this.state} />; } }); return Wrapper; };
其中Component
代指咱们须要附着到Store
中的View,而consumer
则是应该被传递给View的Store中的部分的状态,简单的用法为:
class MyView extends React.Component { ... } ProfilePage = connectToStores(MyView, store, (props, store) => ({ data: store.get('key') }));
这种模式的优点在于其有效地分割了各个模块间的职责,在该模式中Store并不须要主动地推送消息给View,而主须要简单地修改数据而后广播说个人状态已经更新了,而后由HOC去主动地抓取数据。那么在做者具体的实现中,就是选用了HOC模式:
register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer) { consumers.push(consumer); }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } }
另外一个常见的用户场景就是咱们须要为界面提供一些默认的状态,换言之当每一个consumer
注册的时候须要提供一些初始化的默认数据:
var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; };
综上所述,最终的Dispatcher函数以下所示:
var Dispatcher = function () { return { _stores: [], register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } } } };
Actions就是在系统中各个模块之间传递的消息载体,做者以为应该使用标准的Flux Action模式:
{ type: 'USER_LOGIN_REQUEST', payload: { username: '...', password: '...' } }
其中的type
属性代表该Action所表明的操做而payload
中包含了相关的数据。另外,在某些状况下Action中没有带有Payload,所以可使用Partial Application方式来建立标准的Action请求:
var createAction = function (type) { if (!type) { throw new Error('Please, provide action\'s type.'); } else { return function (payload) { return dispatcher.dispatch({ type: type, payload: payload }); } } }
上文咱们已经了解了核心的Dispatcher与Action的构造过程,那么在这里咱们将这两者组合起来:
var createSubscriber = function (store) { return dispatcher.register(store); }
而且为了避免直接暴露dispatcher对象,咱们能够容许用户使用createAction
与createSubscriber
这两个函数:
var Dispatcher = function () { return { _stores: [], register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } } } }; module.exports = { create: function () { var dispatcher = Dispatcher(); return { createAction: function (type) { if (!type) { throw new Error('Please, provide action\'s type.'); } else { return function (payload) { return dispatcher.dispatch({ type: type, payload: payload }); } } }, createSubscriber: function (store) { return dispatcher.register(store); } } } };