原文出自:http://krasimirtsonev.com/blog/article/react-js-in-design-patterns
媒介
我想找一个好的前端前端框架,找了良久。这个框架将能够协助我写出具有可扩展性、可维护性 UI 的代码。经由过程对 React.js 上风的明白,我以为“我找到了它”。在我大批的运用过程当中,我发明了一些形式性的东西。这些手艺被一次又一次的用于编程开辟当中。此时,我将它写下来、议论和分享这些我发明的形式。
这些一切的代码都是可用的,能够在 https://github.com/krasimir/react-in-patterns 中下载。我能够不会更新我的博客,然则我将一直在 GitHub 中宣布一些东西。我也将勉励你在 GitHub 中议论这些形式,经由过程 issue 或许直接 pull request 的体式格局。
一、React 本身的交换体式格局(Communication)
在运用 React 构建了几个月的状况下,你将能够体会到每个 React Component 都是一个小体系,它能够本身运作。它有本身的 state、input、output.
Input
React Component 经由过程 props
作为 input(以后用输入替代)。下面我们来写一个例子:
// 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' />;
}
};
个中的 Title
组件只要一个输入 – text
. 在父组件(App)供应了一个属性,经由过程 <Title>
组件。在 Title
组件中我们增加了两个设置 propTypes
和 defaultProps
,我们来零丁看一下:
propTypes – 定义 props 的范例,这将协助我们通知 React 我们将传什么范例的 prop,能够对这个 prop 举行考证(或许说是测试)。
defaultProps – 定义 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 标签(孩子组件)将不会被衬着。
关于一个组件的间接性输入(就是多层组件通报数据的时刻),我们也能够挪用 context
举行数据的接见。在全部 React tree 中的每个组件中能够会有一个 context 对象。更多的申明将在依靠注入
章节解说。
Output
React 的输出就是衬着事后的 HTML 代码。在视觉上我们将看到一个 React 组件的模样。固然,有些组件能够包括一些逻辑,能够协助我们通报一些数据或许触发一个事宜行动(这类组件能够不会有详细的 UI 形状)。为了完成逻辑范例的组件,我们将继承运用组件的 props:
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');
}
};
我们经由过程一个 callback 的体式格局在子组件中举行挪用,logoClicked
要领能够吸收一些数据,如许我们就能够从子组件向父组件传输一些数据了(这里就是 React 体式格局的子组件向父组件通讯)。
我们之前有提到我们不能够接见 child 的 state。或许换句话说,我们不能够运用 this.props.children[0].state 的体式格局或许其他什么体式格局去接见。准确的姿态应该是经由过程 props callback 的体式格局猎取子组件的一些信息。这是一件功德。这就迫使我们要去定义明确的 APIs,并勉励运用单向数据流(在后面的单向数据流
中将引见)。
二、组件组成(composition)
别的一个很棒的是 React 的可组合性。关于我来讲,除了 React 以外还没有发明有任何框架能够云云简朴的体式格局去建立组件以及兼并组件。这段我将探究一些组件的构建体式格局,来让开辟事情越发棒。
让我们先来看一个简朴的例子:
假定我们有一个运用,包括 header 部份,header 内部有一个 navigation(导航)组件。
所以,我们将有三个 React 组件:App、Header 和 Navigation。
他们是层级嵌套的关联。
所以末了代码以下:
<App>
<Header>
<Navigation> ... </Navigation>
</Header>
</App>
我们为了组合这些小组件,而且援用他们,我们须要向下面如许定义他们:
// 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 组件做为顺序的进口,在这个组件内里去构建组件是一个不错的处所。关于 Header 组件,能够会包括其他组件,比方 logo、search 或许 slogan 之类的。它将黑白常优点置惩罚,能够经由过程某种体式格局从外部传入,因而我们没有须要建立一个强依靠的组件。假如我们在别的的处所须要运用 Header 组件,然则这个时刻又不须要内层的 Navigation 子组件。这个时刻我们就不轻易完成,由于 Header 和 Navigation 组件是两个强耦合的组件。
如许编写组件是不轻易测试的,我们能够在 Header 组件中有一些营业逻辑,为了测试 Header 组件,我们就必需要建立一个 Header 的实例(实在就是援用组件来衬着)。但是,又由于 Header 组件依靠了其他组件,这就致使了我们也能够须要建立一些其他组件的实例,这就让测试不是那末轻易。而且我们在测试过程当中,假如 Navigation 组件测试失利,也将致使 Header 组件测试失利,这将致使一个毛病的测试效果(由于不会晓得是哪一个组件测试没有经由过程)。(注:然后在测试中 shallow rendering 处理了这个题目,能够只衬着 Header 组件,不必实例化其他组件)。
运用 React’s children API
在 React 中,我们能够经由过程 this.props.children
来很轻易的处置惩罚这个题目。这个属机能够让父组件读取和接见子组件。这个 API 将使我们的 Header 组件更笼统和低耦合(原文是 dependency-free 不好翻译,然则是这个意义)。
// 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>;
}
}
这将轻易测试,由于我们能够让 Header 组件衬着成一个空的 div 标签。这就让组件脱离出来,然后只专注于运用的开辟(实在就是笼统了一层父组件,然后让这个父组件和子组件举行相识耦,然后子组件能够才是运用的一些功用完成)。
将 child 做为一个属性
每个 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>
);
}
};
这个手艺在我们要兼并两个组件,这个组件在 Header 内部的时刻黑白常有效的,以及在外部供应这个须要兼并的组件。
三、高阶组件(Higher-order components)
高阶组件看起来很像装潢器形式。他是包裹一个组件和附加一些其他功用或许 props 给它。
这里经由过程一个函数来返回一个高阶组件:
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));
}
};
起首,高阶组件实在也是衬着的原始组件(传入的组件)。一个好的习气是直接传入 state 和 props 给它。这将有助于我们想代办数据和像是用原始组件一样去运用这个高阶组件。
高阶组件让我们能够掌握输入。这些数据我们想经由过程 props 举行通报。如今像我们说的那样,我们有一个设置,OriginalComponent 组件须要这个设置的数据,代码以下:
var config = require('path/to/configuration');
var enhanceComponent = (Component) =>
class Enhance extends React.Component {
render() {
return (
<Component
{...this.state}
{...this.props}
title={ config.appTitle }
/>
)
}
};
这个设置是隐藏在高阶组件中。OriginalComponent 组件只能经由过程 props 来挪用 title 数据。至于 title 数据从哪里来关于 OriginalComponent 来讲并不重要(这就异常棒了!封闭性做的很好)。这是极大的上风,由于它协助我们测试自力组件,以及供应一个好的机制去 mocking 数据。这里能够如许运用 title 属性( 也就是 stateless component[无状况组件] )。
var OriginalComponent = (props) => <p>{ props.title }</p>;
高阶组件是须要别的一个有效的形式-依靠注入(dependency injection)。
四、依靠注入(Dependency injection)
大部份模块/组件都邑有依靠。能够合理的治理这些依靠能够直接影响到项目是不是胜利。有一个手艺叫:依靠注入(dependency injection,以后我就简称 DI 吧)。也有部份人称它是一种形式。这类手艺能够处理依靠的题目。
在 React 中 DI 很轻易完成,让我们随着运用来思索:
// 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 />;
}
};
有一个 “React in patterns” 的字符串,这个字符串以某种体式格局来通报给 Title 组件。
最直接的体式格局是经由过程: App => Header => Title 每一层经由过程 props 来通报。但是如许能够在这个三个组件的时刻比较轻易,然则假如有多个属性以及更深的组件嵌套的状况下将比较贫苦。大批组件将吸收到它们并不须要的属性(由于是逐层通报)。
我们前面提到的高阶组件的体式格局能够用来注入数据。让我们用这个手艺来注入一下 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 }
/>
)
}
};
// Header.jsx
import enhance from './enhance.jsx';
import Title from './Title.jsx';
var EnhancedTitle = enhance(Title);
export default function Header() {
return (
<header>
<EnhancedTitle />
</header>
);
}
这个 title 是隐藏在中间层(高阶组件)中,我们经由过程 prop 来通报给 Title 组件。这很好的处理了,然则这只是处理了一半题目,如今我们没有层级的体式格局去通报 title,然则这些数据都在 echance.jsx 中间层组件。
React 有一个 context 的观点,这个 context 能够在每个组件中都能够接见它。这个长处像 event bus 模子,只不过这里是一个数据。这个体式格局让我们能够在任何处所接见到数据。
// 我们定义数据的处所:context => title
var context = { title: 'React in patterns' };
class App extends React.Component {
getChildContext() {
return context;
}
...
};
App.childContextTypes = {
title: React.PropTypes.string
};
// 我们须要这个数据的处所
class Inject extends React.Component {
render() {
var title = this.context.title;
...
}
}
Inject.contextTypes = {
title: React.PropTypes.string
};
值得注意的是我们必需运用 childContextTypes 和 contextTypes 这两个属性,定义这个上下文对象的范例声明。假如没有声明,context 这个对象将为空(经我测试,假如没有这些范例定义直接报错了,所以一定要记得加上哦)。这能够有些不太适宜的处所,由于我们能够会放大批的东西在这里。所以说 context 定义成一个纯对象不是很好的体式格局,然则我们能够让它成为一个接口的体式格局来运用它,这将许可我们去存储和猎取数据,比方:
// 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 组件就从这个 context 中猎取数据:
// 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
};
最好的体式格局是我们在每次运用 context 的时刻不想定义 contextTypes。这就是能够运用高阶组件包裹一层。以至更多的是,我们能够写一个零丁的函数,去更好的形貌和协助我们声明这个分外的处所。以后经由过程 this.context.get(‘title’) 的体式格局直接接见 context 数据。我们经由过程高阶组件猎取我们须要的数据,然后经由过程 prop 的体式格局来通报给我们的原始组件,比方:
// 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 组件
须要依靠的数据,这个数据以数组的体式格局定义
一个 mapper 的函数,它能吸收上下文的原始数据,然后返回一个我们的 React 组件(比方 Title 组件)现实须要的数据对象(相当于一个 filter 管道的作用)。
这个例子我们只是经由过程这类体式格局通报来一个 title 字符串变量。然后在现实运用开辟过程当中,它多是一个数据的存储鸠合,设置或许其他东西。因而,我们经由过程这类体式格局,我们能够经由过程哪些我们确切须要的数据,不必去污染组件,让它们吸收一些并不须要的数据。
这里的 wire 函数定义以下:
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 对象的 dependencies 一切的设置项数组。这个 mapper 函数能够吸收 context 的数据,并转换它,然后给 props 末了通报到我们的组件。
末了来看一下关于依靠注入
在许多处理方案中,都运用了依靠注入的手艺,这些都基于 React 组件的 context 属性。我以为这很好的晓得发生了什么。在写这篇文凭的时刻,大批盛行构建 React 运用的体式格局会须要 Redux。有名 connect 函数和 Provider 组件,就是运用的 context(如今人人能够看一下源码了)。
我个人发明这个手艺是真的有效。它是满足了我处置惩罚一切依靠数据的须要,使我的组件变得越发地道和更轻易测试。
五、单向数据流(One-way direction data flow)
在 React 中单向数据流的形式运作的很好。它让组件不必修正数据,只是吸收它们。它们只监听数据的转变和能够供应一些新的值,然则它们不会去转变数据存储器内里现实的数据。更新会放在别的处所的机制下,和组件只是供应衬着和新的值。
让我们来看一个简朴的 Switcher 组件的例子,这个组件包括了一个 button。我们点击它将能够掌握切换(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 对象是单例 我们有 helper 去设置和猎取 _flag 这个属性的值。经由过程 getter,然后组件能够经由过程外部数据举行更新。大楷我们的运用事情流看起来是如许的:
User's input
|
Switcher -------> Store
让我们假定我们要经由过程 Store 给后端效劳去保存这个 flag 值。当用户返回的时刻,我们必需设置适宜的初始状况。假如用户脱离后在厥后,我们必需展现 “lights on” 而不是默许的 “lights off”。如今它变得难题,由于我们的数据是在两个处所。UI 和 Store 中都有本身的状况,我们必需在它们之间交换: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 组件和我们就增加了本身 App 的庞杂度。
单向数据流就处理了这个题目。它消除了这类多种状况的状况,只保存一个状况,这个状况平常是在 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 的体式格局,但这类体式格局不引荐运用。平常状况能够运用高阶组件举行从新衬着。我们运用 forceUpdate 只是简朴的演示。
由于这个转变,Switcher 变得比之前简朴。我们不须要内部的 state:
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 数据的一个填鸭式组件。它是真的让 React 组件变成了地道的衬着层。我们写我们的运用是声明的体式格局,而且只在一个处所处置惩罚一些庞杂的数据。
这个运用的事情流就变成了:
Service communicating
with our backend
^
|
v
Store <-----
| |
v |
Switcher ---->
^
|
|
User input
我们看到这个数据流都是一个方向活动的,而且在我们的体系中,不须要同步两个部份(或许更多部份)。单向数据流不止能基于 React 运用,这些就是它让运用变得更简朴的缘由,这个形式能够还须要更多的实践,然则它是确切值得探究的。
六、结语
固然,这不是在 React 中一切的设想形式/手艺。还能够有更多的形式,你能够 checkout github.com/krasimir/react-in-patterns 举行更新。我将勤奋分享我新的发明。