我喜歡React組件式開發(fā)方式。你可以將復(fù)雜的用戶界面分割為一個(gè)個(gè)組件,利用組件的可重用性和抽象的DOM操作。
讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來(lái)自于我們對(duì)這個(gè)行業(yè)的熱愛(ài)。我們立志把好的技術(shù)通過(guò)有效、簡(jiǎn)單的方式提供給客戶,將通過(guò)不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長(zhǎng)期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名注冊(cè)、虛擬主機(jī)、營(yíng)銷軟件、網(wǎng)站建設(shè)、新邱網(wǎng)站維護(hù)、網(wǎng)站推廣。
基于組件的開發(fā)是高效的:一個(gè)復(fù)雜的系統(tǒng)是由專門的、易于管理的組件構(gòu)建的。然而,只有設(shè)計(jì)良好的組件才能確保組合和復(fù)用的好處。
盡管應(yīng)用程序很復(fù)雜,但為了滿足最后期限和意外變化的需求,你必須不斷地走在架構(gòu)正確性的細(xì)線上。你必須將組件分離為專注于單個(gè)任務(wù),并經(jīng)過(guò)良好測(cè)試。
不幸的是,遵循錯(cuò)誤的路徑總是更加容易:編寫具有許多職責(zé)的大型組件、緊密耦合組件、忘記單元測(cè)試。這些增加了技術(shù)債務(wù),使得修改現(xiàn)有功能或創(chuàng)建新功能變得越來(lái)越困難。
編寫React應(yīng)用程序時(shí),我經(jīng)常問(wèn)自己:
如何正確構(gòu)造組件?
在什么時(shí)候,一個(gè)大的組件應(yīng)該拆分成更小的組件?
如何設(shè)計(jì)防止緊密耦合的組件之間的通信?
幸運(yùn)的是,可靠的組件具有共同的特性。讓我們來(lái)研究這7個(gè)有用的標(biāo)準(zhǔn)(本文只闡述 SRP,剩余準(zhǔn)則正在途中),并將其詳細(xì)到案例研究中。
單一職責(zé)
當(dāng)一個(gè)組件只有一個(gè)改變的原因時(shí),它有一個(gè)單一的職責(zé)。
編寫React組件時(shí)要考慮的基本準(zhǔn)則是單一職責(zé)原則。單一職責(zé)原則(縮寫:SRP)要求組件有一個(gè)且只有一個(gè)變更的原因。
組件的職責(zé)可以是呈現(xiàn)列表,或者顯示日期選擇器,或者發(fā)出 HTTP 請(qǐng)求,或者繪制圖表,或者延遲加載圖像等。你的組件應(yīng)該只選擇一個(gè)職責(zé)并實(shí)現(xiàn)它。當(dāng)你修改組件實(shí)現(xiàn)其職責(zé)的方式(例如,更改渲染的列表的數(shù)量限制),它有一個(gè)更改的原因。
為什么只有一個(gè)理由可以改變很重要?因?yàn)檫@樣組件的修改隔離并且受控。單一職責(zé)原則制了組件的大小,使其集中在一件事情上。集中在一件事情上的組件便于編碼、修改、重用和測(cè)試。
下面我們來(lái)舉幾個(gè)例子
實(shí)例1:一個(gè)組件獲取遠(yuǎn)程數(shù)據(jù),相應(yīng)地,當(dāng)獲取邏輯更改時(shí),它有一個(gè)更改的原因。
發(fā)生變化的原因是:
修改服務(wù)器URL
修改響應(yīng)格式
要使用其他HTTP請(qǐng)求庫(kù)
或僅與獲取邏輯相關(guān)的任何修改。
示例2:表組件將數(shù)據(jù)數(shù)組映射到行組件列表,因此在映射邏輯更改時(shí)有一個(gè)原因需要更改。
發(fā)生變化的原因是:
你需要限制渲染行組件的數(shù)量(例如,最多顯示25行)
當(dāng)沒(méi)有要顯示的項(xiàng)目時(shí),要求顯示提示信息“列表為空”
或僅與數(shù)組到行組件的映射相關(guān)的任何修改。
你的組件有很多職責(zé)嗎?如果答案是“是”,則按每個(gè)單獨(dú)的職責(zé)將組件分成若干塊。
如果您發(fā)現(xiàn)SRP有點(diǎn)模糊,請(qǐng)閱讀本文。 在項(xiàng)目早期階段編寫的單元將經(jīng)常更改,直到達(dá)到發(fā)布階段。這些更改通常要求組件在隔離狀態(tài)下易于修改:這也是 SRP 的目標(biāo)。
1.1 多重職責(zé)陷阱
當(dāng)一個(gè)組件有多個(gè)職責(zé)時(shí),就會(huì)發(fā)生一個(gè)常見的問(wèn)題。乍一看,這種做法似乎是無(wú)害的,并且工作量較少:
你立即開始編碼:無(wú)需識(shí)別職責(zé)并相應(yīng)地規(guī)劃結(jié)構(gòu)
一個(gè)大的組件可以做到這一切:不需要為每個(gè)職責(zé)創(chuàng)建組成部分
無(wú)拆分-無(wú)開銷:無(wú)需為拆分組件之間的通信創(chuàng)建 props 和 callbacks
這種幼稚的結(jié)構(gòu)在開始時(shí)很容易編碼。但是隨著應(yīng)用程序的增加和變得復(fù)雜,在以后的修改中會(huì)出現(xiàn)困難。同時(shí)實(shí)現(xiàn)多個(gè)職責(zé)的組件有許多更改的原因?,F(xiàn)在出現(xiàn)的主要問(wèn)題是:出于某種原因更改組件會(huì)無(wú)意中影響同一組件實(shí)現(xiàn)的其它職責(zé)。
不要關(guān)閉電燈開關(guān),因?yàn)樗瑯幼饔糜陔娞荨?/p>
這種設(shè)計(jì)很脆弱。意外的副作用是很難預(yù)測(cè)和控制的。
例如,
當(dāng)你更改表單字段(例如,將 修改為
解決多重責(zé)任問(wèn)題需要將
多重責(zé)任問(wèn)題的最壞情況是所謂的上帝組件(上帝對(duì)象的類比)。上帝組件傾向于了解并做所有事情。你可能會(huì)看到它名為
在組合的幫助下使其符合SRP,從而分解上帝組件。(組合(composition)是一種通過(guò)將各組件聯(lián)合在一起以創(chuàng)建更大組件的方式。組合是 React 的核心。)
1.2 案例研究:使組件只有一個(gè)職責(zé)
設(shè)想一個(gè)組件向一個(gè)專門的服務(wù)器發(fā)出 HTTP 請(qǐng)求,以獲取當(dāng)前天氣。成功獲取數(shù)據(jù)時(shí),該組件使用響應(yīng)來(lái)展示天氣信息:
import?axios?from?'axios'; //?問(wèn)題:?一個(gè)組件有多個(gè)職責(zé) class?Weather?extends?Component?{ ?constructor(props)?{ ?super(props); ?this.state?=?{?temperature:?'N/A',?windSpeed:?'N/A'?}; ?} ?render()?{ ?const?{?temperature,?windSpeed?}?=?this.state; ?return?( ???); ?} ?componentDidMount()?{ ?axios.get('http://weather.com/api').then(function?(response)?{ ?const?{?current?}?=?response.data; ?this.setState({ ?temperature:?current.temperature, ?windSpeed:?current.windSpeed ?}) ?}); ?} } 復(fù)制代碼Temperature:?{temperature}°C?Wind:?{windSpeed}km/h?
在處理類似的情況時(shí),問(wèn)問(wèn)自己:是否必須將組件拆分為更小的組件?通過(guò)確定組件可能會(huì)如何根據(jù)其職責(zé)進(jìn)行更改,可以最好地回答這個(gè)問(wèn)題。
這個(gè)天氣組件有兩個(gè)改變?cè)颍?/p>
componentDidMount() 中的 fetch 邏輯:服務(wù)器URL或響應(yīng)格式可能會(huì)改變。
render() 中的天氣展示:組件顯示天氣的方式可以多次更改。
解決方案是將
import?axios?from?'axios'; //?解決措施:?組件只有一個(gè)職責(zé)就是請(qǐng)求數(shù)據(jù) class?WeatherFetch?extends?Component?{ ?constructor(props)?{ ?super(props); ?this.state?=?{?temperature:?'N/A',?windSpeed:?'N/A'?}; ?} ?render()?{ ?const?{?temperature,?windSpeed?}?=?this.state; ?return?( ??); ?} ?componentDidMount()?{ ?axios.get('http://weather.com/api').then(function?(response)?{ ?const?{?current?}?=?response.data; ?this.setState({ ?temperature:?current.temperature, ?windSpeed:?current.windSpeed ?}); ?}); ?} } 復(fù)制代碼
這種結(jié)構(gòu)有什么好處?
例如,你想要使用 async/await 語(yǔ)法來(lái)替代 promise 去服務(wù)器獲取響應(yīng)。更改原因:修改獲取邏輯
//?改變?cè)??使用?async/await?語(yǔ)法 class?WeatherFetch?extends?Component?{ ?//?.....?// ?async?componentDidMount()?{ ?const?response?=?await?axios.get('http://weather.com/api'); ?const?{?current?}?=?response.data; ?this.setState({ ?temperature:?current.temperature, ?windSpeed:?current.windSpeed ?}); ?} } 復(fù)制代碼
因?yàn)?
//?解決方案:?組件只有一個(gè)職責(zé),就是顯示天氣 function?WeatherInfo({?temperature,?windSpeed?})?{ ?return?( ???); } 復(fù)制代碼Temperature:?{temperature}°C?Wind:?{windSpeed}?km/h?
讓我們更改
//?改變?cè)??無(wú)風(fēng)時(shí)的顯示 function?WeatherInfo({?temperature,?windSpeed?})?{ ?const?windInfo?=?windSpeed?===?0???'calm'?:?`${windSpeed}?km/h`; ?return?( ???); } 復(fù)制代碼Temperature:?{temperature}°C?Wind:?{windInfo}?
同樣,對(duì)
1.3 案例研究:HOC 偏好單一責(zé)任原則
按職責(zé)使用分塊組件的組合并不總是有助于遵循單一責(zé)任原則。另外一種有效實(shí)踐是高階組件(縮寫為 HOC)
高階組件是一個(gè)接受一個(gè)組件并返回一個(gè)新組件的函數(shù)。
HOC 的一個(gè)常見用法是為封裝的組件增加新屬性或修改現(xiàn)有的屬性值。這種技術(shù)稱為屬性代理:
function?withNewFunctionality(WrappedComponent)?{ ?return?class?NewFunctionality?extends?Component?{ ?render()?{ ?const?newProp?=?'Value'; ?const?propsProxy?=?{ ?...this.props, ?//?修改現(xiàn)有屬性: ?ownProp:?this.props.ownProp?+?'?was?modified', ?//?增加新屬性: ?newProp ?}; ?return?; ?} ?} } const?MyNewComponent?=?withNewFunctionality(MyComponent); 復(fù)制代碼
你還可以通過(guò)控制輸入組件的渲染過(guò)程從而控制渲染結(jié)果。這種 HOC 技術(shù)被稱為渲染劫持:
function?withModifiedChildren(WrappedComponent)?{ ?return?class?ModifiedChildren?extends?WrappedComponent?{ ?render()?{ ?const?rootElement?=?super.render(); ?const?newChildren?=?[ ?...rootElement.props.children, ?//?插入一個(gè)元素 ?New?child?]; ?return?cloneElement( ?rootElement, ?rootElement.props, ?newChildren ?); ?} ?} } const?MyNewComponent?=?withModifiedChildren(MyComponent); 復(fù)制代碼
如果您想深入了解HOCS實(shí)踐,我建議您閱讀“深入響應(yīng)高階組件”。
讓我們通過(guò)一個(gè)例子來(lái)看看HOC的屬性代理技術(shù)如何幫助分離職責(zé)。
組件
復(fù)制代碼
input 的狀態(tài)在 handlechange(event) 方法中更新。點(diǎn)擊按鈕,值將保存到本地存儲(chǔ),在 handleclick() 中處理:
class?PersistentForm?extends?Component?{ ?constructor(props)?{ ?super(props); ?this.state?=?{?inputValue:?localStorage.getItem('inputValue')?}; ?this.handleChange?=?this.handleChange.bind(this); ?this.handleClick?=?this.handleClick.bind(this); ?} ?render()?{ ?const?{?inputValue?}?=?this.state; ?return?( ?? ? ??); ?} ?handleChange(event)?{ ?this.setState({ ?inputValue:?event.target.value ?}); ?} ?handleClick()?{ ?localStorage.setItem('inputValue',?this.state.inputValue); ?} } 復(fù)制代碼
遺憾的是:
讓我們重構(gòu)一下
class?PersistentForm?extends?Component?{ ?constructor(props)?{ ?super(props); ?this.state?=?{?inputValue:?props.initialValue?}; ?this.handleChange?=?this.handleChange.bind(this); ?this.handleClick?=?this.handleClick.bind(this); ?} ?render()?{ ?const?{?inputValue?}?=?this.state; ?return?( ?? ? ??); ?} ?handleChange(event)?{ ?this.setState({ ?inputValue:?event.target.value ?}); ?} ?handleClick()?{ ?this.props.saveValue(this.state.inputValue); ?} } 復(fù)制代碼
組件從屬性初始值接收存儲(chǔ)的輸入值,并使用屬性函數(shù) saveValue(newValue) 來(lái)保存輸入值。這些props 由使用屬性代理技術(shù)的 withpersistence() HOC提供。
現(xiàn)在
查詢和保存到本地存儲(chǔ)的職責(zé)由 withPersistence() HOC承擔(dān):
function?withPersistence(storageKey,?storage)?{ ?return?function?(WrappedComponent)?{ ?return?class?PersistentComponent?extends?Component?{ ?constructor(props)?{ ?super(props); ?this.state?=?{?initialValue:?storage.getItem(storageKey)?}; ?} ?render()?{ ?return?( ??); ?} ?saveValue(value)?{ ?storage.setItem(storageKey,?value); ?} ?} ?} } 復(fù)制代碼
withPersistence()是一個(gè) HOC,其職責(zé)是持久的。它不知道有關(guān)表單域的任何詳細(xì)信息。它只聚焦一個(gè)工作:為傳入的組件提供 initialValue 字符串和 saveValue() 函數(shù)。
將
const?LocalStoragePersistentForm ?=?withPersistence('key',?localStorage)(PersistentForm); const?instance?=?; 復(fù)制代碼
只要
反之亦然:只要 withPersistence() 提供正確的 initialValue 和 saveValue(),對(duì) HOC 的任何修改都不能破壞處理表單字段的方式。
SRP的效率再次顯現(xiàn)出來(lái):修改隔離,從而減少對(duì)系統(tǒng)其他部分的影響。
此外,代碼的可重用性也會(huì)增加。你可以將任何其他表單
const?LocalStorageMyOtherForm ?=?withPersistence('key',?localStorage)(MyOtherForm); const?instance?=?; 復(fù)制代碼
你可以輕松地將存儲(chǔ)類型更改為 session storage:
const?SessionStoragePersistentForm ?=?withPersistence('key',?sessionStorage)(PersistentForm); const?instance?=?; 復(fù)制代碼
初始版本
在不好分塊組合的情況下,屬性代理和渲染劫持的 HOC 技術(shù)可以使得組件只有一個(gè)職責(zé)。
謝謝各位小伙伴愿意花費(fèi)寶貴的時(shí)間閱讀本文,如果本文給了您一點(diǎn)幫助或者是啟發(fā),請(qǐng)不要吝嗇你的贊和Star,您的肯定是我前進(jìn)的最大動(dòng)力。