編者按
使用React的思想來構建應用對我在實際項目中以及幫助他人解決實際問題時起到了很大作用,所以我翻譯此文來向那些正在或即將陷入React或React-Native深坑的同胞們表示慰問。網上已經有人翻譯過,我想用更易讀的語言翻譯一次,這也是我首次如此一本正經的翻譯技術文章給大眾閱讀,權當練習吧。
原文地址:https://facebook.github.io/react/docs/thinking-in-react.html
轉載還請注明出處以及原文地址,出於對作者和譯者勞動成果的尊重吧,謝謝了我的哥。
Thinking in React
作者:Pete Hunt 譯者:Rex Rao (sohobloo)
我認為React是使用JavaScript構建高性能大型Web應用的首選方案,我們已經在Facebook和Instagram中廣泛使用,哎喲,效果不錯喲。
React的眾多優勢之一是——且看它如何讓你能順着思路構建應用。在此,我將引領你用React逐步構建出一個可搜索的商品列表應用。
從模型圖開始
假設設計師已經為我們提供了API並可以返回模擬的JSON數據。容我小小鄙視一下這位美工,因為原型圖長成這個挫樣:
我們的API返回的模擬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"} ];
第一步:拆分並構建界面的組件層次結構樹
你應該做的第一件事是為你原型圖所有組件和子組件畫個邊框、起個名。要是你跟設計師坐一起,找他們喝喝茶,說不定他們的Photoshop圖層名恰巧可以用作你React組件的名字!(譯者:我只能說,Too young too simple, sometimes naive!)
但你怎么知道如何拆分一個組件呢?這和你平時決定是不是要新建一個函數或者類的道理一樣一樣的。其中有個叫做單一職責原則的原理,也就是說理想狀態下一個組件只做一件事,當他需要做更多,那就應該繼續拆拆拆。
如果你經常向用戶展示JSON數據,你會發現只要你的數據模型建得好,你的界面乃至你的組件架構也會完美的與之映射。因為界面和數據模型傾向於支持相同的信息架構,這讓界面拆分工作變簡單了,拆分出的一個組件只對應展示數據模型中的一種數據就行。
你看,咱這簡單的應用有5種組件。我用斜體標示出了每個組件要展示的數據。
FilterableProductTable
(橙色): 包含整個示例SearchBar
(藍色): 接收用戶輸入(user input)ProductTable
(綠色): 顯示基於用戶輸入(user input)過濾的數據集 (data collection)ProductCategoryRow
(青色): 顯示分類( category)頭ProductRow
(紅色): 顯示每一行商品(product)
看ProductTable你會發現表頭(含"Name"和"Price"標簽)並沒有拆分成組件
,這是出於一種存在爭議的個人喜好而已啦。這個例子中,既然渲染數據集 (data collection)是ProductTable的職責,那就讓它作為此組件的一部分好了。要是它再復雜一點的話(比如排序功能),那就另當別論獨立成
ProductTableHeader
組件咯。
讓我們把從原型圖中定義的組件組合成層次結構樹。如果一個組件出現在另一個組件中,那么這個組件就是它的子組件,so easy:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
第二步:用React做個靜態版

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') );
有了組件層次結構,是時候表演真正的技術了實現你的應用了。最簡單的方式是把數據渲染到界面上,但是不帶交互功能。最好是分離這些步驟,因為構建一個靜態版本更多是需要你敲鍵盤而增加交互功能就需要你敲腦袋了。
將你的應用構建出一個靜態版本來展示數據模型,你也許會需要構建組件來復用其他組件,用屬性(props)傳入數據。 屬性(props)是一種將數據從父組件傳入子組件的途徑。 即便你對狀態(state)模式非常熟悉,在靜態版本中也不要使用狀態哦。狀態是留給交互來處理那些會變化的數據使用的。作為一個靜態版請無視之。
你可以用自上而下或自下而上的方式構建應用。既可以從最頂層組件開始(比如從FilterableProductTable開始)也可以從最底層組件開始(如ProductRow)。在簡單的示例中,自上而下往往更容易;而在大型項目中,使用自下而上更好,你還能方便的寫單元測試呢!
這一步完成之后,你就有了一個可以展示你的數據模型的組件庫。作為一個靜態版本,每個組件都只有一個 render()方法。頂層組件(FilterableProductTable)通過屬性(prop)獲得你的數據模型。如果此時你改變你的基礎數據模型並再次調用ReactDOM.render(),界面會刷新。界面的刷新和變化了一目了然直接了當。React的單向數據流(又名單向綁定)讓所有事情有序且快速。
如果在這一步中遇到問題,你可以參考React文檔。
小插曲兒: 屬性(props)與狀態(state)
React中有兩類數據模型:屬性和狀態,了解他們的區別是很有必要的!還不太清楚?來來來看這里React官方文檔咋說的。
第三步:定義最小(完整)的界面狀態值
界面想要動起來?數據必須變起來!React使用狀態(state)來實現。
若想正確構建你的應用,首先你得考慮你的應用至少需要一組什么樣的可變狀態值。來跟我念口訣:取其精華,去其糟粕。找出你的應用的那組干貨——絕對最小化的界面狀態值組,並且其他任何需要都可以通這組值計算得出。比如你要構建一個待辦列表,只需要維護一組待辦項即可;你不需要再維護這組列表的個數的值,因為在你需要展示待辦數時可以直接獲取列表長度得到結果。
來看看我們例子里有哪些數據:
- 原始的商品列表
- 用戶輸入的搜索文本
- 勾選框的值
- 篩選后的商品列表
讓我們逐條看看哪些是狀態,對每一條數據三省吾身:
- 是否是父組件傳入的屬性?如果是的,估計不是狀態。
- 是否會隨時間變化改變?如果不會變,估計不是狀態。
- 能否從其他狀態或屬性計算得到?如果可以,肯定不是狀態。
原始商品列表通過屬性傳進來,因此它不是狀態。搜索框的值和勾選框的值可以改變而且其他東西也計算不出來這些值,看上去應該是狀態。最后,篩選后的商品列表也不是狀態,因為它可以通過原始商品列表、搜索框的值和勾選框的值計算得出。
最后得出我們需要的狀態:
- 用戶輸入的輸入框的值
- 勾選框的值
第四步:給狀態找個家
最小狀態集新鮮出爐,接下來我們需要定義哪些組件會變化,或者說擁有這些狀態。
記住了: React數據總是單向且「下流」的——流向組件層次中的底層。可能並不是一開始就看得出哪個組件擁有什么狀態。這常常是萌新最難理解的部分,所以就讓老司機帶帶你吧:
對於你應用的每一條狀態:
- 找出每一個需要基於此狀態來渲染界面的組件。
- 找到它們共同的爹(一個在組件層次中需要此狀態的所有控件的頂層父組件)。
- 它們共同的父組件或更高層級的組件都可以作為狀態的持有者。
- 如果你覺得哪個組件持有這個狀態都很別扭,可以為了這個狀態創造一個新的組件來持有,並把這個新組件加到它們共同父組件的上層結構中的任何合適位置。
針對我們的應用,讓我們根據以上策略捋一捋:
ProductTable
需要根據狀態值來過濾商品列表,SearchBar
需要顯示搜索文本和勾選框狀態值。FilterableProductTable
是它們的共同父組件。- 看起來搜索文本和勾選框值放在
FilterableProductTable
挺合適。
就這么愉快的決定了,把這些狀態放FilterableProductTable里吧。
首先在FilterableProductTable中增加
getInitialState()(譯者:ES6中如果用class構建組件,初始化狀態的方法將發生改變)
方法並返回{filterText: '', inStockOnly: false}
來對應應用的初始狀態。然后將filterText和
inStockOnly作為屬性傳給
ProductTable
和SearchBar
。最后就用屬性來過濾ProductTable中的商品列表並把搜索文本設置到SearchBar的輸入框中。
來看看你應用的表現如何:把filterText
設置成"ball"
然后刷新。厲害了我的哥,列表正確的更新了!
第五步:增加反向數據流
至此,我們已經構建了一個能正確渲染屬性和狀態從組件層次自上而下傳遞的應用了。是時候表演真正的技術了支持數據反向傳遞了:底層組件需要更新FilterableProductTable里的狀態。
React明確的數據傳遞能讓你更容易搞清楚你的程序是怎么運作的,但比起傳統的雙向數據綁定你就需要敲稍微多一點的代碼了。 React提供了一個叫ReactLink的插件來讓這種模式變得和雙向綁定一樣方便,但本文的目的在於讓一切明晰,暫不使用。
如果你嘗試在當前版本的示例中輸入或勾選,你會發現React完全無視你的輸入。 怎么回事難道有Bug?乖乖我們故意的!因為我們剛才把input的
value
屬性設置成總是等於FilterableProductTable傳進來的狀態了。
然並卵,我們需要用戶的輸入立刻更新狀態。既然控件只允許更新自己的狀態,FilterableProductTable可以
傳一個每次狀態需要發生變化時都會觸發的回調函數回傳到SearchBar。我們可以用輸入框的
onChange事件來觸發並在
FilterableProductTable傳入的
回調函數中調用setState()來更新狀態。
看上去好像很復雜的樣子,其實只是多了幾行代碼而已,但這真真真的讓你能看清數據是如何在你應用的身體里流來流去的。
沒錯就是這樣
希望這篇文章能在你用React構建組件或應用時給你點亮一盞明燈。雖然可能比以前要搬更多磚,但請你記住代碼寫出來是要可以給人閱讀的,特別是那些標准統一、邏輯清晰的代碼更賞心悅目。當你開始構建大型的控件庫的時候,你會感激這種規則化、清晰化的風格,再加上代碼的復用,你的代碼行數會得到縮減。☺