React組件應(yīng)該如何封裝?
翻譯:劉小夕
原文鏈接:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/
原文的篇幅非常長,不過內(nèi)容太過于吸引我,還是忍不住要翻譯出來。此篇文章對編寫可重用和可維護(hù)的React組件非常有幫助。但因?yàn)槠鶎?shí)在太長,我對文章進(jìn)行了分割,本篇文章重點(diǎn)闡述 封裝。因本人水平有限,文中部分翻譯可能不夠準(zhǔn)確,如果您有更好的想法,歡迎在評論區(qū)指出。
更多文章可戳: https://github.com/YvetteLau/Blog
封裝
一個封裝組件提供
props控制其行為而不是暴露其內(nèi)部結(jié)構(gòu)。
耦合是決定組件之間依賴程度的系統(tǒng)特性。根據(jù)組件的依賴程度,可區(qū)分兩種耦合類型:
當(dāng)應(yīng)用程序組件對其他組件知之甚少或一無所知時,就會發(fā)生松耦合。
當(dāng)應(yīng)用程序組件知道彼此的許多詳細(xì)信息時,就會發(fā)生緊耦合。
松耦合是我們設(shè)計應(yīng)用結(jié)構(gòu)和組件之間關(guān)系的目標(biāo)。
松耦合應(yīng)用(封裝組件)

松耦合會帶來以下好處:
可以在不影響應(yīng)用其它部分的情況下對某一塊進(jìn)行修改。、
任何組件都可以替換為另一種實(shí)現(xiàn)
在整個應(yīng)用程序中實(shí)現(xiàn)組件復(fù)用,從而避免重復(fù)代碼
獨(dú)立組件更容易測試,增加了測試覆蓋率
相反,緊耦合的系統(tǒng)會失去上面描述的好處。主要缺點(diǎn)是很難修改高度依賴于其他組件的組件。即使是一處修改,也可能導(dǎo)致一系列的依賴組件需要修改。
緊耦合應(yīng)用(組件無封裝)

封裝 或 信息隱藏 是如何設(shè)計組件的基本原則,也是松耦合的關(guān)鍵。
信息隱藏
封裝良好的組件隱藏其內(nèi)部結(jié)構(gòu),并提供一組屬性來控制其行為。
隱藏內(nèi)部結(jié)構(gòu)是必要的。其他組件沒必要知道或也不依賴組件的內(nèi)部結(jié)構(gòu)或?qū)崿F(xiàn)細(xì)節(jié)。
React 組件可能是函數(shù)組件或類組件、定義實(shí)例方法、設(shè)置 ref、擁有 state 或使用生命周期方法。這些實(shí)現(xiàn)細(xì)節(jié)被封裝在組件內(nèi)部,其他組件不應(yīng)該知道這些細(xì)節(jié)。
隱藏內(nèi)部結(jié)構(gòu)的組件彼此之間的依賴性較小,而降低依賴度會帶來松耦合的好處。
通信
細(xì)節(jié)隱藏是隔離組件的關(guān)鍵。此時,你需要一種組件通信的方法:props。porps 是組件的輸入。
建議 prop 的類型為基本數(shù)據(jù)(例如,string 、 number 、boolean):
<Message text="Hello world!" modal={false} />;
必要時,使用復(fù)雜的數(shù)據(jù)結(jié)構(gòu),如對象或數(shù)組:
<MoviesList items={['Batman Begins', 'Blade Runner']} />
prop 可以是一個事件處理函數(shù)和異步函數(shù):
<input type="text" onChange={handleChange} />
prop 甚至可以是一個組件構(gòu)造函數(shù)。組件可以處理其他組件的實(shí)例化:
function If({ component: Component, condition }) {
return condition ? <Component /> : null;
}
<If condition={false} component={LazyComponent} />
為了避免破壞封裝,請注意通過 props 傳遞的內(nèi)容。給子組件設(shè)置 props 的父組件不應(yīng)該暴露其內(nèi)部結(jié)構(gòu)的任何細(xì)節(jié)。例如,使用 props 傳輸整個組件實(shí)例或 refs 都是一個不好的做法。
訪問全局變量同樣也會對封裝產(chǎn)生負(fù)面影響。
案例研究:封裝修復(fù)
組件的實(shí)例和狀態(tài)對象是封裝在組件內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)。因此,將狀態(tài)管理的父組件實(shí)例傳遞給子組件會破壞封裝。
我們來研究一下這種情況。
一個簡單的應(yīng)用程序顯示一個數(shù)字和兩個按鈕。第一個按鈕增加數(shù)值,第二個按鈕減少數(shù)值:

