camiler

深入React:选择组件类型

原文链接: benmccormick.org

过去几个月,我一直在建立我的第一个使用了react的生产应用。我注意到的第一件事是在一个新的react应用中可以有很多选择。很多是围绕react的系统环境问题(用webpack还是Browserify?Redux还是Relay?我需要用css模块或者Immutable.js么?)但是这众多的选择只是这个生态库中的一部分,得找到最好的或者至少是一个稳定一致的做事方法。

一个选择就是你选择使用的定义组件的语法。在React中,你至少可以用3种方式定义组件:React.createClass,ES6 class, 纯函数式1。每一种都有一些优点和缺点:

React.createClass

React.createClass是定义React组件中原始的方式,也一直是React官方文档首选方式。

下面是看起来像这种方式的一个假设的SaveButton组件。这是一个简单的组件,监听click事件,然后用了一个单独的分发器来发送一个在页面上保存数据的请求。在点击这个按钮后,就会显示一些指定的saved text或者是“saved”。

import React from ‘react’;  
import {dispatcher} from ‘./lib/dispatcher’;

export const SaveButton = React.createClass({

    propTypes: {
        //text to show after the component is saved
        savedText: React.PropTypes.string.isRequired,
        //primary text to show on the button
        text: React.PropTypes.string.isRequired,
    },

    getDefaultProps() {
        return { 
             savedText: ‘Saved’,
        };
    },

    getInitialState() {
        return { saved: false };
    },

    save() {
        dispatcher.sendMessage(‘saveData’);
        this.setState({saved: true});
    },

    render() {
        let {text, savedText} = this.props;
        return (<span className=“button” onClick={this.save}>
           {this.state.saved ? savedText : text}
        </span>);
    },
}); 

这种方式最好的一点就是它的内聚性。组件的所有属性都被描述成一个单一的对象传给React.createClass。因为它只是React处理的一个对象,所以它比ES6 classes和函数式组件更能适应(accommodate容纳)更多的使用方式。具体的说,我们可以用 mixins以及像直接定义组件propTypes那样添加声明属性。

用createClass方式的缺点就是它自定义的特性。在没有一些深入的React源代码检查或者需要人记录和维护已有的React.createClass知识的情况下,外部工具无法检查组件声明,无法知道哪些函数和方法在最终的组件上可用。

ES6 Classes

去年三月,随着React v0.13的发布,React团队引入了使用ES6 classes作为React组件的第二种语法。用ES6 class定义组件也带来一些约束,这使得component API在几个方面做了些改动。用ES6 class的话就没有方法来添加属性到类属性-作为主要定义的一部分,它们需要在事后补充。还有一些细节,用React.createClass是可行的但在ES6 classes中就无法运行。mixins不可用,React也不会再“自动绑定”你的函数到组件实例,所以在你的组件中,当你传类方法作为回调函数时,需要绑定到当前上下文环境,或者通过使用箭头函数2

this.save()} > 或者像这样通过绑定函数的方式 this.save = this.save.bind(this).

import React from ‘react’;  
import {dispatcher} from ‘./lib/dispatcher’;

export class SaveButton extends React.Component {

    constructor(props) {
        super(props);
        this.state = { saved: false };
    }

    save() {
        dispatcher.sendMessage(‘saveData’);
        this.setState({saved: true});
    },

    render() {
        let {text, savedText} = this.props;
        return (<span className=“button” onClick={() =>this.save()}>
           {this.state.saved ? savedText : text}
        </span>);
    },
});

SaveButton.propTypes = {  
    //text to show after the component is saved
    savedText: React.PropTypes.string.isRequired,            
    //primary text to show on the button
    text: React.PropTypes.string.isRequired,
};

SaveButton.defaultProps = {  
    savedText: ‘Saved’,
}; 

虽然这种方式对mixins不支持,也造成了一些语法妥协,但是它具有规范化以及工具支持的优点。一个好的JavaScript工具可以轻松的告知我们一个SaveButton组件应该有render和save方法,也可以检测出它继承自React.Component的方法。这种方式对于像auto-complete, linting, 还有运行性能(理论上)的工具都有帮助。

纯函数式组件

上个秋季随着React 0.14的发布,React又增加了第三种组件定义方式。函数式组件去掉了许多React次要特征,并专注于“render”方法。它们是所有组件语法中最不强大的。除了丢掉mixins之外, 纯函数式组件也没有类的生命周期函数这样的基础语法,同时也没用任何内部状态。对于这种组件,所有的状态必须通过像Redux,或者拥有状态的父组件来外部管理。

import React from ‘react’;  
import {dispatcher} from ‘./lib/dispatcher’;

