Thinking in React

本文翻译自React的官方博客,详情请阅读原文。

React非常适合构建组件化的应用,它注重高性能,因此组建的重用,项目的扩展都十分灵活,Facebook和instagram的不少商业项目使用了此框架。

本文主要通过“输入查询数据”这个简单的demo来说明或者学习如何用React来架构。

数据模型

       我们需要根据JSON API来显示并且操作数据,最终的可视化操作是基于JSON数据的基础之上。最终的效果图如下:

thinking-in-react-mock

以下便是我们模拟的JSON数据:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

step1 变UI为组件继承

           我们如何确定哪一部分应该为一个组件呢?我们可以遵循“单一职责”原则,也就是说,理想的组件只做一件事情。组件也应该根据数据(model)的结构灵活进行设计,这样最终的UI匹配提供的数据(model),利于维护和理解。

thinking-in-react-components

如上图所示,我们将这个应用分为5个组件。

  1. FilterableProductTable (orange): 包含所有的子组件,是个容器
  2. SearchBar (blue): 用于用户输入交互
  3. ProductTable (green): 呈现数据项并根据用户输入过滤数据
  4. ProductCategoryRow (turquoise): 显示条目信息
  5. ProductRow (red): 显示产品的具体信息

我们可以看到,tHead部分(Name和Price)并不是一个单独的组件,在这个例子中,之所以tHead属于ProductTable组件是因为它并没有与数据(model)有关联,考虑这种情况,如果要单击tHead部分的表头实现表格内容的排列,我们最好为tHead单独设计一个组件,并在该组件上绑定事件处理函数。

至此,我们将这五个组件的继承关系确定下来:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

step2 创建静态版本

       有了组件的继承关系,我们首先创建一个静态版本的应用。我们可以通过组件复用以及父子组件之间的props通信来完成模型数据的渲染。props是父子组件通信的一种方式,如果你也了解state特性的话,那么一定不要使用state来构建静态版本,state用于创建交互版本,也就是说,state中的数据会随着时间而改变,下面的一节会讲解何时将数据放入state中。

      我们可以自顶向下或者自下而上来构建应用,在做测试时我们可以自下而上来进行每个模块的测试,而一般构建应用我们则是采用自顶向下的模式,结合数据的自上而下传递,利于开发。

      在这一步,由于我们构建的是静态版本,因此每个组件只实现了其render方法,用以基本的数据渲染。最顶层的组件(FilterableProductTable)的props中存入要渲染的数据模型,每当模型数据发生改变时,会对应的视图层的改变,这也正是React所提出的的单向数据流模型(one-way data flow)。

      在React中,组件有两种类型数据--props和state。它们之间的具体区别可以参考官方文档

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        });
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." />
                <p>
                    <input type="checkbox" />
                    {' '}
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    render: function() {
        return (
            <div>
                <SearchBar />
                <ProductTable products={this.props.products} />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
 
React.render(<FilterableProductTable products={PRODUCTS} />, document.body);

step3 确定组件的state

       为了使应用具有动态交互性,必须将状态的改变(用户的输入或者单击操作等)反映到我们的UI上,通过React给组件提供的state完成上述需求。

       我们首先要确定应用的可变的状态集合,遵循DRY原则:don't repeat youself。我们需要考虑应用中的所有的数据,它包括:

  • 基本的产品列表
  • 用户输入的过滤条件
  • checkbox的值
  • 过滤后的产品列表

根据下面条件选择哪些数据可以作为state:

  1. 是否通过父组件通过props传递,如果是,则不是state
  2. 是否随着时间而改变,如果不变,则不是state
  3. 可以通过其他state或者props计算得到,如果可以,则不是state

产品数据列表是通过父组件的props传递,因此不是state,用户输入和checkbox满足上述三个条件,可以作为state,二对于过滤的列表,则可以根据产品数据和用户输入来获取到,因此不是state。

故,input输入值和checkbox的值可以作为state。

step4 确定state所属的组件

目前确定了state集合,接下来需要确定究竟是哪个组件拥有这个state,或者随着state而变化。

我们要明确React的单项数据流是沿着组件继承链流动的,这有时很难确定哪一个组件拥有这个state,不过我们可以根据以下原则来大体确定state所属的组件。

       在每一个状态期,

  • 确保每个组件都会根据当前状态来渲染
  • 寻找其共同的祖先组件
  • 在继承链中层级较高的组件拥有state

回到我们的应用中,

  • ProductTable需要根据state来过滤数据,SearchBar需要显示输入的文字和选项.
  • 这两个组件的共同祖先是 FilterableProductTable.
  • 因此state的集合应该所属FilterableProductTable组件

所以,我们确定了state所属的组件是FilterableProductTable。我们需要给该组件设置getInitialState方法设置组件的初始状态,并且通过props将状态传递给ProductTable和SearchBar,最后我们就可以在ProductTable和SearchBar中获取状态并根据当前状态显示相应的数据。

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." value={this.props.filterText} />
                <p>
                    <input type="checkbox" checked={this.props.inStockOnly} />
                    {' '}
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.render(<FilterableProductTable products={PRODUCTS} />, document.body);

step5 添加反向数据流

       等等,目前构建的应用并不能通过表单来反向设置state,因此,我们无法再input标签输入任何值。这就需要我们手动进行反向数据设置。React默认的单项数据流是从model渲染到UI,而通过UI来设置model则需要手动编写,主要的操作就是通过获取组件对应的DOM对象,获取当前DOM的属性值并反向设置state来完成。

      当前版本的应用,React会忽略输入值和选定值,这是理所当然的,因为我们在FilterableProductTable中设置的state初始值为filterText=‘’,inStockOnly=false,所以对于ProductTable和SearchBar而言,也就是针对这两个值渲染,但是由于通过input和checkbox的输入并未改变这两个state的值,因此,这两个组件其实并没有被渲染。

      所以我们通过在ProductTable和SearchBar设置事件监听函数,并且每当函数触发时setState当前的状态,促使组件渲染重绘,完成数据的动态呈现。在具体实现中,可以通过refs锚点来获取具体的具名组件,并通过调用组件的getDOMNode方法,获取对于DOM对象并据此设置新的state。

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        console.log(this.props);
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    handleChange: function() {
        this.props.onUserInput(
            this.refs.filterTextInput.getDOMNode().value,
            this.refs.inStockOnlyInput.getDOMNode().checked
        );
    },
    render: function() {
        return (
            <form>
                <input
                    type="text"
                    placeholder="Search..."
                    value={this.props.filterText}
                    ref="filterTextInput"
                    onChange={this.handleChange}
                />
                <p>
                    <input
                        type="checkbox"
                        checked={this.props.inStockOnly}
                        ref="inStockOnlyInput"
                        onChange={this.handleChange}
                    />
                    {' '}
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    handleUserInput: function(filterText, inStockOnly) {
        this.setState({
            filterText: filterText,
            inStockOnly: inStockOnly
        });
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                    onUserInput={this.handleUserInput}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.render(<FilterableProductTable products={PRODUCTS} />, document.body);

对,就是这样

       例子虽然非常简单,但是里面蕴含的思想确实值得玩味。组件的设计,数据的传递,状态集的确定,双向数据的传递以及事件处理和获取具名组件等等技术都包含在内,如果真的吃透了这个例子,那么我想在今后的可重用敏捷开发之路上必定又有新的收获,具体到我们的实现上就是组件设计的更为优美,代码量更为精少。

posted @ 2015-04-19 00:30  royalrover  阅读(718)  评论(0编辑  收藏  举报