React的思想
在我看來, React 是較早使用 JavaScript 構(gòu)建大型直焙、快速的 Web 應(yīng)用程序的技術(shù)方案似将。它已經(jīng)被我們廣泛應(yīng)
用于 Facebook 和 Instagram 念搬。
React 眾多優(yōu)秀特征中的其中一部分就是,教會你去重新思考如何構(gòu)建應(yīng)用程序奕扣。 本文中,我將跟你一起使用 React 構(gòu)建一個具備搜索功能的產(chǎn)品列表显押。<br />
從設(shè)計稿(mock或譯作'原型')開始
假設(shè)你已經(jīng)得到了一份JSON API文檔和設(shè)計稿, 設(shè)計稿如下圖:
![](https://github.com/dev-zhoukang/ZKReactSummary/blob/master/imgs/img-1.png?raw=true)
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分離成組件層次
你要做的第一件事是,為所有組件(及子組件)命名并畫上線框圖。假如你和設(shè)計師一起工作,也許他們已經(jīng)完 成了這項工作,所以趕緊去跟他們溝通!他們的 Photoshop 圖層名也許最終可以直接用于你的 React 組件名待诅。
然而你如何知道哪些才能成為組件?想象一下,當(dāng)你創(chuàng)建一些函數(shù)或?qū)ο髸r,用到一些類似的技術(shù)。其中一項技
術(shù)就是單一指責(zé)原則,指的是,理想狀態(tài)下一個組件應(yīng)該只做一件事,假如它功能逐漸變大就需要被拆分成更小
的子組件熊镣。
由于你經(jīng)常需要將一個JSON數(shù)據(jù)模型展示給用戶,因此你需要檢查這個模型結(jié)構(gòu)是否正確以便你的 UI (在這里 指組件結(jié)構(gòu))是否能夠正確的映射到這個模型上卑雁。這是因為用戶界面和數(shù)據(jù)模型在 信息構(gòu)造 方面都要一致,這 意味著將你可以省下很多將 UI 分割成組件的麻煩事。你需要做的僅僅只是將數(shù)據(jù)模型分隔成一小塊一小塊的組 件,以便它們都能夠表示成組件绪囱。
![](https://github.com/dev-zhoukang/ZKReactSummary/blob/master/imgs/img-2.png?raw=true)
* FilterableProductTable (orange): contains the entirety of the example <br />
* SearchBar (blue): receives all user input <br />
* ProductTable (green): displays and filters the data collection based on user input <br />
* ProductCategoryRow (turquoise): displays a heading for each category <br />
* ProductRow (red): displays a row for each product <br />
看看ProductTable测蹲,你會看到表頭(包含“name”和“price”標(biāo)簽)不是自己的組件。 這是一個個人偏好的問題鬼吵。 對于這個例子扣甲,我們把它作為ProductTable的一部分,因為它是渲染數(shù)據(jù)收集的一部分齿椅,這是ProductTable的責(zé)任琉挖。
然而,如果這個頭部變得復(fù)雜(如果我們添加用于排序的可用性)媒咳,那么使它自己的ProductTableHeader
組件會更好一些粹排。
下面就是結(jié)構(gòu)層次:
* FilterableProductTable
* SearchBar
* ProductTable
* ProductCategoryRow
* ProductRow
第二步: 用React構(gòu)建一個靜態(tài)版本
var ProductCategoryRow = React.createClass({
render: function() {
return (<tr>
<th colSpan="2">{this.props.category}</th>
</tr>);
}
});
var ProductRow = React.createClass({
render: function() {
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>
);
}
});
var ProductTable = React.createClass({
render: function() {
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>
);
}
});
var SearchBar = React.createClass({
render: function() {
return (
<form>
<input type="text" placeholder="Search..." />
<p>
<input type="checkbox" />
{' '}
Only show products in stock
</p>
</form>
);
}
});
var FilterableProductTable = React.createClass({
render: function() {
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')
);
- 現(xiàn)在已經(jīng)擁有了組件層次結(jié)構(gòu),現(xiàn)在是實現(xiàn)應(yīng)用程序的時候了涩澡。 最簡單的方法是構(gòu)建一個版本顽耳,它接收您的數(shù)據(jù)模型并呈現(xiàn)UI,但沒有交互性妙同。
最好要解耦這些過程射富,因為構(gòu)建靜態(tài)版本需要大量的typing,沒有thinking粥帚,添加交互性需要很多thinking胰耗,而不是很多typing。 我們看看為什么芒涡。 - 要構(gòu)建呈現(xiàn)您的數(shù)據(jù)模型的應(yīng)用程序的靜態(tài)版本柴灯,您需要構(gòu)建可復(fù)用其他組件和使用
props
傳遞數(shù)據(jù)的組件。props
是將數(shù)據(jù)從父級傳遞到子級的一種方式费尽。 如果你熟悉state
的概念赠群,不要使用state
來構(gòu)建這個靜態(tài)版本。
因為state
僅適用于交互性即隨時間變化的數(shù)據(jù)旱幼。 由于這是一個靜態(tài)版本的應(yīng)用程序查描,所以你不需要state
。 - 關(guān)于構(gòu)建順序, 您可以構(gòu)建自頂向下或自下而上。 也就是說冬三,您可以從層次結(jié)構(gòu)中的較高層(即從FilterableProductTable開始)或在其中較低的層(ProductRow)開始構(gòu)建組件匀油。
在更簡單的例子中,通常應(yīng)該從上到下構(gòu)建勾笆,而在更大的項目中敌蚜,更應(yīng)該從底層向上構(gòu)建應(yīng)用和編寫測試。 - 在此步驟結(jié)束時匠襟,您將有一個用于呈現(xiàn)您的數(shù)據(jù)模型的可重用組件庫钝侠。 組件將只有
render()
方法,因為這是一個靜態(tài)版本的應(yīng)用程序酸舍。 層次結(jié)構(gòu)頂部的組件(FilterableProductTable)將把您的數(shù)據(jù)模型作為props
帅韧。
如果對基礎(chǔ)數(shù)據(jù)模型進(jìn)行更改并再次調(diào)用ReactDOM.render()
,則UI將更新啃勉。 很容易看到你的UI是如何更新的和更改的地方忽舟,因為沒有什么復(fù)雜的。 React的單向數(shù)據(jù)流(也稱為單向綁定)會保持模塊化和快速化淮阐。
如果在此步驟需要幫助叮阅,請參閱React文檔。
一個簡短的插曲:Props
vs State
React中有兩種類型的“模型”數(shù)據(jù):props和state泣特。 重要的是要了解兩者之間的區(qū)別;
如果你不確定有什么區(qū)別, 請參閱state文檔
第三步: 確定 UI state
的最泻评选(但完整)表示
要使你的UI交互,你需要能夠觸發(fā)對基礎(chǔ)數(shù)據(jù)模型的更改状您。 React的state
讓交互變得簡單勒叠。
為了正確構(gòu)建應(yīng)用,首先需要考慮應(yīng)用需要的最小的可變 state
數(shù)據(jù)模型集合。此處關(guān)鍵點在于精簡:不要存儲重復(fù)的數(shù)據(jù)膏孟。
構(gòu)造出絕對最小的滿足應(yīng)用需要的最小 state 是有必要的,并且計算出其它強(qiáng)烈需要的東西眯分。例如,如果構(gòu)建一個 TODO 列表,僅保存一個 TODO 列表項的數(shù)組,而不要保存另外一個指代數(shù)組長度的 state
變 量。當(dāng)想要渲染 TODO 列表項總數(shù)的時候,簡單地取出 TODO 列表項數(shù)組的長度就可以了柒桑。
示例程序中所有需要的的數(shù)據(jù)如下:
- 產(chǎn)品的原始列表 (The original list of products)
- 用戶在搜索框輸入的文字 (The search text the user has entered)
- 選擇框的值 (The value of the checkbox)
- 已過濾的產(chǎn)品列表 (The filtered list of products)
讓我們找出哪一個應(yīng)該是用state
管理弊决。 只需詢問每個數(shù)據(jù)的三個問題:
- 它是繼承而來的
props
嗎兼贡? 如果是咧栗,它應(yīng)該不是state
。 - 它是一直不變的嗎? 如果是郊丛,它應(yīng)該不是
state
界逛。 - 能通過其他的
state
或者props
計算而來嗎? 如果是昆稿,它應(yīng)該不是state
。
經(jīng)過分析, 原始的產(chǎn)品列表作為props
傳遞仇奶,所以不是state
貌嫡。 搜索文本和復(fù)選框似乎是state
,因為它們隨時間變化该溯,不能從任何計算岛抄。
最后,過濾的產(chǎn)品列表不是state
狈茉,因為它可以通過將原始產(chǎn)品列表與復(fù)選框的搜索文本和值組合來計算夫椭。
綜上, 我們的state只有兩項:
- 用戶在搜索框輸入的文字
- 選擇框的值
第四步: 確定state
的位置
var ProductCategoryRow = React.createClass({
render: function() {
return (<tr>
<th colSpan="2">{this.props.category}</th>
</tr>);
}
});
var ProductRow = React.createClass({
render: function() {
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>
);
}
});
var ProductTable = React.createClass({
render: function() {
var rows = [];
var lastCategory = null;
this.props.products.forEach(function(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;
}.bind(this));
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
});
var SearchBar = React.createClass({
render: function() {
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>
);
}
});
var FilterableProductTable = React.createClass({
getInitialState: function() {
return {
filterText: '',
inStockOnly: false
};
},
render: function() {
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,我們已經(jīng)確定了什么是最小的應(yīng)用state
集氯庆。 接下來蹭秋,我們需要確定哪個組件的state
會突變, 哪個組件應(yīng)該擁有此state
。
記椎棠臁:React的所有層次的內(nèi)容都是單向數(shù)據(jù)流傳輸仁讨。 可能不是立即清楚哪個組件應(yīng)該擁有什么state
。
對于新手來說实昨,這通常是最具挑戰(zhàn)性的部分洞豁,因此請按照以下步驟了解:
對于應(yīng)用中所有的state:
- 找出每一個基于那個
state
渲染界面的組件。 - 找出共同的祖先組件(某個單個的組件,在組件樹中位于需要這個
state
的所有組件的上面 - 要么是共同的祖先組件,要么是另外一個在組件樹中位于更高層級的組件應(yīng)該擁有這個
state
- 如果找不出擁有這個
state
數(shù)據(jù)模型的合適的組件,創(chuàng)建一個新的組件來維護(hù)這個state
,然后添加到組件樹中,層級位于所有共同擁有者組件的上面荒给。
讓我們根據(jù)上面的策略來確定示例程序的state
的位置:
-
ProductTable
需要根據(jù)狀態(tài)過濾產(chǎn)品列表丈挟,搜索欄需要顯示搜索文本和選中狀態(tài)。 - 公共所有者組件是
FilterableProductTable
- 過濾器文本(filter text)和檢查值(checked value)放在
FilterableProductTable
是可行的.
所以我們決定我們的state
放置在FilterableProductTable
志电。 首先曙咽,將getInitialState()
方法添加到FilterableProductTable
,返回{filterText:''挑辆,inStockOnly:false}
以反映應(yīng)用程序的初始狀態(tài)例朱。
然后,將filterText
和inStockOnly
傳遞給ProductTable
和SearchBar
作為props
之拨。 最后茉继,使用這些props
來過濾ProductTable
中的rows,并在SearchBar
中設(shè)置表單字段的值蚀乔。
你可以試著修改:將filterText
設(shè)置為“ball”
并刷新你的應(yīng)用程序烁竭。 您將看到數(shù)據(jù)表已正確更新。
第五步: 添加逆向數(shù)據(jù)流
var ProductCategoryRow = React.createClass({
render: function() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
});
var ProductRow = React.createClass({
render: function() {
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>
);
}
});
var ProductTable = React.createClass({
render: function() {
var rows = [];
var lastCategory = null;
this.props.products.forEach(function(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;
}.bind(this));
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
});
var SearchBar = React.createClass({
handleChange: function() {
this.props.onUserInput(
this.refs.filterTextInput.value,
this.refs.inStockOnlyInput.checked
);
},
render: function() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
ref="filterTextInput"
onChange={this.handleChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
ref="inStockOnlyInput"
onChange={this.handleChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
});
var FilterableProductTable = React.createClass({
getInitialState: function() {
return {
filterText: '',
inStockOnly: false
};
},
handleUserInput: function(filterText, inStockOnly) {
this.setState({
filterText: filterText,
inStockOnly: inStockOnly
});
},
render: function() {
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')
);
到目前為止吉挣,我們已經(jīng)構(gòu)建了一個應(yīng)用程序派撕,通過props
和state
沿著層次結(jié)構(gòu)向下的函數(shù)正確執(zhí)行。
現(xiàn)在是時候以其他方式支持?jǐn)?shù)據(jù)流:層次結(jié)構(gòu)中深層的表單form組件需要更新FilterableProductTable
中的state
睬魂。
React 讓這種數(shù)據(jù)流動非常明確,從而很容易理解應(yīng)用是如何工作的,但是相對于傳統(tǒng)的雙向數(shù)據(jù)綁定,確實需 要輸入更多的東西终吼。
React 提供了一個叫做 ReactLink
的插件來使其和雙向數(shù)據(jù)綁定一樣方便,但是考慮到這篇文章的目的,我們將會保持所有東西都直截了當(dāng)。
如果嘗試在當(dāng)前實例中鍵入或選中該框氯哮,您將看到React忽略您的輸入际跪。 這是有意的,因為我們已將輸入的值prop設(shè)置為始終等于從FilterableProductTable
傳遞的狀態(tài)。
我們要確保每當(dāng)用戶更改表單時姆打,我們更新狀態(tài)以反映用戶輸入良姆。 因為組件只應(yīng)該更新自己的state
狀態(tài),FilterableProductTable
將傳遞一個回調(diào)到SearchBar
幔戏,每當(dāng)狀態(tài)應(yīng)該更新時觸發(fā)玛追。
我們可以使用onChange
事件對輸入進(jìn)行通知。 并且FilterableProductTable
傳遞的回調(diào)將調(diào)用setState()
闲延,并且應(yīng)用程序?qū)⒈桓?/p>
雖然聽起來比較復(fù)雜痊剖,但是幾行代碼就能實現(xiàn)。而且他能讓我們更加明晰React
的數(shù)據(jù)流通方式垒玲。
后記
希望以上內(nèi)容讓你明白了如何思考用 React 去構(gòu)造組件和應(yīng)用陆馁。雖然可能比你之前要輸入更多的代碼,記住,讀代碼的時間遠(yuǎn)比寫代碼的時間多,并且閱讀這種模塊化的清晰的代碼是相當(dāng)容易的。
當(dāng)你開始構(gòu)建大型的組件庫 的時候,你將會非常感激這種清晰性和模塊化,并且隨著代碼的復(fù)用,整個項目代碼量就開始變少了