此文章是翻译thingking in react这篇React(版本v15.5.4)官方文档。
React 的编程思想
在我们看来,React 是 JavaScript 构建大型,高性能 Web 应用的首选。在 Facebook 和 Instagram 中都能很好的应用。
React 中许多重要部分之一是思考如何构建应用程序。 在本文档中,我们将引导你完成用 React 构建一个可搜索的产品数据表的思考过程。
从一个线框图开始
设想你已经根据我们的设计有一个模拟JSON API。这个Mock 看上去如下:
我们的JSON API 返回的数据如下:
[
{category: "Sporting Goods",price: "$49.99",stocked: true,name: "Football"},{category: "Sporting Goods",price: "$9.99",name: "Baseball"},price: "$29.99",stocked: false,name: "Basketball"},{category: "Electroincs",price: "$99.99",name: "iPod Touch"},price: "$399.99",name: "iPhone 5"},price: "$199.99",name: "Nexus 7"}
]
步骤1:将 UI 拆解到组件层次结构中
第一件事在mock 中给每一个组件(子组件)画框并进行命名。他们可能已经做了这些工作,所以去和他们交流一下!他们的 Photoshop 图层名称可能最终成为你的 React 组件名称(愚人码头注:意思是可以和设计师交流一下,约定图层命名规范)!
但是你该如何拆分组件呢?其实只需要像拆分一个新方法或新对象一样的方式即可。一个常用的技巧是单一职责原则single responsibility principle,也就是说,理论上一个组件只做一件事。如果组件变大,它应该被拆分成几个小组件。
由于你经常展示一个JSON 数据模型给用户,你会发现如果你的模型被正确构建,那么你的UI(和你的组件框架)会非常好的匹配。那是因为UI 和数据模型总是依附相同的信息架构(information architecture),也就是意味着分离你的UI 成为组件是非常容易的。只要正确地的将你的每一个数据模型对应组件就可以了。
你会发现在我们这个简单的app中有5 个组件。我们使用斜体数据来表示每一个组件
FiterableProductTable
(orange):包含整个例子SearchBar
(blue):接受所有的用户输入ProdutTable
(green):根据用户输入来展示过滤的数据集合ProductCategoryRow
(宝石绿turquoise):展示每一个分类的标题ProductRow
(red):每一个产品展示一行
如果你看ProductTable
,你会发现表格头(包含”Name” 和”Price” 标签)并不是单独一个组件。这是一个重要的表现,并且这里可以使用两种方式作为参数传入。例如,我们将它作为ProductTable
的一部分,因为因为它是ProductTable
的负责的渲染数据的一部分。无论怎样,如果它的头变复杂了(例如,如果我们添加了一个排序功能),最好的方式的作为一个单独的ProductTableHeader
组件。
现在,我们已经在线框图中做了标识,让我们对它们进行层级结构排列。这很简单。在线框图中,出现在其它组件内的组件,应该在层次结构中显示为子组件即可:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
步骤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',name: 'Baseball'},price: '$29.99',stocked: false,name: 'Basketball'},{category: 'Electronics',price: '$99.99',name: 'iPod Touch'},price: '$399.99',name: 'iPhone 5'},price: '$199.99',name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,document.getElementById('container')
);
现在你有了自己的组件系统,是时候实现你的app 了。最简单的方式的是个构建一个没有交互的只是将数据渲染成UI。最好解耦这些过程,因为构建一个静态版本需要许多代码不需要思考,添加交互需要更多思考和少量的代码。我们会看到为什么。
为了构建一个静态的app 去渲染你的数据模型(data model),你想要构建组件并复用其他的组件同使用props 去传递数据。props 是从父组件传递给子组件数据的一种方式。如果你了解state 的概念,在构建静态版本时,一点也不需要使用state(don’t use state at all)。State 只用于交互,也就是说,数据可以随时被改变。因为是静态版本,所以你不需要state。
在构建时你可以自上而下(top-down)或自下而上(down-top)。也就是说,你可以从位于高层级的组件(例如,FilterableProductTable
)或者是低层级的组件(ProductRow
)开始。在简单的例子中,通常是自上而下(top-down),而在大型项目中,通常是从下而上(down-top)这样便于编写测试用例。
在此最后,你有了许多可复用的组件来渲染你的数据模型。因为是静态版本的应用,所以这些组件中仅有render()
方法。在顶层组件(FilterableProductTable
)将会接受你的数据模型作为prop。如果你改变你的基础数据模型(underlying data model)并且又调用了ReactDOM
,这个UI 将会被更新。这是很容易看淡你的UI 是如何更新,并在哪里做出改变,因为没有什么复杂的事情。React 是单向数据流one-way data flow(也叫做单向绑定one-way bind)来保持一切模块化和高性能。
.render()
如果你在执行这一步的时候需要帮助,可以简单浏览React doc。
小插曲: Props(属性) vs State(状态)
在 React 中有两种类型的“模型”数据:props(属性) 和 state(状态)。理解这两者的差异非常重要;如果你不确定它们之间的区别,请参阅官方 React 文档the offical React docs
步骤3: 确定 UI state(状态) 的最小(但完整)表示
为了你的 UI 可以交互,你需要能够触发更改底层的数据模型。React 通过 state
使其变得容易。
要正确的构建应用程序,你首先需要考虑你的应用程序需要的可变 state(状态) 的最小集合。这里的关键是:不要重复你自己 (DRY,don’t repeat yourself)。找出你的应用程序所需 state(状态) 的绝对最小表示,并且可以以此计算出你所需的所有其他数据内容。例如,如果你正在构建一个 TODO 列表,只保留一个 TODO 元素数组即可;不需要为元素数量保留一个单独的 state(状态) 变量。相反,当你要渲染 TODO 计数时,只需要获取 TODO 数组的长度即可。
考虑我们在实例应用程序中所有的数据块。我们有:
- 最初的产品列表(
The original list of products
) - 用户键入的搜索文本(
The search text the user has entered
) - checkBox 的值(
The value of the checkBox
) - 过滤后的产品列表(
The filtered list of products
)
让我们仔细分析每一个数据,弄清楚哪一个是 state(状态) 。请简单地提出有关每个数据的 3 个问题:
- 它是父组件通过props 传递的吗?如果是,它很可能不是state。
- 是否永远不会发生变化? 如果是这样,它可能不是 state(状态)。
- 在组件中,你能够基于其他的state 或props 来计算出它吗?如果是,它不是state。
The original list of products
是通过props 进行传递的,所以它不是state 。The search text and the checkBox
似乎是state 因为它们会根据用户的输入发生变化,并且不能从其他数据计算得出。 最后,The filtered list of products
不是state,因为它可以通过结合原始产品列表the original list of products
和搜索文本和复选框的值 the searche text and value of the checkBox
计算出来。
所以最终,我们的state 是:
步骤4:确定 state(状态) 的位置
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',document.getElementById('container')
);
OK,现在我们已经确认了app state 的最小集合。Next,我们需要确认一个组件改变或拥有这些 state。
记住:React 单向数据流在层级中自上而下进行。它可能并不立即清楚那一个组件应该拥有什么state。这个通常对新手去了解是充满挑战的,所以根据接下来的步骤来搞清楚它:
对于你应用中的每一个state :
- 确认每一个组件基于那个state 来进行渲染
- 找出公共父级组件(一个单独的组件,在组件层级中位于所有需要这个 state(状态) 的组件的上面。愚人码头注:父级组件)。
- 公共父级组件 或者 另一个更高级组件拥有这个 state(状态) 。
- 如果找不出一个拥有该 state(状态) 的合适组件,可以创建一个简单的新组件来保留这个 state(状态) ,并将其添加到公共父级组件的上层即可。
我们在我们的应用中贯穿这个策略:
* ProductTable
需要这个state 去过滤产品列表(product list),并且SearchBar
需要去展示搜索文字(search text)和选中状态(checked)state
* 公共的父级组件是FilterableProductTable
* 在概念上来说是有道理的,这个filter text 和checked value 位于FilterableProductTable
组件中
那么我们已经决定 state(状态) 保存在 FilterableProductTable 中。首先,添加一个实例属性 this.state = {filterText: ”,inStockOnly: false} 到 FilterableProductTable 的constructor 来反映你应用的初始 state(状态) 。然后,将filterText
和inStockOnly
作为prop 传递给SearchBar
和ProductTable
。最后,使用这些props 过滤ProductTable
中的row 以及在SearchBar
中设置form fields 的值。
你可以看你的应用的行为:设置filterText
为ball
并刷新你的应用,你会看到table 中数据正确更新了。
步骤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;
console.log(this.props.inStockOnly)
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.handleFilterTextInputChange = this.handleFilterTextInputChange.bind(this);
this.handleInStockInputChange = this.handleInStockInputChange.bind(this);
}
handleFilterTextInputChange(e) {
this.props.onFilterTextInput(e.target.value);
}
handleInStockInputChange(e) {
this.props.onInStockInput(e.target.checked);
}
render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
onChange={this.handleFilterTextInputChange}
/>
<p>
<input
type="checkBox"
checked={this.props.inStockOnly}
onChange={this.handleInStockInputChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',inStockOnly: false
};
this.handleFilterTextInput = this.handleFilterTextInput.bind(this);
this.handleInStockInput = this.handleInStockInput.bind(this);
}
handleFilterTextInput(filterText) {
this.setState({
filterText: filterText
});
}
handleInStockInput(inStockOnly) {
this.setState({
inStockOnly: inStockOnly
})
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextInput={this.handleFilterTextInput}
onInStockInput={this.handleInStockInput}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
var PRODUCTS = [
{category: 'Sporting Goods',document.getElementById('container')
);
到现在为止,我们已经建立了一个应用程序,进行正确渲染将props 和state 作为功能在层级中向下流动。现在,是时候添加支持数据从另一个方法流动:在层级中最深出的form 组件去更新位于FilterableProductTable
中的state。
Reack 为了使数据流明确,使它能够容易的了解你的程序是如何工作的,但是它需要更多的数据相比传统的双线数据绑定(two-way data binding)
如果在这个例子版本中进行键入信息或操作多选框,你会发现React 忽略了你的输入。这是趋势,通过设置input
的value
等于在FiterableProductTable
中的state
。
想想我们希望发生什么。我们期望当用户改变表单输入的时候,我们更新 state(状态) 来反映用户的输入。由于组件只能更新它们自己的 state(状态) ,FilterableProductTable 将传递回调到 SearchBar,然后在 state(状态) 被更新的时候触发。我们可以使用 input 的 onChange 事件来接收通知。而且通过 FilterableProductTable 传递的回调调用 setState(),然后应用被更新。
尽管这听起来很复杂,但真的只需要简单的几行代码即可实现。同时清晰的表达数据在应用中的流动。
就这么简单
希望,他能够给你一个思路,在React 中如何思考构建组件和应用。虽然它可能比你之前使用更多的代码,记住代码可读性远超过它的编写, 模块化、结构清晰的代码最利于阅读。当你开始构建大量的组件库,你将感激模块化、结构清晰和可以重用的代码,同时你的代码行数会慢慢减少。:)
原文链接:https://www.f2er.com/react/302334.html