在我看來, React 是較早使用 JavaScript 構(gòu)建大型、快速的 Web 應(yīng)用程序的技術(shù)方案。它已經(jīng)被我們廣泛應(yīng)用于 Facebook 和 Instagram 。
React 眾多優(yōu)秀特征中的其中一部分就是,教會(huì)你去重新思考如何構(gòu)建應(yīng)用程序。
本文中,我將跟你一起使用 React 構(gòu)建一個(gè)具備搜索功能的產(chǎn)品列表。
注意:
如果你無法看到本頁內(nèi)嵌的代碼片段,請(qǐng)確認(rèn)你不是用
https
協(xié)議加載本頁的。
假設(shè)我們已經(jīng)擁有了一個(gè) JSON API 和設(shè)計(jì)師設(shè)計(jì)的原型。我們的設(shè)計(jì)師顯然不夠好,因?yàn)樵涂雌饋砣缦拢?/p>
http://wiki.jikexueyuan.com/project/react/images/thinking-in-react-mock.png" alt="Mockup" />
JSON接口返回?cái)?shù)據(jù)如下:
[
{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"}
];
你要做的第一件事是,為所有組件(及子組件)命名并畫上線框圖。假如你和設(shè)計(jì)師一起工作,也許他們已經(jīng)完成了這項(xiàng)工作,所以趕緊去跟他們溝通!他們的 Photoshop 圖層名也許最終可以直接用于你的 React 組件名。
然而你如何知道哪些才能成為組件?想象一下,當(dāng)你創(chuàng)建一些函數(shù)或?qū)ο髸r(shí),用到一些類似的技術(shù)。其中一項(xiàng)技術(shù)就是單一功能原則,指的是,理想狀態(tài)下一個(gè)組件應(yīng)該只做一件事,假如它功能逐漸變大就需要被拆分成更小的子組件。
由于你經(jīng)常需要將一個(gè)JSON數(shù)據(jù)模型展示給用戶,因此你需要檢查這個(gè)模型結(jié)構(gòu)是否正確以便你的 UI (在這里指組件結(jié)構(gòu))是否能夠正確的映射到這個(gè)模型上。這是因?yàn)橛脩艚缑婧蛿?shù)據(jù)模型在 信息構(gòu)造 方面都要一致,這意味著將你可以省下很多將 UI 分割成組件的麻煩事。你需要做的僅僅只是將數(shù)據(jù)模型分隔成一小塊一小塊的組件,以便它們都能夠表示成組件。
http://wiki.jikexueyuan.com/project/react/images/thinking-in-react-components.png" alt="Component diagram" />
由此可見,我們的 app 中包含五個(gè)組件。下面我已經(jīng)用斜體標(biāo)示出每個(gè)組件對(duì)應(yīng)的數(shù)據(jù)。
FilterableProductTable
(橘色): 包含整個(gè)例子的容器SearchBar
(藍(lán)色): 接受所有 用戶輸入( user input )ProductTable
(綠色): 根據(jù) 用戶輸入( user input ) 過濾和展示 數(shù)據(jù)集合( data collection )ProductCategoryRow
(青色): 為每個(gè) 分類( category ) 展示一列表頭ProductRow
(紅色): 為每個(gè) 產(chǎn)品( product ) 展示一列如果你仔細(xì)觀察 ProductTable
,你會(huì)發(fā)現(xiàn)表頭(包含“ Name ”和“ Price ”標(biāo)簽)并不是單獨(dú)的組件。這只是一種個(gè)人偏好,也有一定的爭(zhēng)論。在這個(gè)例子當(dāng)中,我把表頭當(dāng)做 ProductTable
的一部分,因?yàn)樗卿秩尽皵?shù)據(jù)集合”的一份子,這也是 ProductTable
的職責(zé)。但是,當(dāng)這個(gè)表頭變得復(fù)雜起來的時(shí)候(例如,添加排序功能),就應(yīng)該單獨(dú)地寫一個(gè) ProductTableHeader
組件。
既然我們?cè)谠彤?dāng)中定義了這個(gè)組件,讓我們把這些元素組成一棵樹形結(jié)構(gòu)。這很簡(jiǎn)單。被包含在其它組件中的組件在屬性機(jī)構(gòu)中應(yīng)該是子級(jí):
FilterableProductTable
SearchBar
-ProductTable
ProductCategoryRow
ProductRow
既然已經(jīng)擁有了組件樹,是時(shí)候開始實(shí)現(xiàn)應(yīng)用了。最簡(jiǎn)單的方式就是創(chuàng)建一個(gè)應(yīng)用,這個(gè)應(yīng)用將數(shù)據(jù)模型渲染到 UI 上,但是沒有交互功能。拆分這兩個(gè)過程是最簡(jiǎn)單的,因?yàn)闃?gòu)建一個(gè)靜態(tài)的版本僅需要大量的輸入,而不需要思考;但是添加交互功能卻需要大量的思考和少量的輸入。我們將會(huì)知道這是為什么。
為了創(chuàng)建一個(gè)渲染數(shù)據(jù)模型的應(yīng)用的靜態(tài)版本,你將會(huì)構(gòu)造一些組件,這些組件重用其它組件,并且通過 props 傳遞數(shù)據(jù)。 props 是一種從父級(jí)向子級(jí)傳遞數(shù)據(jù)的方式。如果你對(duì) state 概念熟悉,那么不要使用 state 來構(gòu)建這個(gè)靜態(tài)版本。 state 僅用于實(shí)現(xiàn)交互功能,也就是說,數(shù)據(jù)隨著時(shí)間變化。因?yàn)檫@是一個(gè)靜態(tài)的應(yīng)用版本,所以你并不需要 state 。
你可以從上至下或者從下至上來構(gòu)建應(yīng)用。也就是說,你可以從屬性結(jié)構(gòu)的頂部開始構(gòu)建這些組件(例如,從 FilterableProductTable
開始),或者從底部開始( ProductRow
)。在簡(jiǎn)單的應(yīng)用中,通常情況下從上至下的方式更加簡(jiǎn)單;在大型的項(xiàng)目中,從下至上的方式更加簡(jiǎn)單,這樣也可以在構(gòu)建的同時(shí)寫測(cè)試代碼。
在這步結(jié)束的時(shí)候,將會(huì)有一個(gè)可重用的組件庫來渲染數(shù)據(jù)模型。這些組件將會(huì)僅有 render()
方法,因?yàn)檫@是應(yīng)用的一個(gè)靜態(tài)版本。位于樹形結(jié)構(gòu)頂部的組件( FilterableProductTable
)將會(huì)使用數(shù)據(jù)模型作為 prop 。如果你改變底層數(shù)據(jù)模型,然后再次調(diào)用 React.render()
, UI 將會(huì)更新。查看 UI 如何被更新和什么地方改變都是很容易的,因?yàn)?React 的單向數(shù)據(jù)流(也被稱作“單向綁定”)保持了一切東西模塊化,很容易查錯(cuò),并且速度很快,沒有什么復(fù)雜的。
如果你在這步中需要幫助,請(qǐng)查看 React 文檔。
在 React 中有兩種類型的數(shù)據(jù)“模型”: props 和 state 。理解兩者的區(qū)別是很重要的;如果你不太確定兩者有什么區(qū)別,請(qǐng)大致瀏覽一下官方的 React 文檔。
為了使 UI 可交互,需要能夠觸發(fā)底層數(shù)據(jù)模型的變化。 React 通過 state 使這變得簡(jiǎn)單。
為了正確構(gòu)建應(yīng)用,首先需要考慮應(yīng)用需要的最小的可變 state 數(shù)據(jù)模型集合。此處關(guān)鍵點(diǎn)在于精簡(jiǎn):不要存儲(chǔ)重復(fù)的數(shù)據(jù)。構(gòu)造出絕對(duì)最小的滿足應(yīng)用需要的最小 state 是有必要的,并且計(jì)算出其它強(qiáng)烈需要的東西。例如,如果構(gòu)建一個(gè) TODO 列表,僅保存一個(gè) TODO 列表項(xiàng)的數(shù)組,而不要保存另外一個(gè)指代數(shù)組長度的 state 變量。當(dāng)想要渲染 TODO 列表項(xiàng)總數(shù)的時(shí)候,簡(jiǎn)單地取出 TODO 列表項(xiàng)數(shù)組的長度就可以了。
思考示例應(yīng)用中的所有數(shù)據(jù)片段,有:
讓我們分析每一項(xiàng),指出哪一個(gè)是 state 。簡(jiǎn)單地對(duì)每一項(xiàng)數(shù)據(jù)提出三個(gè)問題:
初始的 products 列表通過 props 傳入,所以不是 state 。搜索文本和復(fù)選框看起來像是 state ,因?yàn)樗鼈冸S著時(shí)間改變,也不能根據(jù)其它數(shù)據(jù)計(jì)算出來。最后,過濾的 products 列表不是 state ,因?yàn)榭梢酝ㄟ^搜索文本和復(fù)選框的值從初始的 products 列表計(jì)算出來。
所以最終, state 是:
OK,我們辨別出了應(yīng)用的 state 數(shù)據(jù)模型的最小集合。接下來,需要指出哪個(gè)組件會(huì)改變或者說擁有這個(gè) state 數(shù)據(jù)模型。
記?。?React 中數(shù)據(jù)是沿著組件樹從上到下單向流動(dòng)的??赡懿粫?huì)立刻明白哪個(gè)組件應(yīng)該擁有哪些 state 數(shù)據(jù)模型。這對(duì)新手通常是最難理解和最具挑戰(zhàn)的,因此跟隨以下步驟來弄清楚這點(diǎn):
對(duì)于應(yīng)用中的每一個(gè) state 數(shù)據(jù):
讓我們?cè)趹?yīng)用中應(yīng)用這個(gè)策略:
ProductTable
需要基于 state 過濾產(chǎn)品列表,SearchBar
需要顯示搜索文本和復(fù)選框狀態(tài)。FilterableProductTable
。FilterableProductTable
中是合適的。太酷了,我們決定了 state 數(shù)據(jù)模型位于 FilterableProductTable
之中。首先,給 FilterableProductTable
添加 getInitialState()
方法,該方法返回 {filterText: '', inStockOnly: false}
來反映應(yīng)用的初始化狀態(tài)。然后傳遞 filterText
和 inStockOnly
給 ProductTable
和 SearchBar
作為 prop 。最后,使用這些 props 來過濾 ProductTable
中的行,設(shè)置在 SearchBar
中表單字段的值。
你可以開始觀察應(yīng)用將會(huì)如何運(yùn)行:設(shè)置 filterText
為 "ball"
,然后刷新應(yīng)用。將會(huì)看到數(shù)據(jù)表格被正確更新了。
到目前為止,已經(jīng)構(gòu)建了渲染正確的基于 props 和 state 的沿著組件樹從上至下單向數(shù)據(jù)流動(dòng)的應(yīng)用?,F(xiàn)在,是時(shí)候支持另外一種數(shù)據(jù)流動(dòng)方式了:組件樹中層級(jí)很深的表單組件需要更新 FilterableProductTable
中的 state 。
React 讓這種數(shù)據(jù)流動(dòng)非常明確,從而很容易理解應(yīng)用是如何工作的,但是相對(duì)于傳統(tǒng)的雙向數(shù)據(jù)綁定,確實(shí)需要輸入更多的東西。 React 提供了一個(gè)叫做 ReactLink
的插件來使其和雙向數(shù)據(jù)綁定一樣方便,但是考慮到這篇文章的目的,我們將會(huì)保持所有東西都直截了當(dāng)。
如果你嘗試在示例的當(dāng)前版本中輸入或者選中復(fù)選框,將會(huì)發(fā)現(xiàn) React 會(huì)忽略你的輸入。這是有意的,因?yàn)橐呀?jīng)設(shè)置了 input
的 value
屬性,使其總是與從 FilterableProductTable
傳遞過來的 state
一致。
讓我們思考下我們希望發(fā)生什么。我們想確保無論何時(shí)用戶改變了表單,都要更新 state 來反映用戶的輸入。由于組件只能更新自己的 state , FilterableProductTable
將會(huì)傳遞一個(gè)回調(diào)函數(shù)給 SearchBar
,此函數(shù)將會(huì)在 state 應(yīng)該被改變的時(shí)候觸發(fā)。我們可以使用 input 的 onChange
事件來監(jiān)聽用戶輸入,從而確定何時(shí)觸發(fā)回調(diào)函數(shù)。 FilterableProductTable
傳遞的回調(diào)函數(shù)將會(huì)調(diào)用 setState()
,然后應(yīng)用將會(huì)被更新。
雖然這聽起來有很多內(nèi)容,但是實(shí)際上僅僅需要幾行代碼。并且關(guān)于數(shù)據(jù)在應(yīng)用中如何流動(dòng)真的非常清晰明確。
希望以上內(nèi)容讓你明白了如何思考用 React 去構(gòu)造組件和應(yīng)用。雖然可能比你之前要輸入更多的代碼,記住,讀代碼的時(shí)間遠(yuǎn)比寫代碼的時(shí)間多,并且閱讀這種模塊化的清晰的代碼是相當(dāng)容易的。當(dāng)你開始構(gòu)建大型的組件庫的時(shí)候,你將會(huì)非常感激這種清晰性和模塊化,并且隨著代碼的復(fù)用,整個(gè)項(xiàng)目代碼量就開始變少了 :)。