项目来源及简介
该学习项目来自React官方文档中的“React哲学部分”(https://react.docschina.org/docs/thinking-in-react.html)。该文档为读者提供了一个用于介绍React学习理论的实例项目:可搜索产品数据表格。并对该项目从设计方面进行了讨论和分析,但文档中没有给出相应的实现代码,应该是给React学习者提供一个实际练手的机会。
借此学习机会,本文根据React文档中对该项目的分析和设计来实现该数据表格的所有功能。并根据该文档中的分析思路来一步一步实现相应的功能,最终构建出完整可用的数据表格。
关于项目环境:该项目没有依据React官方文档中创建的项目,而是使用了阿里的ice作为基础项目框架,其中由于自动整合了React,因此可以进行React的开发工作,具体的环境可以见文章:Ice项目结构理解和使用Ice搭建React多页面学习和开发环境。本文以此为基础进行开发。
数据结构和原型设计
下面在实际的开发之前,我们对需要实现的数据表格进行设计。首先是该表格需要使用的数据和结构,以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"}
];
根据上述的数据源,文档中给出该数据表格的设计稿如下:
数据表格组件分析和设计
基于该数据表格的数据表,下面对其进行组件的拆分和层次的设计。该部分的设计React文档中已经给出。可以将上述的数据表格根据下图所示的结构进行拆分。而对于React中组件的设计原则,本着高可复用性的思想,应尽可能的将React组件设计为单一职责。也就是说,一个组件原则上只能负责一个功能。如果它需要负责更多的功能,这时候就应该考虑将它拆分成更小的组件。
对于该数据表格,我们将其根据层次拆分为五个组件,根据颜色如下:
ProductFilterTable
(橙色): 是整个示例应用的整体ProductSearchBar
(蓝色): 接受所有的用户输入ProductTable
(绿色): 展示数据内容并根据用户输入筛选结果ProductCategoryRow
(天蓝色): 为每一个产品类别展示标题ProductRow
(红色): 每一行展示一个产品
现在我们已经确定了设计稿中应该包含的组件,接下来我们将把它们描述为更加清晰的层级。设计稿中被其他组件包含的子组件,在层级上应该作为其子节点。
· ProductFilterTable
· ProductSearchBar
· ProductTable
· ProductCategoryRow
· ProductRow
使用props实现数据表格的静态版本
现在我们已经确定了组件层级,可以编写对应的应用了。在编写的过程中包含两个步骤:
- 先用已有的数据模型渲染一个不包含交互功能的 UI(使用props实现的静态版本)
- 添加组件间的交互(使用state来实现组件之间数据的交互)
这样将这两个过程分开。是因为编写一个应用的静态版本时,往往要编写大量代码,而不需要考虑太多交互细节;添加交互功能时则要考虑大量细节,而不需要编写太多代码。所以,将这两个过程分开进行更为合适。
下面使用props来定义上述设计的组件并使用一些静态数据来初始的填充数据表格,各个组件的定义如下:
ProductFilterTable
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import ProductSearchBar from "../ProductSearchBar";
import ProductTable from "../ProductTable";
class ProductFilterTable extends Component {
render() {
return (
<div>
<h1>React Practice</h1>
<ProductSearchBar/>
<ProductTable />
</div>
)
}
}
export default ProductFilterTable;
ProductSearchBar
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import './index.css'
class ProductSearchBar extends Component {
render() {
return (
<div>
<input
type="text"
value="SearchText"
name="searchProduct"/>
<div className="searchCheckDiv">
<input
type="checkbox"
checked="true"
name="hasStock"
style={{display: 'inline'}}/>
<p style={{display: 'inline'}}>
Only show products in stock.
</p>
</div>
<br/>
</div>
)
}
}
export default ProductSearchBar;
ProductTable
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import ProductCategoryRow from "../ProductCategoryRow";
import ProductRow from "../ProductRow";
class ProductTable extends Component {
render() {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<ProductCategoryRow category="Category1"/>
<ProductRow name="product1" price="1.0" />
<ProductRow name="product2" price="1.0" />
<ProductRow name="product3" price="1.0" />
<ProductCategoryRow category="Category2"/>
<ProductRow name="product4" price="1.0" />
<ProductRow name="product5" price="1.0" />
<ProductRow name="product6" price="1.0" />
</tbody>
</table>
)
}
}
export default ProductTable;
ProductCategoryRow
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
class ProductCategoryRow extends Component {
render() {
return (
<tr>
<label>
{this.props.category}
</label>
</tr>
)
}
}
export default ProductCategoryRow;
ProductRow
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
class ProductRow extends Component {
render() {
return (
<tr>
<td>{this.props.name}</td>
<td>{this.props.price}</td>
</tr>
)
}
}
export default ProductRow;
使用如上的定义, 现在可以得到其中不包含实际数据的数据表格,显示如下:
此时对于该数据表格我们只是使用了页面中的硬编码形式定义了该表格的数据,并将其UI渲染出来。在搞定了基本UI的基础上,我们还有两个主要的方面没有做:
- 使用文档中给出的真实数据,利用props实现数据从父组件到子组件的单项数据流。
- 确定需要的state,完成搜索框以及“only show product in stock”选项的交互。
这部分的下面我们首先完成第一个事项,使用props来完成给定数据的向下流动。为此我们对以上的组件内容进行相应的修改。
首先在ProductFilterTable组件中定义要进行传递的数据,并将其作为props传递给用于显示product的ProductTable组件,ProductFilterTable修改如下:
class ProductFilterTable extends Component {
render() {
const 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"}
];
return (
<div>
<h1>React Practice</h1>
<ProductSearchBar/>
<ProductTable
products={products} />
</div>
)
}
}
为了在ProductTable组件中显示传递的products数组中的数据内容,我们对ProductTable中的内容进行如下修改:
class ProductTable extends Component {
// 用于对数组中的元素进行去重
unique = (arr) => {
return Array.from(new Set(arr));
};
render() {
// 获得product的所有category
const categorys = this.props.products.map((product) => {
return product.category;
});
// 取到category去重之后的数组值
let cateArr = this.unique(categorys);
// 构建数据表项
const productGroup = cateArr.map((cate) => {
// 得到每个cate对应的productList
const productList = this.props.products.filter((product) => {
return product.category === cate;
});
return (
<React.Fragment>
<ProductCategoryRow category={cate}/>
{
productList.map((product) => {
return <ProductRow
key={product.id}
name={product.name}
price={product.price}/>;
}
)
}
</React.Fragment>
)
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{productGroup}
</tbody>
</table>
)
}
}
该段代码是对products传入的数据进行处理,来依次显示如下的格式:
ProductCategory1
product1
product2
ProductCategory2
product3
product4
... ...
代码中需要关注的几个问题点如下:
- 遍历所有数据得到产品中所有的category列表,并对其进行去重(unique方法)。
- 遍历去重之后的cateArr列表,并将products中对应cate的产品过滤出来用于构建上述的数据表现形式。
- 使用了React的<React.Fragment>隐藏标签,使多个组件可以直接并列。
- 使用map函数的嵌套来渲染productRow组件。
完成以上修改之后,就可以使用products中包含的给定数据,通过props来实现数据自顶向下的传递。得到的页面如下:
此时使用props来进行数据的静态化展示就已经完成了。但此时搜索框以及下面的选择的项仍然是没有作用的,下面就需要对其中组件的交互进行实现。
确定包含的state以及所在位置
上面的过程已经得到了使用指定数据来进行渲染的数据表的数据内容。但其中的搜索框和checkbox标签的功能还没有实现。此时组件之间的交互以及动态数据的渲染,需要涉及到React中state的使用。对于该数据表中需要使用那些state以及state应该放在什么位置,是需要解决的问题。
在React文档中有如下说明:
为了正确地构建应用,你首先需要找出应用所需的 state 的最小表示,并根据需要计算出其他所有数据。其中的关键正是 DRY: Don’t Repeat Yourself。只保留应用所需的可变 state 的最小集合,其他数据均由它们计算产生。
确定State包含的最小表示
对于该示例应用,分析可知其中拥有如下数据:
- 包含所有产品的原始列表
- 用户输入的搜索词
- 复选框是否选中的值
- 经过搜索筛选的产品列表
通过问自己以下三个问题,你可以逐个检查相应数据是否属于 state:
- 该数据是否是由父组件通过 props 传递而来的?如果是,那它应该不是 state。
- 该数据是否随时间的推移而保持不变?如果是,那它应该也不是 state。
- 你能否根据其他 state 或 props 计算出该数据的值?如果是,那它也不是 state。
而对于该示例中的各种数据:
- 包含所有产品的原始列表是经由 props 传入的,所以它不是 state;
- 搜索词和复选框的值应该是 state,因为它们随时间会发生改变且无法由其他数据计算而来;
- 经过搜索筛选的产品列表不是 state,因为它的结果可以由产品的原始列表根据搜索词和复选框的选择计算出来。
因此综上所述,该示例中属于 state 的有:
- 用户输入的搜索词(filterText)
- 复选框是否选中的值(isStockOnly)
确定State所在的位置
上面我们已经确定了需要的state,下面需要找到谁该持有这些state值。对于如何确定state的所在位置,react文档中给出了一些tips:
对于应用中的每一个 state:
- 找到根据这个 state 进行渲染的所有组件。
- 找到他们的共同所有者(common owner)组件(在组件层级上高于所有需要该 state 的组件)。
- 该共同所有者组件或者比它层级更高的组件应该拥有该 state。
- 如果你找不到一个合适的位置来存放该 state,就可以直接创建一个新的组件来存放该 state,并将这一新组件置于高于共同所有者组件层级的位置。
对于该示例中,filterText用于显示ProductSearchBar的值,isOnlyStock用于显示checkbox的值。ProductTable根据filterText和isOnlyStock的状态对products进行过滤,而ProductSearchBar和ProductTable组件的父组件是ProductFilterTable。因此很自然的filterText和isOnlyStock这两个state应该放置待ProductFilterTable组件中。
因此在确定了state的内容和位置之后,我们就有了如下的实现思路;
- 首先,将实例属性
this.state = {filterText: '', inStockOnly: false}
添到 ProductFilterTable 的constructor
中,设置应用的初始 state; - 接着,将
filterText
和inStockOnly
作为 props 传入 ProductSearchBar组件,并在SearchBar中<input>和<checkbox>标签进行改变时,修改相应的filterText和isStockOnly数值(需要使用下面的反向数据流)。 - 然后,将
filterText
和inStockOnly
作为 props 传入 ProductTable ,用这些 props 筛选 ProductTable 中的产品信息,并更新ProductTable中的数据表单。
添加组件间的反向数据流
下面我们完成上述思路中的前两步,通过使用state和反向数据流来实现在ProductSearchBar中对父组件ProductFilterTable中state数值的更新操作。React 通过一种比传统的双向绑定的方式来实现反向数据传递。
首先我们梳理一下需要实现的功能:
每当用户改变ProductSearchBar中filterText和isStockOnly的值时,我们需要改变 state 来反映用户的当前输入。由于 state 只能由拥有它们的组件(ProductSearchBar)进行更改,因此ProductFilterTable组件
必须将一个能够触发 state 改变的回调函数(callback)传递给 ProductSearchBar。我们可以使用输入框的 onChange
事件来监视用户输入的变化,并通知 ProductFilterTable
传递给 SearchBar
的回调函数。然后该回调函数将调用 setState()
,从而更新应用。
使用props传递回调函数反向更新state
ProductFilterTable组件中具有如下更改:
- 添加constructor初始化state。
- 添加handleFilter和handleStock回调函数,用于在ProductSearchBar组件中进行回调。
- 将state和回调函数作为props传递给子组件ProductSearchBar和ProductTable。
class ProductFilterTable extends Component {
// React构造方法
constructor(props) {
super(props);
this.state = {
filterText: '',
isStockOnly: false,
}
}
// 更新filterText状态的callback function
handelFilter = (filterText) => {
this.setState({
filterText: filterText
});
};
// 更新isStockOnly的callback function
handleStock = (checked) => {
this.setState({
isStockOnly: checked,
});
};
render() {
const products = [
...
];
console.log("filterText updated: " + this.state.filterText);
console.log("checked updated: " + this.state.isStockOnly);
return (
<div>
<h1>React Practice</h1>
<ProductSearchBar
filterText={this.state.filterText}
isStockOnly={this.state.isStockOnly}
onFilterChange={this.handelFilter}
onStockChange={this.handleStock} />
<ProductTable
filterText={this.state.filterText}
isStockOnly={this.state.isStockOnly}
products={products} />
</div>
)
}
}
ProductSearchBar组件中更新代码如下:
- 为两个input选项绑定onChange方法,在input标签中的内容进行更改时调用。
- 绑定的方法中调用传入props中的回调函数,用于反向更新state。
class ProductSearchBar extends Component {
handleFilterChange = (event) => {
this.props.onFilterChange(event.target.value);
};
handleStockChange = (event) => {
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
this.props.onStockChange(value);
};
render() {
return (
<div>
<input
type="text"
value={this.props.filterText}
name="searchProduct"
onChange={this.handleFilterChange}/>
<div className="searchCheckDiv">
<input
type="checkbox"
checked={this.props.isStockOnly}
name="hasStock"
onChange={this.handleStockChange}
style={{display: 'inline'}}/>
<p style={{display: 'inline'}}>
Only show products in stock.
</p>
</div>
<br/>
</div>
)
}
}
此时在查看更新后的页面,在search框或者点击下面的stock选择框时,可以在控制台看到相应state值的更新:
此时我们就通过在ProductFilterTable组件中,将更新state值的函数当做props传递给ProductSearchBar组件,从而在ProductSearchBar组件中通过反向调用传递的函数来更新父组件中state的功能。
实现了state的反向更新后,下面就需要将更新后的state(filterText和isStockOnly)作为props传递给ProductTable组件中,用于对products数据的过滤和更新了。此时我们需要修改ProductTable组件中的render方法,其他部分保存不变:
render() {
// 获得product的所有category
const categorys = this.props.products.map((product) => {
return product.category;
});
// 取到category去重之后的数组值
let cateArr = this.unique(categorys);
console.log("cateArr is: " + cateArr);
// 根据不同的category来渲染ProductCategoryRow和相应的ProductRow
const productGroup = cateArr.map((cate) => {
// 得到每个cate对应的productList
let productList;
if (this.props.isStockOnly) {
productList = this.props.products.filter((product) => {
return product.category === cate && product.stocked === true;
});
} else {
productList = this.props.products.filter((product) => {
return product.category === cate;
});
}
// 根据filterText来对productList进行遍历和过滤(也可以使用正则表达式)
let productFiltered = productList;
if (this.props.filterText !== '') {
productFiltered = productList.filter((product) => {
// 使用正则表达式匹配
// let reg = new RegExp("");
// return reg.test(product.name);
// 使用String对象的search方法进行匹配
return -1 !== product.name.search(this.props.filterText);
});
}
return (
<React.Fragment>
<ProductCategoryRow category={cate}/>
{
productFiltered.map((product) => {
return <ProductRow
key={product.id}
name={product.name}
price={product.price}/>;
}
)
}
</React.Fragment>
)
});
这里的主要更新就是在构建最后用于数据渲染的productList时,根据filterText和isStackOnly的值来对其中不符合条件的值进行过滤,从而动态的更新数据得到符合条件的数据值。
说明:其中对于filterText使用,主要使用的是javascript中String对象的search方法,同时也可以使用正则表达式来进行匹配过滤。
完成数据表格
完成以上的内容之后就完成了整个可搜索产品数据表格,具体的效果如下。
数据表格的初始状态:
勾选"only show product in stock":
输入搜索文本:
总结
本文基于React文档中(https://react.docschina.org/docs/thinking-in-react.html)的内容实现了一个可搜索产品数据表格,其中使用到的React的相关知识主要包括:
- props和组件
- state和反向数据更新
- map方法嵌套
- React.Fragment的使用
- 多组件的使用
- 以及使用React来实现相关功能的思路分析
上述代码仅对该数据表格中的具体组件进行了说明,具体的项目代码见:https://github.com/Yitian-Zhang/my-ice-start。该文件中的代码和该项目为自己React学习之用,如有不足之处敬请指正。