項目來源及簡介
該學習項目來自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學習之用,如有不足之處敬請指正。