React版本:15.4.2
**翻译:xiyoki **
在我们看来,React是使用Javascript构建大型,快速的Web应用程序的首要方式。它在我们的Facebook和Instagram上已经非常好。
React许多重要的部分之一是它如何让你在构建应用程序时进行思考。在本文档中,我们将向你介绍使用React构建可搜索的产品数据表的思考过程。
Start With A Mock(从Mock开始)
想象一下,我们已经有了一个JSON API和设计师提供的mock。Mock看起来像这样:
我们的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"}
];
Step 1: Break The UI Into A Component Hierarchy(步骤1:将UI拆分为一个组件层次结构)
你要做的第一件事是为mock中每个组件(和子组件)的周围绘制框,并为所有的框命名。如果你正在与一个设计师工作,他们可能已经这样做了,所以去和他们谈谈吧!他们的Photoshop图层名称最终可能是你的React组件的名称。
但你怎样知道什么能自成为一个组件呢?只需使用相同的技术来决定是否应该创建一个新的函数或对象即可。其中一个技术是单一职责原则,即理想情况下,组件应该只做一件事。如果它最终生长,它应该被分解为更小的子组件。
由于你经常向用户展示JSON数据模型,你会发现如果你的模型构建正确,你的UI(即你的组件结构)将会很好地映射。这是因为UI和数据模型倾向于遵循相同的信息架构,这意味着将UI划分为组件的工作通常是微不足道的。只需要将UI分解成一个完全代表数据模型的组件即可。
你会看到,在我们简单的应用程序中有五个组件。我们用斜体表示每个组件所代表的数据。
- FilterableProductTable (橙色): 包含全部示例。
- SearchBar (蓝色): 接收所有用户输入。
- ProductTable (绿色): 根据用户输入显示和过滤数据集。
- ProductCategoryRow (绿松石): 显示每个类别的标题。
- ProductRow (红色): 每个产品显示一行。
如果你看看ProductTable
,你会看到表头(包含名称和价格的标签)不自成为一个组件。这是一个偏好问题,也是一个争论。就这个示例而言,我们将它作为ProductTable
的一部分是因为它是数据集渲染的一部分,而渲染数据集是ProductTable
的职责。然而,如果这个头部变得复杂(即,如果我们添加排序功能),则使其自成为ProductTableHeader
组件是有意义的。
现在我们已经在我们的mock中识别了组件,让我们将它们排列成层次结构。这很容易。在mock中,出现在另一个组件中的组件应该在层次结构中显示为子级:
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
Step 2: Build A Static Version in React(步骤2:在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')
);
现在,你已经拥有了组件层次结构,是时候实现应用程序了。最简单的方法是构建一个版本,它接收你的数据模型并渲染UI,但没有交互性。最好解耦这些进程,因为构建静态版本需要大量的打字,没有思考,添加交互性需要很多思考,而不是打很多字。我们会看到其中的原因。
要构建渲染你的数据模型的应用程序的静态版本,你需要构建可重用其他组件并使用props传递数据的组件。Props是将数据从父级传递到子级的一种方式。如果你熟悉状态的概念,不要使用状态来构建这个静态的版本。状态仅保留用于交互性,即随时间变化的数据。由于这是一个静态版本的应用程序,你不需要它。
你可以自顶向下或自下而上的构建。也就是说,你可以从层次结构中较高的组件开始(即从FilterableProductTable
开始)或从较低的组件开始(ProductRow
)。在更简单的例子中,通常更容易自上而下,在更大的项目中,更容易从底层向上编写和测试。
在此步骤结束时,你将有一个可重用的组件库,用于呈现你的数据模型。因为这是一个静态版本的应用程序,所以组件只用render()方法。位于层次结构顶部的组件(FilterableProductTable
)将把你的数据模型作为props。如果对基础数据模型进行更改并再次调用ReactDOM.render()
,则UI将更新。很容易看到你的UI是如何更新的,在哪里做更改,因为没有什么复杂的。React的单向数据流(也称为单向绑定)使一切模块化和快速。
A Brief Interlude: Props vs State(一个简短的插曲:Props vs State)
React中有两种类型的‘模型’数据:props和state。重要的是要了解二者之间的区别。
Step 3: Identify The Minimal (but complete) Representation Of UI State(步骤3:识别UI状态的最小(但完整)表示)
要使你的UI交互,你需要能够触发对基础数据模型的更改。React 状态使这个很容易。
要正确构建你的应用程序,你首先需要考虑你的应用需要的最小可变状态集。这里的关键是DRY:不要重复自己。确定你的应用程序需要的状态的绝对最小表示,并计算其他所有你需要的内容。例如,你正在构建一个TODO列表,只需要保留一个TODO项数组;不要为计数保留单独的状态变量。相反,当你想渲染TODO计数时,只需取TODO项数组的长度。
想想我们应用程序中的所有数据。我们有:
- 原始产品列表
- 用户输入的搜索文本
- 复选框的值
- 已过滤的产品列表
让我们一 一过一遍,找出哪一个是状态。只需对每个数据询问三个问题:
- 该数据是从父级props变量传递进来的吗?如果是这样,它可能不是状态。
- 该数据是否随时间保持不变?如果是这样,它可能不是状态。
- 你可以根据组件中其他任何状态或props来计算该数据吗?如果是这样,它可能不是状态。
原始的产品列表作为props传递,所以不是状态。搜索文本和复选框似乎是状态,因为它们随时间变化,不从任何其他数据计算而来。最后,过滤的产品列表不是状态,因为它可以通过将原始产品列表与搜索文本和复选框的值组合来计算。
所以最后,状态有:
- 用户输入的搜索文本
- 复选框的值
Step 4: Identify Where Your State Should Live(步骤4:识别状态应该处在什么位置上)
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,我们已经确定了什么是最小的应用程序状态集。接下来,我们需要确定哪个组件变动或拥有此状态。
记住:React就是单向数据流的组件层次结构。
哪个组件应该拥有什么状态可能不是立即就能清楚的。对于新手来说,这通常是最具有挑战性的部分,因此请按以下步骤了解:
对于应用程序中的每个状态:
- 标识基于该状态渲染某物的每个组件。
- 找到一个公共所有者组件(在层次结构中,一个在所有组件之上并且需要状态的单个组件)。
- 公共所有者或层次结构中较高的另一组件应该拥有该状态。
- 如果你找不到一个拥有该状态是有意义的组件,那么就创建一个只是为了保持状态的新组件,并将其添加到公共所有者组件上层的某个位置。
让我们来运行一下这个策略:
productTable
需要基于状态过滤产品列表,并且SearchBar
需要显示搜索文本和选中状态。
- 公共所有者组件是
FilterableProductTable
。 - 过滤文本和选中值存在于
FilterableProductTable
上,这从概念上来说是有意义的。
酷。所以我们决定让状态存在于FilterableProductTable
上。
首先,将这个实例属性this.state={filterText: '', inStockOnly: false}
添加到FilterableProductTable
的constructor
,以此来反映应用程序的初始状态。
然后,通过将filterText
和inStockOnly
作为prop传递到ProductTable
和SearchBar
。
最后,用这些props来过滤ProductTable
中的行,并且设置表单字段inSearchBar
的值。
此刻你可以看到你的应用程序将如何表现:为ball
设置filterText
并且刷新你的应用程序。你将看到数据表已正确更新。
Step 5: Add Inverse Data Flow (步骤5:添加反向数据流)
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')
);
到目前为止,我们已经构建了一个应用程序,该应用程序渲染正确,其函数式的props和state在层次结构中向下流动。现在是时候以其他方式支持数据流:深层的表单组件需要更新FilterableProductTable
中的状态。
为便于理解你的应用程序是如何工作的,React使这个数据流显示。但这确实比传统的双向数据绑定需要打更多的字。
如果你尝试在示例的当前版本中键入或选中该框,你将看到React忽略了你的输入。这是有意的,因为我们已设置input 的value prop总是等于从FilterableProductTable
传递过来的state。
让我们想想,我们想要发生什么。我们要确保每当用户更改表单时,我们将更新状态以反映用户输入。由于组件应该只更新自己的状态,FilterableProductTable
将向SearchBar
传递回调函数,该回调函数将在状态更新时触发。我们可以将input的onChange事件作为它的通知。由FilterableProductTable
传递的回调函数将会调用setState()
,应用程序将被更新。
虽然这听起来很复杂,但它只是几行代码。数据如何在整个应用程序中流动已非常明确。
And That's It
希望,这给了你如何使用React来构建组件和应用程序的想法。虽然它可能比你常用的方法更多的打字,记住代码的可读性远远超过它的书写,并且阅读这个模块化、明确性的代码是非常容易的。当你开始构建大型的组件库时,你会欣赏这种明确性和模块性,并且使用代码重用,你的代码行将开始收缩。