Thinking in React

react

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

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

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

数据模型

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

以下便是我们模拟的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),利于维护和理解。

如上图所示,我们将这个应用分为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);

对,就是这样

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

以上是 Thinking in React 的全部内容, 来源链接: utcz.com/z/381155.html

回到顶部