本文翻译自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个组件。
FilterableProductTable
(orange): 包含所有的子组件,是个容器
SearchBar
(blue): 用于用户输入交互
ProductTable
(green): 呈现数据项并根据用户输入过滤数据
ProductCategoryRow
(turquoise): 显示条目信息
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:
是否通过父组件通过props传递,如果是,则不是state
是否随着时间而改变,如果不变,则不是state
可以通过其他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);
对,就是这样
例子虽然非常简单,但是里面蕴含的思想确实值得玩味。组件的设计,数据的传递,状态集的确定,双向数据的传递以及事件处理和获取具名组件等等技术都包含在内,如果真的吃透了这个例子,那么我想在今后的可重用敏捷开发之路上必定又有新的收获,具体到我们的实现上就是组件设计的更为优美,代码量更为精少。
还没有评论,来说两句吧...