React.js编程思想
发布在使用React.js开发web应用2015年8月7日view:46075React
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

React.js编程思想

JavaScript框架层出不穷,在很多程序员看来,React.js是创建大型、快速的Web应用的最好方式。这一款由Facebook出品的JS框架,无论是在Facebook还是在Instagram中,它的表现都非常出色。

使用React.js的好处多多,但是其中非常好的一部分是它能够让你重新思考如何创建一个web应用。在本文中,我们将通过一步一步的创建一个产品数据表,并以此过程带领你思考和领悟React.js的编程思想。

步骤零:创建一些伪数据

假设现在我们已经有了一个JSON API和一些伪数据。如下图所示。当然,现在的设计看起来非常糟糕,但是它可以体现出我们将要创建的应用结构:

enter image description here

同时,我们可以通过JSON API返回下列数据:

    [
  {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"}
];

第一步 –将你的UI打散为层级组件

你需要做的第一件事情是在每一个组件(以及子组件)周围画上一个框,然后给每个组件起一个名字。如果设计图是由一个设计师给你的,他通常已经帮你完成了这件事情,因此你可以跑去问他设计图中各个层的名字,并以这些名字为你的React组件命名。

但是我们怎么知道哪些部分应该是一个组件呢?想象一下,加入你现在不是在创建一个应用,而是在创建一个新的函数或者对象,你会怎么做。在这里,我们最好遵循”单一任务原则”,也就是说,一个组件最好只是做一件事情。如果一个组件的任务有所增加,那么你应该考虑是不是要把这个组件拆成几个更小的组件。

由于你经常需要讲一个JSON数据模型展示给用户,因此你需要检查这个模型结构是否正确以便你的UI(在这里指的就是你的组件结构)是否能够正确的映射到这个模型上。这是因为用户界面和数据模型在信息构造方面都要一直,这意味着将你可以省下很多将UI分割成组件的麻烦事。你需要做的仅仅只是将数据模型分隔成一小块一小块的组件以便它们都能够表示成组件。

enter image description here

在上面的示意图中,你可以看到在我们这个简单的应用中,包含着5个组件:

  1. FilterableProductTable(橙色): 包含着整个例子
  2. SearchBar(蓝色): 接收用户的输入
  3. ProductTable(绿色): 基于用户输入展示和过滤数据集合
  4. ProductCategoryRow(青色): 为每一种类型展示一个头部数据
  5. ProductRow(红色): 为每种产品展示一行

如果你注意看ProductTable,你将会看到表头(就是包含着”Name”和”Price”标签的部分)并不是单独一个组件。这并不是什么硬性规定,无论你是否将这部分单独隔离出来都没有错。但是在这个例子中,我们将它作为ProductTable的一个部分,因为它是渲染数据集合的一个部分,而这是ProductTable的任务。然而,如果表头变得很复杂(例如,如果我们在其中添加了排序功能),那么你应该毫不犹豫的将它作为一个单独的ProductTableHeader组件。

现在我们已经理清楚了我们的应用中的组件。我们可以简单的将它们做一个层级排序,如下所示:

 - FilterableProductTable
   - SearchBar
   - ProductTable
     - ProductCategoryRow
     - ProductRow

步骤二:使用React创建一个静态版本的应用

/** @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() {
        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.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

既然现在我们的组件已经都创建完毕了,是时候开始实现我们的应用了。最简单的方式是使用数据模型来渲染一个有UI但是暂时没有交互的应用。在这里之所以把交互单独拿出来,是因为如果只是构建UI的话,非常直观而且几乎不需要什么思考,但是一旦涉及到交互,则会复杂的多。

在创建一个静态版本的应用时,你需要创建一个可以使用给其他组件的组件,同时你需要通过props来传递数据。在React中,props是一种从父组件将数据传递给子组件的方式。当然,在React中还有一个叫做state的概念,但是在创建一个静态版本的应用时,千万不要使用state。state仅仅只在有交互时使用,这意味着,数据会随时发生变化。因此,在这里不应该使用state。

你可以从上到下或者从下到上开始。这就是说,你可以从最外面的组件开始,或者从最里面的组件开始。一般来说,在简单应用中,一半是从上到下,而在大型的项目中,一半是从下到上开始。

在这一步的最后,你会拥有一个可重用的组件库,这个组件库将会渲染你的数据模型。由于这个版本是一个静态的版本,因此所有的组件中仅仅只有render()方法。最外层的组件(FilterableProductTable)将会接收你的数据模型作为prop。如果你对底层数据模型做了一些修改,并且调用renderComponent()方法,相应的UI将会发生变化。由于React是单向数据流(也可以说是单向数据绑定),同时具有很强的模块化,这些变化查看起来将会非常容易,这也是React运行这么快的原因之一。

在React中存在两种”模型”数据:props和state。理解二者之间的区别很重要。

步骤三:识别应用UI的状态(state)

为了让UI变得可交互,你需要能够在数据模型发生改变时触发一些UI的变化。React使用state使得这项工作变得简单。

为了能够正确的创建应用,首先要思考的第一件事情是应用状态的集合。标准很简单:不要重复你自己。例如如果你创建了一个TODO列表,你只需要创建一个TODO项目列表,而不需要专门分出一个状态参数来记录TODO项目的数量。当你想要渲染这个TODO数量时,你只需要直接获取TODO数组的长度即可。

现在思考一下例子中的数据。我们拥有:

  • 原始的产品列表
  • 用户输入的搜索关键字
  • 复选框的值
  • 过滤之后的产品列表

现在我们来看看上面这些数据,看看哪一个是state。问自己三个问题:

  1. 这个数据是否来自父组件中的props?如果是,那么它不是state。
  2. 它会随时间改变吗?如果不是,那么它不是state。
  3. 它能够基于组件中的任何state或者props计算出来吗?如果是,那么它不是state。

原始的产品列表作为props传递到组件中,因此它不是state。搜索字段和复选框因为会随时间变化并且不能够由其他state或者props计算出,因此是state。最后,产品的过滤列表不是state,因为它可以根据原始数据、搜索字段以及复选框一起计算出。

因此,我们的state包括:

  • 用户输入的搜索字段
  • 复选框的值

步骤四:弄清楚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() {
        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" value={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.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

现在我们已经弄清楚了state集合。下面我们要做的事情就是弄清楚这些state应该存在什么地方。

记住:React的数据是通过层级组件单向流动。弄清楚哪个组件应该拥有state并不是一件容易的事情。这往往是React新手最容易弄错的地方。

首先,对于应用中的每一个state,你需要:

  • 找出那些需要基于state渲染的组件
  • 找出一个公共的组件(也就是能够包含所有可能涉及state的组件的组件)
  • 公共组件以及之外的组件应该拥有这个state
  • 如果你找不出一个拥有state的组件,创建一个简单的可以包含这个state的组件,然后将它添加到公共组件之上

具体到例子中的应用:

  • ProductTable需要基于state过滤产品列表,而SearchBar需要展示搜索字段和复选框的状态
  • 公共组件是FilterableProductTable
  • 很明过滤字段和复选框值都存在于FilterableProductTable之中

到目前为止,我们已经弄清楚了我们的state位于FilterableProductTable中。首先,我们需要在FilterableProductTable中添加一个getInitialState()方法,这个方法将会返回{filterText: ", inStockOnly: false}来反映应用的初始状态。其次,将filterTextinStockOnly作为一个prop传递给ProductTableSearchBar。最后,在ProductTable中使用这些props去过滤,并在SearchBar中设置相应的值。

步骤五:添加反向数据流

/** @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"
                        value={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.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

到目前为止,我们的应用已经基本完成了。现在我们需要使用另一种方法来支持数据流:层级深处的组件需要更新FilterableProductTable的state。

在React中完成这件事情非常直观,但是这和传统的双向数据绑定有一些不同。虽然React提供了一个叫做ReactLink的插件来帮助我们做类似于双向绑定的事情,但是为了能够明确其中的具体过程,我们在这里不使用这个插件。

到目前为止,如果你进行了一些输入或者选中了一些复选框,你会发现React并没有做出反应。这很正常,因为我们将value这个prop的值总是设置为等于来自FilterableProductTable中的state

现在来明确一下我们想要做的事情。我们想要确保无论用户在何时做了修改,应用都能正确的反应state。因为组件只可以改变自己的state,FilterableProductTable将会给SearchBar传递一个回调函数,这个毁掉函数将会在state发生变化时被触发。我们可以在输入框上使用onChange事件来进行触发。接着,由FilterableProductTable传递的回调函数将会调用setState(),应用将会被更新。

尽管听起来很复杂,但是其实需要写的代码非常少。同时,这也清晰的向我们展示了数据流是怎样穿过整个应用的。

总结

到这里,我们的应用就算是创建完成了。本文可能并不会教你很多关于React的用法,但是你可以从中学习到React应用和模块创建的基本思想。React的代码非常具有模块化,而且易于阅读。现在就开学习使用React来创建不可思议的web应用吧。


本文参考自thinking in react,原文地址http://facebook.github.io/react/docs/thinking-in-react.html

评论
发表评论
2年前
添加了一枚【评注】:hi
2年前
赞了此文章!
2年前
添加了一枚【评注】:假如
2年前

8错

2年前
添加了一枚【评注】:这段话应该在上面代码的前面吧
2年前
添加了一枚【评注】:?
2年前
添加了一枚【评注】:too long, didn't read
2年前
赞了此文章!
2年前
赞了此文章!
2年前
赞了此文章!
2年前
添加了一枚【评注】:回调
2年前
添加了一枚【评注】:呵呵
2年前
添加了一枚【评注】:test
2年前
添加了一枚【评注】:niubi
2年前
添加了一枚【评注】:test
2年前
添加了一枚【评注】:test
2年前
添加了一枚【评注】:假如
2年前
添加了一枚【评注】:假如
2年前
赞了此文章!
2年前
赞了此文章!
2年前
添加了一枚【评注】:致
2年前
添加了一枚【评注】:将
2年前
添加了一枚【评注】:回调
2年前
添加了一枚【评注】:test
2年前
赞了此文章!
2年前

对浏览器兼容的情况如何。没有查到

3年前
赞了此文章!
3年前
赞了此文章!
3年前
赞了此文章!
WRITTEN BY
张小俊128
Intern in Baidu mobile search department。认真工作,努力钻研,期待未来更多可能。
TA的新浪微博
PUBLISHED IN
使用React.js开发web应用

介绍React.js库使用的基础知识

我的收藏