在使用JavaScript开发大型、快速的网页应用时,React是我们的首选。在Facebook和Instagram,React很好地减少了我们的工作量。
React最强大部分之一,是让你在开发应用的同时,按照开发应用的思路去思考。这篇文章,将指导你通过几个步骤,用React开发一个可搜索数据表
从一个模型开始
假设我们已经有了一个JSON API和一个设计师设计的模型。看起来像这样:
我们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分解为组件结构
首先,在模型图上框处每个组件(和子组件),并给组件命名。如果与设计师一起工作,他可能已经心中有数,不妨和他聊聊。他们Photoshop图层名可能就能成为你的组件名!
但如何划分父组件子组件呢?就像创建一个函数或对象一样。其中之一是单一职责原则,理想情况下,一个组件仅做一件事。如果一个组件太大,它应该被分解为更小的子组件。
如果经常把JSON数据模型展现给用户,你会发现,若模型创建正确,UI(以及组件结构)能很好的反映数据。这是因为UI和数据模型一定程度都是依附于信息架构的,这意味着,将UI分解为组件并不是最重要的,最重要的是,被分解的组件能准确表现你数据模型的一部分。
可以看出,我们的app有五个组件,我们用斜体表示每个组件所代表的数据。
FilterableProducTable
(可过滤产品表,橙色):包含整个示例SearchBar
(搜索栏,蓝色):接收用户输入ProductTable
(产品表,绿色):根据用户输入,展示和过滤数据集ProductCategoryRow
(产品分类行,蓝绿色):显示为每个分类的头部ProductRow
(产品行,红色):每个产品显示为一行
观察ProductTable
,你会发现表头(包含“Name”和“Price”标签部分)未被划分为一个组件。表头是否做为组件属于个人偏好,至于使用哪种方式,以下可以作为参考。本例中,表头作为ProductTable
的一部分,因为表头属于渲染的数据集的一部分,所以表头在ProductTable
的职责范围内。但如果表头变得更为复杂(如为添加排序功能),将表头分离为ProductTableHeader
更合理。
现在,我们已经将模型划分为了组件,接着要安排组件的层次结构。这并不难,模型中一个组件出现在另一个组件的内部,在层次结构呈现为它的一个子级。
-
FilterableProductTable
SearchBar
-
ProductTable
ProductCategeoryRow
ProductRow
第二步:创建React静态版本
class ProductCategoryRow extends React.Component {
render() {
return <tr><th colSpan="2">{this.props.category}</th></tr>;
}
}
class ProductRow extends React.Component {
render() {
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>
);
}
}
class ProductTable extends React.Component {
render() {
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>
);
}
}
class SearchBar extends React.Component {
render() {
return (
<form>
<input type="text" placeholder="Search..." />
<p>
<input type="checkbox" />
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
render() {
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'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
现在,你已经有了组件的层次结构,是时候实现这个app了。最简单的方式,利用数据模型渲染一个无交互的UI版本。创建组件静态版本需要敲很多键盘,无需过多思考,而添加交互效果需要很多思考,不需要敲很多键盘,这正是极好的解耦过程。
用数据模型创建app的静态版本,你在创建组件时会想着复用其它组件,想着用属性来传递数据,而属性是父级向子级传递数据的方式。即便你熟悉状态的概念,创建静态版本时也不要用任何状态。状态仅用作交互,状态数据随时间而变化,因为静态版本无需任何交互,所以没必要使用任何状态。
在层级结构中,既可以自上而下开始创建组件(从FilterableProductTable
开始),也可以自下而上开始创建组件(从ProductRow
开始)。简单的例子中,自上而下的方式往往更简单,对于大型项目来说,自下而上的方式更简单,更容易测试。
该步完成时,你已经有了一些可复用的组件,这些组件渲染了你的数据模型。静态版本的组件仅有render()
方法。顶层组件(FilterableProductTable
)会把数据模型作为一个属性。如果数据模型发生变化,再次调用ReactDOM.render()
,UI会被更新。你可以很容易的看出UI是如何被更新的,哪儿发生了改变。React的单向数据流(也叫单向数据绑定),使得一切变得模块化,变得快速。
简述:属性与状态
属性和状态是React中的两种数据模型,理解两者的区别非常重要。如果你无法确定两者的区别,请浏览官方文档
第三步:确定描述UI的最少状态
为了让UI可以交互,需要能够触发数据模型发生改变。React的状态能让此变得简单。
为了能正确的创建app,你要考虑app需要的最少状态。关键在于不重复,使用最少的状态,计算出所有你需要的数据。例如,你创建一个TODO列表,只需要保留一个TODO项数组,不需要再使用一个变量来保存TODO项数量,当你需要渲染TODO项数目时,使用TODO项数组长度即可。
考虑示例应用中所有的数据,我们有:
原始产品列表
用户输入的搜索文本
多选框的值
过滤过的产品列表
简单地问三个问题,让我们过一遍,看看哪些是状态:
是否为从父组件传入的属性?如果是,或许不是状态
是否是随时间不变的?如果是,或许不是状态
是否能通过组件中的其他状态或属性计算出来?如果是,不是状态
原始产品列表是从属性传入的,因此不是状态。搜索文本和多选框因为会发生变化,且不能通过计算得出,所以是状态。最后,过滤过的产品列表,可以通过原始产品列表、搜索文本和多选框值计算出来,因此它不是状态。
最终,我们的状态有:
用户输入的搜索文本
多选框的值
第四步:确定状态应该放置的位置
class ProductCategoryRow extends React.Component {
render() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
}
class ProductRow extends React.Component {
render() {
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>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach((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;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
render() {
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>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
}
render() {
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'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
OK,我们已经确定了app的最少状态。接下来,我们要确定哪个组件是可变的,或者说状态属于哪个组件。
记住,React在层级及结构中,数据向下流动。这或许不能马上清楚状态究竟属于哪个组件。这经常也是初学者认识React最有挑战性的部分。所以,按照以下步骤,弄清这个问题:
对于应用中的每个状态:
确认每个需要这个状态进行渲染的组件
找到它们共有的父组件(层级结构中,所有需要这个状态的组件之上的组件)
让共有父组件,或者比共有父组件更高层级的组件持有该状态
如果你找不到合适的组件持有该状态,创建一个新的组件,简单持有该状态。把这个组件插入层级结构共有父组件之上
将这个策略用到我们的应用中:
ProductTable
要根据状态过滤产品列表,SearchBar
要显示搜索文本和多选框状态共有父组件是
FilterableProductTable
FilterableProductTable
持有过滤文本和多选框值很合理
Cool,就这样决定把状态放置到FilterableProductTable
。首先,在FilterableProductTable
的constructor
中添加示例属性this.state = {filterText: '', inStockOnly: false}
,来初始化状态。然后,将filterText
和isStockOnly
作为属性传入ProductTable
和SearchBar
。最后,使用这些属性过滤ProductTable
中的行,和设置SearchBar
中表单的值
第五步:添加反向数据流
class ProductCategoryRow extends React.Component {
render() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
}
class ProductRow extends React.Component {
render() {
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>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach((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;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange() {
this.props.onUserInput(
this.filterTextInput.value,
this.inStockOnlyInput.checked
);
}
render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
ref={(input) => this.filterTextInput = input}
onChange={this.handleChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
ref={(input) => this.inStockOnlyInput = input}
onChange={this.handleChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
this.handleUserInput = this.handleUserInput.bind(this);
}
handleUserInput(filterText, inStockOnly) {
this.setState({
filterText: filterText,
inStockOnly: inStockOnly
});
}
render() {
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'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
目前为止,我们已经用属性和状态,创建了具有在层级结构中至上而下的数据流。现在,是时候支持反向的数据流动,让底层表单组件能够更新FilterableProductTable
中的状态。
React的单向数据流动使得程序更加清晰易懂,但相比双向数据绑定,确实需要多打些代码。
如果你尝试在当前示例中,输入文本或选择多选框,会发现React忽略了你的输入。这是故意的,因为我们要设置input
的value
属性要恒等于FilterableProductTable
传入的状态。
思考下我们期望的状况。我们希望,表单中无论何时发生改变,用户的输入要能够更新状态。因为组件应该只更新自己的状态,所以FilterableProductTable
会将回调函数传递给SearchBar
,SearchBar
会在要更新状态时调用回调函数。我们用onChange
事件通知状态更新。从FilterableProductTable
传入的回调函数会调用setState()
,从而更新组件。
虽然听起来有点复杂,但是也就添加几行代码。它非常清晰地表现出了app中的数据流动。
就这样
希望此文能带给你使用React创建组件和应用的思路。尽管你可能需要打比平时更多的代码,但记住,代码看着总是远比打着的时候多,并且这些代码都是模块化、清晰而易读的。当你开始开发大型组件库的时候,你将会庆幸React的数据流清晰和模块化的特性,并且随着代码的复用,代码规模会开始缩小。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。