[譯] 更可靠的 React 元件:合理的封裝

江米小棗tonylua發表於2018-06-16

原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/

封裝過的元件應提供 props 以控制其行為,而不是暴露內部的結構

耦合(coupling) 是一種表示元件之間依賴度的系統特徵。根據依賴的程度,可以區分出兩種耦合:

  • 元件對其他元件瞭解的很少,甚至一無所知的情況,就是鬆耦合
  • 元件掌握著其他元件的大量細節時,就是緊耦合

在設計系統結構和元件間關係的時候,應以鬆耦合為目標。

[譯] 更可靠的 React 元件:合理的封裝

鬆耦合將帶來如下的好處:

  • 系統中的區域性改變不影響他處
  • 任何元件都可以被替代品取代
  • 系統之間的元件可以複用,順應了 DRY(Don't repeat yourself)原則
  • 可以輕易測試獨立的元件,提高了應用的測試程式碼覆蓋率

反之,緊耦合的系統就沒有上述便利。主要的缺點就在於無法輕易修改一個大量依賴其他元件的元件。甚至一個簡單的改變都會導致連鎖的修改。

[譯] 更可靠的 React 元件:合理的封裝

封裝,或者說是 資訊隱藏,是設計元件時的基本原則,也是達成鬆耦合的關鍵。

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> 變得易於重用。測試它同樣方便:只需要修改點選按鈕時的回撥就可以了。


(end)


----------------------------------------

轉載請註明出處

[譯] 更可靠的 React 元件:合理的封裝
長按二維碼或搜尋 fewelife 關注我們哦

相關文章