class App extends React.Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
render() {
return (
<div className="app">
<span className="number">{this.state.number}</span>
<Controls parent={this} />
</div>
);
}
}
class Controls extends React.Component {
render() {
return (
<div className="controls">
<button onClick={() => this.updateNumber(+1)}>
Increase
</button>
<button onClick={() => this.updateNumber(-1)}>
Decrease
</button>
</div>
);
}
updateNumber(toAdd) {
this.props.parent.setState(prevState => ({
number: prevState.number + toAdd
}));
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<Controls> 負(fù)責(zé)渲染按鈕,并為其設(shè)置事件處理函數(shù),當(dāng)用戶點(diǎn)擊按鈕時,父組件的狀態(tài)將會被更新:number 加1或者減1 (updateNumber()方法)
當(dāng)前的實(shí)現(xiàn)有什么問題?
第一個問題是:
<App>的封裝被破壞,因?yàn)樗膬?nèi)部結(jié)構(gòu)在應(yīng)用中傳遞。<App>錯誤地允許<Controls>直接去修改其state。第二個問題是: 子組件
Controls知道了太多父組件的內(nèi)部細(xì)節(jié),它可以訪問父組件的實(shí)例,知道父組件是一個有狀態(tài)組件,知道父組件的state對象的細(xì)節(jié)(知道number是父組件state的屬性),并且知道怎么去更新父組件的state.
// 問題: 使用父組件的內(nèi)部結(jié)構(gòu)
class Controls extends Component {
render() {
return (
<div className="controls">
<button onClick={() => this.updateNumber(+1)}>
Increase
</button>
<button onClick={() => this.updateNumber(-1)}>
Decrease
</button>
</div>
);
}
updateNumber(toAdd) {
this.props.parent.setState(prevState => ({
number: prevState.number + toAdd
}));
}
}
這樣就會導(dǎo)致:<Controls> 將很難測試和重用。對 <App> 結(jié)構(gòu)的細(xì)微修改會導(dǎo)致需要對 <Controls> 進(jìn)行修改(對于更大的應(yīng)用程序,也會導(dǎo)致類似耦合的組件需要修改)。
解決方案是設(shè)計一個方便的通信接口,考慮到松耦合和封裝。讓我們改進(jìn)兩個組件的結(jié)構(gòu)和屬性,以便恢復(fù)封裝。
只有組件本身應(yīng)該知道它的狀態(tài)結(jié)構(gòu)。<App> 的狀態(tài)管理應(yīng)該從 <Controls>(updateNumber()方法)移到正確的位置:即 <App> 組件中。
<App> 被修改為 <Controls> 設(shè)置屬性 onIncrease 和 onDecrease。這些是更新 <App> 狀態(tài)的回調(diào)函數(shù):
// 解決: 恢復(fù)封裝
class App extends Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
render() {
return (
<div className="app">
<span className="number">{this.state.number}</span>
<Controls
onIncrease={() => this.updateNumber(+1)}
onDecrease={() => this.updateNumber(-1)}
/>
</div>
);
}
updateNumber(toAdd) {
this.setState(prevState => ({
number: prevState.number + toAdd
}));
}
}
現(xiàn)在,<Controls> 接收用于增加和減少數(shù)值的回調(diào),注意解耦和封裝恢復(fù)時:<Controls> 不再需要訪問父組件實(shí)例。也不會直接去修改父組件的狀態(tài)。
而且,<Controls> 被修改為了一個函數(shù)式組件:
// 解決方案: 使用回調(diào)函數(shù)去更新父組件的狀態(tài)
function Controls({ onIncrease, onDecrease }) {
return (
<div className="controls">
<button onClick={onIncrease}>Increase</button>
<button onClick={onDecrease}>Decrease</button>
</div>
);
}
<App> 組件的封裝已經(jīng)恢復(fù),狀態(tài)由其本身管理,也應(yīng)該如此。
此外,<Controls> 不在依賴 <App> 的實(shí)現(xiàn)細(xì)節(jié),onIncrease 和 onDecrease 在按鈕被點(diǎn)擊的時候調(diào)用,<Controls> 不知道(也不應(yīng)該知道)這些回調(diào)的內(nèi)部實(shí)現(xiàn)。
<Controls> 組件的可重用性和可測試性顯著增加。
<Controls> 的復(fù)用變得很容易,因?yàn)樗诵枰卣{(diào),沒有其它依賴。測試也變得簡單,只需驗(yàn)證單擊按鈕時,回調(diào)是否執(zhí)行。
如果你喜歡探討技術(shù),或者對本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當(dāng)然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。
魚頭的微信號是:krisChans95 也可以掃碼關(guān)注公眾號,訂閱更多精彩內(nèi)容。