export const SaveButton = ({text, savedText, isSaved, setSaved}) => {  
    const save = () =>{
        dispatcher.sendMessage(‘saveData’);
        setSaved();
    },
    return (<span className=“button” onClick={save}>
       {isSaved ? savedText : text}
    </span>);
};

SaveButton.propTypes = {  
    //text to show after the component is saved
    savedText: React.PropTypes.string.isRequired,            
    //primary text to show on the button
    text: React.PropTypes.string.isRequired,
    // has the data already been saved?
    isSaved: React.PropTypes.bool.isRequired,
    // a function to update the application state and mark the page as saved
    setSaved: React.PropTypes.func.isRequired,
};

SaveButton.defaultProps = {  
    savedText: ‘Saved’,
}; 

尽管函数式组件是3种组件方式中最弱的,但它确实有一些优点。首先,非常简单。对于它们是怎么回事一目了然,并且React很容易优化。像ES6 classes一样,对于第三方工具来说函数式组件很容易理解因为他们仅仅是函数,也正因如此可以轻松地跟新开发者讲解。另外,函数式组件可以跟像Redux那样的系统良好工作,尽管Redux鼓励将状态放到组件外存在一个全局的store中。

选择一个组件方式

给出以上3种方式后,你该如何选择要使用组件的哪种方式?我们得在遵守最小功率原则和渴望稳定性两者之间衡量一下。

就稳定性要求而言,不要在一个独立的工程中使用超过2种方式。这里主要是打破同时在一个工程中“不要使用createClass 和 ES6 classes”的说法。在前两种方式和函数式组件这种方式之间,无论是性能还是复杂度上面存在着显著差别。但是基于类的风格也差不多,增加了复杂性却未能根据信号意图做很多。在两种不同的方式之间进行概念通信转换也应该在目的上表明一种有意义的差异。

当然最一致的方式还是一直用createClass。因为它是最强大的,任何可以用其他2种方式实现的组件同样也可以用createClass实现。这就是最小功率原则大体现。附上Tim Berners-Lee关于这个原则的最初描述:

在20世纪60年代到80年代,计算机科学花费了大量的精力,使语言尽可能的强大。现今,我们必须感激不是选择了最强大的方案而是最不强大的方案。原因就是语言功能性越小,你可以用该语言存储的数据做的越多。如果你把它写入一个简单的声明名称中,任何人都可以写一个程序在许多方面来分析它。语义化Web主要是试图将大量现有的数据映射到一个通用的语言上,这样这些数据就可以用一种连它的创造者都没有想到的方式进行分析。比如,如果一个天气数据的Web页面有RDF描述该数据,用户可以当作表来检索它,计算它平均值,将它绘制成曲线图,或者结合其他信息来推断。在页面的另一端是狡猾的Java小程序在描绘气象信息。虽然这可能会是一个非常酷的用户界面,但它不能进行分析。搜索引擎完全不知道这个页面有什么数据或者关于什么。要查明一个java小程序的意思是什么,唯一的方法就是让它在人面前运行。

无论对于人类还是计算机来说,react组件类型越少,可读性的方式就越多。这种可读性也限制了错误的类型以及我们可以制定的意外行为。我们应该力求简单的组件,直到可以和我们对于一致性的渴望达到平衡。

对于项目的调查,下面有几个简单的问题,你可以回答来决定使用哪种类型。

  1. 你需要集成可以修改DOM的非React库么?
  2. 你需要在组件中管理状态么(即你不会使用像Redux那样的外部状态管理库)?
  3. 在你的项目中有任何需要使用mixin的么?

如果你对其中任何问题的答案是肯定的,那么在你项目的选项中需要包含一个类为基础的组件样式。如果对于第三个问题的答案都是肯定的,那么你需要使用React.createClass。

一旦在项目层面上做好这些决定,那对于单个的组件该选择什么就小菜一碟了。如果对上面所有问题回答是不(未必是针对大项目,但也有可能),你应该从头到尾都使用函数式组件。如果难以决定每个新的组件该用什么,可以针对每一个问同样的这3个问题。如果对于一个组件,所有问题的答案是不,就使用函数式组件,否则就使用选择用于项目上的基于类的方式。

更多资源

  • React文档 查看不同组件方式的优缺点的最好资料。

  • James Nelson 去年写的类似的指导 专注于组件方式的决策树。他完全摒弃了createClass方式,也给出了类似但稍有不同的一系列提问。

  • github话题 说明了一个重要区别,函数式组件在任何方面至今都未被优化,虽然在未来可能会有。

  • 技术上说来,你也可以使用基于ES5 class的方式,但我打算忽略这个。就我所知,这不是一个通用的React程序术语。

  • 现在有提议把静态属性加到ES6 classes,但目前还处于早期阶段,而且也没有保证最终是ECMAScript标准的一部分。