[译] React.js 模式

原文出自:http://krasimirtsonev.com/blog/article/react-js-in-design-patternsjavascript

前言

我想找一个好的前端前端框架,找了好久。这个框架将可以帮助我写出具备可扩展性、可维护性 UI 的代码。经过对 React.js 优点的理解,我认为“我找到了它”。在我大量的使用过程当中,我发现了一些模式性的东西。这些技术被一次又一次的用于编程开发之中。此时,我将它写下来、讨论和分享这些我发现的模式。html

这些全部的代码都是可用的,可以在 https://github.com/krasimir/react-in-patterns 中下载。我可能不会更新个人博客,可是我将一直在 GitHub 中发布一些东西。我也将鼓励你在 GitHub 中讨论这些模式,经过 issue 或者直接 pull request 的方式。前端

1、React 本身的交流方式(Communication)

在使用 React 构建了几个月的状况下,你将可以体会到每个 React Component 都是一个小系统,它可以本身运做。它有本身的 state、input、output.java

Input

React Component 经过 props 做为 input(以后用输入代替)。下面咱们来写一个例子:react

// 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 组件中咱们添加了两个设置 propTypesdefaultProps,咱们来单独看一下:git

  • propTypes - 定义 props 的类型,这将帮助咱们告诉 React 咱们将传什么类型的 prop,可以对这个 prop 进行验证(或者说是测试)。程序员

  • defaultProps - 定义 props 默认的值,设置一个默认值是一个好习惯。github

还有一个 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 标签(孩子组件)将不会被渲染。redux

对于一个组件的间接性输入(就是多层组件传递数据的时候),咱们也能够调用 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,并鼓励使用单向数据流(在后面的单向数据流中将介绍)。

2、组件构成(composition)

源码:https://github.com/krasimir/r...

另一个很棒的是 React 的可组合性。对于我来讲,除了 React 以外尚未发现有任何框架可以如此简单的方式去建立组件以及合并组件。这段我将探索一些组件的构建方式,来让开发工做更加棒。

让咱们先来看一个简单的例子:

  1. 假设咱们有一个应用,包含 header 部分,header 内部有一个 navigation(导航)组件。

  2. 因此,咱们将有三个 React 组件:App、Header 和 Navigation。

  3. 他们是层级嵌套的关系。

因此最后代码以下:

<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 内部的时候是很是有用的,以及在外部提供这个须要合并的组件。

3、高阶组件(Higher-order components)

源码:https://github.com/krasimir/r...

高阶组件看起来很像装饰器模式。他是包裹一个组件和附加一些其余功能或者 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)。

4、依赖注入(Dependency injection)

源码:https://github.com/krasimir/r...

大部分模块/组件都会有依赖。可以合理的管理这些依赖可以直接影响到项目是否成功。有一个技术叫:依赖注入(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 函数有三个参数:

  1. 一个 React 组件

  2. 须要依赖的数据,这个数据以数组的方式定义

  3. 一个 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(如今你们能够看一下源码了)。

我我的发现这个技术是真的有用。它是知足了我处理全部依赖数据的须要,使个人组件变得更加纯粹和更方便测试。

5、单向数据流(One-way direction data flow)

源码:https://github.com/krasimir/r...

在 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 应用,这些就是它让应用变得更简单的缘由,这个模式可能还须要更多的实践,可是它是确实值得探索的。

6、结语

固然,这不是在 React 中全部的设计模式/技术。还可能有更多的模式,你可以 checkout github.com/krasimir/react-in-patterns 进行更新。我将努力分享我新的发现。

相关文章
相关标签/搜索