如何構建React組件?
編程是一項非常復雜的工程,尤其要編寫干凈整潔的代碼更為困難。我們需要考慮很多問題—變量命名、函數作用域、異常處理、安全保障、性能監控等等。在編程中,變量命名唯一還是一件比較困難的事情,我傾向于編寫松散耦合且高度聚合的組件。如果從面向對象或者函數式編程的角度來說,也會遇到同樣的問題。構建體系是我認為最難的事情,因為它將影響到整個項目。想要成為能熟練設計軟件架構的人,需要經過多年磨礪和積累。(也許沒有人能完全掌握它—在如此快速發展的行業中,總有人走在前面,也總有一個方法可以提高軟件設計)。
我非常喜歡使用React,因為我覺得它最大優點就是足夠簡單。 在簡單和容易之間還是存在區別 的,我的意思是React也很簡單。當然你需要些時間來了解它,當你掌握其核心內容后,其他的事都是水到渠成的了。下文將介紹比較難的部分。
耦合&內聚
這些指標(耦合&內聚)或多或少的給我們改變編程習慣帶了挑戰。它們經常被運用在類形式的面向對象編程中。我們也將參考并且運用同樣的規則在編寫React組件上。
耦合指元素之間的相互連接和依賴關系。如果你改變一個元素需要同步的更新另外一個元素,我們稱此為緊密耦合。而松散耦合指的是改變一個元素時,并不需要改變另外一個元素。舉個例子,顯示銀行轉賬金額功能。如果展示金額依賴于匯率計算,那么內部轉換結構更改時,展示的代碼也會被更新。如果我們設計基于一個元素接口的,松散耦合的系統,這樣元素的改變并不會影響視圖層展示。很明顯,松散耦合的組件更易于管理和控制。
內聚則是組件是否只為一件事情負責。這個指標是依循單一原則和unix原則:專注一件事情并且做好這件事。如果賬戶余額格式化展示需要計算相關匯率和檢查是否有查閱歷史權限,那么這個包含很多功能職責,而這些功能并不相互依賴。也許,權限處理和匯率應該是不同的組件。另一方面,如果有多個組件,一個用于整數部分,一個用于小數部分,另外一個用于貨幣展示,程序員想展示余額,他們則需要找到所有組件進行組裝。其中的挑戰則是創造高度內聚的組件。
構建組件
創建組件有很多種方法。我們希望在合理程度下組件是可以被重用的 。我們也希望構建的小組件可以被用在更大的組件中。理想情況下,我們想構建松散耦合和高度聚合的組件,這樣我們的系統更有利于管理和擴展。在React組件中props 類似函數中的參數,它們也可以看做是無狀態功能的組件。我們需要思考在組件中如何定義props和組件如何被重用。
接下來,我們以費用管理系統為背景,分析費用詳細的格式,來介紹如果構建組件:
type Expense {
description: string
category: string
amount: number
doneAt: moment
}
根據模型,會有以下幾種對費用格式的程序建模方式:
- 無props
- 傳遞一個expense對象
- 傳遞必要的屬性
- 傳遞所有屬性的map
- 傳遞一個格式化的子對象
下面分別討論使用上述傳遞方式的優缺點。不過需要時刻關注使用以上任何方式是要看使用場景和依賴系統的。這也是我們所做的,建立適當的抽象場景。
無props
這是最簡單的解決辦法,往往就是構建一個寫靜態數據的組件。
const ExpenseDetails = () => (
<div className='expense-details'>
<div>Category: <span>Food</span></div>
<div>Description: <span>Lunch</span></div>
<div>Amount: <span>10.15</span></div>
<div>Date: <span>2017-10-12</span></div>
</div>
)
不傳遞props,就不會給我們帶來任何靈活性,而且組件也只能用于單一的場景。在費用明細的例子中,我們可以看到,最初組件是需要接受一些props。不過在某些場景下,沒有任何props也是一個好的解決方式。首先,我們可以使用一些組件,其props的內容是一些不會輕易更改的內容,比如:商標,logo或者公司信息等。
const Logo = () => (
<div className='logo'>
<img src='/logo.png' alt='DayOne logo'/>
</div>
)
編寫盡可能小的組件使得系統更加容易維護。保持信息只保存在一處而且只需要在一處進行修改。不要在多處寫重復的代碼。
傳遞expense對象
在費用明細確定情況下,我們需要給組件傳遞數據。首先,我們需要傳遞一個expense對象。
const ExpenseDetails = ({ expense }) => (
<div className='expense-details'>
<div>Category: <span>{expense.category}</span></div>
<div>Description: <span>{expense.description}</span></div>
<div>Amount: <span>{expense.amount}</span></div>
<div>Date: <span>{expense.doneAt}</span></div>
</div>
)
傳遞費用對象給費用明細的組件是非常有意義的。費用明細的格式是高度一致的,它顯示費用的數據。無論什么時候需要改變格式,這是唯一可以修改的地方。改變費用明細的格式也不會對費用對象自身帶來什么副作用。
這個組件是和費用對象緊密耦合,這是一個壞的事情嗎?當然不是,但是我們必須意識到這是如何影響我們系統的。 傳遞一個對象作為props,費用明細的組件將會依賴費用內部結構。當我們改變費用的內部結構時候,我們將需要更改費用明細組件。當然,我們只需要在一處進行修改。
這樣的設計如何適應未來的改變呢? 如果我們增加、改變或者刪除一個字段,我們將只需改變一個組件。如果我們需要增加一個其他的日歷格式化展示?我們可以為日歷格式化增加一個新的prop。
const ExpenseDetails = ({ expense, dateFormat }) => (
<div className='expense-details'>
<div>Category: <span>{expense.category}</span></div>
<div>Description: <span>{expense.description}</span></div>
<div>Amount: <span>{expense.amount}</span></div>
<div>Date: <span>{expense.doneAt.format(dateFormat)}</span></div>
</div>
)
我們開始增加屬性來使組件更加靈活。如果只有幾個選項,那么一切都是很ok的。系統業務開始擴展后問題就來了,在不同的場景下我們需要維護大量的props。
const ExpenseDetails = ({ expense, dateFormat, withCurrency, currencyFormat, isOverdue, isPaid ... })
增加props可以使得組件重用性更好,但你可能設計了多重功能職責的組件。這種規則也同樣在函數寫法中運用。可以用多個參數來創建一個函數,當參數的數目超過3個或者4個時候,意味著這個函數在做很多事情了,也許這時候應該將函數拆成更小的函數來的更加簡單。
隨著組件props的增加,我們將其拆分成定義明確的組件,比如:OverdueExpenseDetails, PaidExpenseDetails等。
只傳遞必要的屬性
為了減少對象自身的內容,我們可以只傳遞必要的屬性值。
const ExpenseDetails = ({ category, description, amount, date }) => (
<div className='expense-details'>
<div>Category: <span>{category}</span></div>
<div>Description: <span>{description}</span></div>
<div>Amount: <span>{amount}</span></div>
<div>Date: <span>{date}</span></div>
</div>
)
我們分別傳遞屬性,這樣我們將一部分工作責任轉移給了組件使用者。如果費用的內部結構發生變化,他將不會影響費用明細的格式化。但可能影響每個使用組件的地方,因為我們可能需要修改props。當我們以獨立的屬性傳遞props時候,一個組件將更加抽象了。
只傳遞需要的字段對未來設計改動是如何影響的?增加、更新或者刪除字段將不會很容易。無論何時我們要增加一個字段,我們不僅僅要改變費用細節的實現,也需要改變每個使用組件的地方。另外一個方面,支持多種日歷格式化幾乎是現成的,我們可以傳遞日歷作為prop,也可以傳遞格式化后的日歷。
<ExpenseDetails
category={expense.category}
description={expense.description}
amount={expense.amount}
date={expense.doneAt.format('YYYY-MM-DD')}
/>
決定如何展示特定的字段應該在掌握在具體使用組件的人手中,這將不是費用明細組件關心的內容。
傳遞map或者array的屬性
為了達到組件抽象化,我們可以傳遞一個map的屬性值。
const ExpenseDetails = ({ expense }) => (
<div class='expense-details'>
{
_.reduce(expense, (acc, value, key) => {
acc.push(<div>{key}<span>{value}</span></div>)
}, [])
}
</div>
)
使用組件的人控制費用明細的格式化,傳遞給組件的對象格式則必須正確。
const expense = {
"Category": "Food",
"Description": "Lunch",
"Amount": 10.15,
"Date": 2017-10-12
}
這個方案有很多缺陷,我們很難控制組件展示的樣式,并且展示順序也沒有指定。因此,如果我們需要某種順序的話,可以采用array代替map來解決這個問題。但是仍然還有缺陷。
傳遞map 和array作為props 不與費用耦合,也根本與它不一致。增加和刪除新屬性雖然只改變了prop,但是我們無法控制組件本身的格式。如果我們只改變類別的格式化,這不是一個可行的辦法。(確切地說,總有一個辦法來解決,例如,傳遞另外一個格式化后的props。這個解決方案似乎不再簡單了。)
傳遞一個格式化的子對象
我們也可以只通過直接傳對一個子對象,這樣就能考慮更少的組件內需要如何展示。
const ExpenseDetails = ({ children }) => (
<div class='expense-details'>
{ children }
</div>
)
在這種情況下,費用明細只是一個提供結構和樣式的容器。展示所有信息則是使用組件的人必須提供的。
<ExpenseDetails>
<div>Category: <span>{expense.category}</span></div>
<div>Description: <span>{expense.description}</span></div>
<div>Amount: <span>{expense.amount}</span></div>
<div>Date: <span>{expense.doneAt}</span></div>
</ExpenseDetails>
在費用明細這個案例中,我們需要重復許多工作,因此這也許不是一個好的解決方案。盡管如此,靈活性則是巨大的,因為有可能有很多不同的格式化操作。增刪改只需要改變使用組件時候傳入的值。日期格式也是一樣的,我們雖然失去了功能內聚的特點,但這也是我們不得不付出的代價。
環境為王
正如你所看到,我們討論了它們的不同優缺點和可能性。哪一個最好呢,這取決于:
- 項目本身
- 項目階段
- 組件自身,需要很多特殊的組件組成還是只需要簡單的一些選項值
- 自己的習慣
- 使用環境-適合頻繁的改變和被多次使用
沒有一個萬能的解決方案,一個方案也并不能適用所有場景。我們如何構建組件對于系統的維護和系統可擴展方面有著深遠的影響。這完全依賴于組件所使用的環境。非常幸運的是,我們有很多可使用的方案。組件是功能的抽象集合,它既能構建小系統也能構建大系統。所以這僅僅只是一個選擇問題。
來自:https://jdc.jd.com/archives/211843