原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/
封裝過的元件應提供 props 以控制其行為,而不是暴露內部的結構
耦合(coupling) 是一種表示元件之間依賴度的系統特徵。根據依賴的程度,可以區分出兩種耦合:
- 元件對其他元件瞭解的很少,甚至一無所知的情況,就是鬆耦合
- 元件掌握著其他元件的大量細節時,就是緊耦合
在設計系統結構和元件間關係的時候,應以鬆耦合為目標。
鬆耦合將帶來如下的好處:
- 系統中的區域性改變不影響他處
- 任何元件都可以被替代品取代
- 系統之間的元件可以複用,順應了 DRY(Don't repeat yourself)原則
- 可以輕易測試獨立的元件,提高了應用的測試程式碼覆蓋率
反之,緊耦合的系統就沒有上述便利。主要的缺點就在於無法輕易修改一個大量依賴其他元件的元件。甚至一個簡單的改變都會導致連鎖的修改。
封裝,或者說是 資訊隱藏,是設計元件時的基本原則,也是達成鬆耦合的關鍵。
1. 資訊隱藏
一個封裝良好的元件會隱藏其內部結構,並通過一組 props 提供控制其行為的途徑。
隱藏內部結構是必須的。內部結構或實現細節不能被其他元件知道或關聯。
React 元件可以是函式式的,也可以是基於類的,可以定義例項方法、設定 refs、維護 state 或是使用生命週期方法。這些實現細節被封裝在元件自身中,其他元件不應該窺見其中的任何細節。
隱藏了內部結構的單元(units)-- 如此處談論的元件,對其他單元的依賴是低的。低依賴度帶來的是鬆耦合的好處。
2. 通訊
細節隱藏是一種用來隔離元件的約束手段。雖然如此,還是需要元件之間的通訊的。所以有請 props 吧~
作為元件的輸入,prop 最好是 JS 基本型別 (如 string、number、boolean):
<Message text="Hello world!" modal={false} />;
複製程式碼
必要的時候可以用物件或陣列等複雜型別:
<MoviesList items={['Batman Begins', 'Blade Runner']} />
複製程式碼
作為事件處理和非同步操作時,可以指定為函式:
<input type="text" onChange={handleChange} />
複製程式碼
prop 甚至可以是一個元件構造器。元件可被用來處理其他元件的例項化:
function If({ Component, condition }) {
return condition ? <Component /> : null;
}
<If condition={false} component={LazyComponent} />
複製程式碼
為避免破壞封裝,要謹慎對待 props 傳遞的細節。父元件對子元件設定 props 時,也不應該暴露自身的結構。比如,把整個元件例項或 refs 當成 props 傳遞就是個糟糕的決定。
訪問全域性變數是另一個對封裝造成負面影響的問題。
3. 案例學習:封裝的恢復
元件例項和 state 物件都是封裝在元件內部的實現。當把父元件例項傳遞給子元件,想籍此來管理 state 時,就百分之百的破壞了封裝。
來看一個這樣的情況。
這是個顯示一個數字,以及“加”、“減”兩個按鈕的簡單應用:
<div id="root"></div>
複製程式碼
class App extends React.Component {
...
}
class Controls extends React.Component {
...
}
ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼
該應用由兩個元件組成:<App>
和 <Controls>
。
<App>
的 state 物件中包含了一個可修改的數字屬性,並負責渲染該數字:
// 問題在於:破壞了封裝
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 parent={this} />
</div>
);
}
}
複製程式碼
<Controls>
渲染兩個按鈕,並在按鈕上附加了點選事件處理函式。當使用者點選時,父元件的 state 被更新,相應的數字顯示也會加 1 或減 1。
// 問題在於:使用了父元件的內部結構
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
}));
}
}
複製程式碼
當前的實現錯在何處呢?
第一個問題是 <App>
被破壞的封裝,其內部結構在應用裡盡人皆知了。<App>
錯誤的允許 <Controls>
直接更新其內部 state 了。
隨之發生的,第二個問題是 <Controls>
知道了太多 <App>
的細節。它可以訪問父元件的例項、瞭解父元件的 state 物件結構,還知道如何更新父元件的 state。
被破壞的封裝造成了兩個元件的耦合。
一個麻煩的後果就是,<Controls>
難以被測試和重用了。對 <App>
的一個細小的結構改變,都將引起 <Controls>
或更多層子元件的連鎖修改。
解決方法是設計一個方便的通訊介面,同時滿足鬆耦合和強封裝。讓我們對兩個元件的結構和 props 都做出一些改進,以修復封裝。
只有元件自身可以瞭解其 state 結構。<App>
的 state 管理要從 <Controls>
中挪回來。
然後,<App>
被修改為向 <Controls>
的 onIncrease 和 onDecrease 兩個 props 中提供回撥函式,用於升級 state:
// 解決方法:恢復封裝
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
}));
}
}
複製程式碼
現在 <Controls>
接受到用於加減數字的兩個回撥函式。關鍵在於 <Controls>
不用再直接訪問父元件 <App>
的 state 了。
此外 <Controls>
被轉換成了一個無狀態元件:
// 解決方法:使用回撥函式升級符元件的 state
function Controls({ onIncrease, onDecrease }) {
return (
<div className="controls">
<button onClick={onIncrease}>Increase</button>
<button onClick={onDecrease}>Decrease</button>
</div>
);
}
複製程式碼
<App>
的封裝得到了恢復。由自身管理 state,這正是其本職工作。
此外 <Controls>
不再依賴於 <App>
的實現細節了。onIncrease 和 onDecrease 兩個 prop 回撥函式會在點選相應按鈕時被呼叫,而這些回撥函式中的實現細節, <Controls>
不再需要了解,也本不應該知道。
<Controls>
的可重用性和可測試性顯著的提升了。
因為只需要回撥函式,沒有其他依賴,<Controls>
變得易於重用。測試它同樣方便:只需要修改點選按鈕時的回撥就可以了。
轉載請註明出處
長按二維碼或搜尋 fewelife 關注我們哦