[譯] 你是如何拆分組件的?

你是如何拆分組件的?

React 組件會隨着時間的推移而逐步增加。幸虧我意識到了這一點,否則個人一些應用程序的組件將變得很是可怕。html

但這其實是一個問題嗎?雖然建立許多隻使用一次的小組件彷佛有點奇怪……前端

在一個大型的 React 應用程序中,擁有大量的組件自己沒有什麼錯。實際上,對於狀態組件,咱們固然是但願它們越小越好。node

臃腫組件的出現

關於狀態它一般不會很好地分解。若是有多個動做做用於同一狀態,那麼它們都須要放在同一個組件中。狀態能夠被改變的方式越多,組件就越大。另外,若是一個組件有影響多個狀態類型的動做,那麼它將變得很是龐大,這是不可避免的。react

但即便大型組件不可避免,它們使用起來仍然是很是糟糕的。這就是爲何你會盡量地拆分出更小的組件,遵循關注點分離的原則。android

固然,提及來容易作起來難。ios

尋找關注點分離的方法是一門技術,更是一門藝術。但你能夠遵循如下幾種常見模式……git

4 種類型的組件

根據個人經驗,有四種類型的組件能夠從較大的組件中拆分出來。github

視圖組件

有關視圖組件(有些人稱爲展現組件)的更多信息,請參閱 Dan Abramov 的名著 —— 展現組件和容器組件redux

視圖組件是最簡單的組件類型。它們所作的就是顯示信息,並經過回調發送用戶輸入。它們:後端

  • 將屬性分發給子元素。
  • 擁有將數據從子元素轉發到父組件的回調。
  • 一般是函數組件,但若是爲了性能,它們須要綁定回調,則多是類。
  • 通常不使用生命週期方法,性能優化除外。
  • 直接存儲狀態,除了以 UI 爲中心的狀態,例如動畫狀態。
  • 使用 refs 或直接與 DOM 進行交互(由於 DOM 的改變意味着狀態的改變)。
  • 修改環境。它們不該該直接將動做發送給 redux 的 store 或者調用 API 等。
  • 使用 React 上下文。

你能夠從較大的組件中拆分出展現組件的一些跡象:

  • 有 DOM 標記或者樣式。
  • 有像列表項這樣重複的部分。
  • 有「看起來」像一個盒子或者區域的內容。
  • JSX 的一部分僅依賴於單個對象做爲輸入數據。
  • 有一個具備不一樣區域的大型展現組件。

能夠從較大的組件中拆分出展現組件的一些示例:

  • 爲多個子元素執行佈局的組件。
  • 卡片和列表項能夠從列表中拆分出來。
  • 字段能夠從表單中拆分出來(將全部的更新合併到一個 onChange 回調中)。
  • 標記能夠從控件中拆分出來。

控制組件

控制組件指的是存儲與部分輸入相關的狀態的組件,即跟蹤用戶已發起動做的狀態,而這些狀態還未經過 onChange 回調產生有效值。它們與展現組件類似,可是:

  • 能夠存儲狀態(當與部分輸入相關時)。
  • 可使用 refs 和與 DOM 進行交互。
  • 可使用生命週期方法。
  • 一般沒有任何樣式,也沒有 DOM 標記。

你能夠從較大的組件中拆分出控制組件的一些跡象:

  • 將部分輸入存儲在狀態中。
  • 經過 refs 與 DOM 進行交互。
  • 某些部分看起來像原生控件 —— 按鈕,表單域等。

控制組件的一些示例:

  • 日期選擇器
  • 輸入提示
  • 開關

你常常會發現你的不少控件具備相同的行爲,但有不一樣的展示形式。在這種狀況下,經過將展示形式拆分紅視圖組件,並做爲 themeview 屬性傳入是有意義的。

你能夠在 react-dnd 庫中查看鏈接器函數的實際示例。

當從控件中拆分出展現組件時,你可能會發現經過 props 將單獨的 ref 函數和回調傳遞給展現組件感受有點不對。在這種狀況下,它可能有助於傳遞鏈接器函數,這個函數將 refs 和回調克隆到傳入的元素中。例如:

class MyControl extends React.Component {
  // 鏈接器函數使用 React.cloneElement 將事件處理程序
  // 和 refs 添加到由展現組件建立的元素中。
  connectControl = (element) => {
    return React.cloneElement(element, {
      ref: this.receiveRef,
      onClick: this.handleClick,
    })
  }

  render() {
    // 你能夠經過屬性將展現組件傳遞給控件,
    // 從而容許控件以任意標記和樣式來做爲主題。
    return React.createElement(this.props.view, {
      connectControl: this.connectControl,
    })
  }

  handleClick = (e) => { /* ... */ }
  receiveRef = (node) => { /* ... */ }

  // ...
}

// 展現組件能夠在 `connectControl` 中包裹一個元素,
// 以添加適當的回調和 `ref` 函數。
function ControlView({ connectControl }) {
  return connectControl(
    <div className='some-class'> control content goes here </div>
  )
}複製代碼

你會發現控制組件一般會很是大。它們必須處理和狀態密不可分的 DOM,這就使得控制組件的拆分特別有用;經過將 DOM 交互限制爲控制組件,你能夠將任何與 DOM 相關的雜項放在一個地方。

控制器

一旦你將展現和控制代碼拆分到獨立的組件中後,大部分剩餘的代碼將是業務邏輯。若是有一件事我想你在閱讀本文以後記住,那就是業務邏輯不須要放在 React 組件中。將業務邏輯用普通 JavaScript 函數和類來實現一般是有意義的。因爲沒有一個更好的名字,我將它稱之爲控制器

因此只有三種類型的 React 組件。但仍然有四種類型的組件,由於不是每一個組件都是一個 React 組件。

並非每輛車都是豐田(但至少在東京大部分都是)。

控制器一般遵循相似的模式。它們:

  • 存儲某個狀態。
  • 有改變那個狀態的動做,並可能引發反作用。
  • 可能有一些訂閱狀態變動的方法,而這些變動不是由動做直接形成的。
  • 能夠接受相似屬性的配置,或者訂閱某個全局控制器的狀態。
  • 依賴於任何 React API。
  • 與 DOM 進行交互,也沒有任何樣式。

你能夠從你的組件中拆分出控制器的一些跡象:

  • 組件有不少與部分輸入無關的狀態。
  • 狀態用於存儲從服務器接收到的信息。
  • 引用全局狀態,如拖放或導航的狀態。

一些控制器的示例:

  • 一個 Redux 或者 Flux 的 store。
  • 一個帶有 MobX 可觀察的 JavaScript 類。
  • 一個包含方法和實例變量的普通 JavaScript 類。
  • 一個事件發射器。

一些控制器是全局的;它們徹底獨立於你的 React 應用程序。Redux 的 stores 就是一個是全局控制器很好的例子。但並非全部的控制器都須要是全局的,也並非全部的狀態都須要放在單獨的控制器或者 store 中。

經過將表單和列表的控制器代碼拆分爲單獨的類,你能夠根據須要在容器組件中實例化這些類。

容器組件

容器組件是將控制器鏈接到展現組件和控制組件的粘合劑。它們比其餘類型的組件更具備靈活性。但仍然傾向於遵循一些模式,它們:

  • 在組件狀態中存儲控制器實例。
  • 經過展現組件和控制組件來渲染狀態。
  • 使用生命週期方法來訂閱控制器狀態的更新。
  • 使用 DOM 標記或樣式(可能出現的例外是一些無樣式的 div)。
  • 一般由像 Redux 的 connect 這樣的高階函數生成。
  • 能夠經過上下文訪問全局控制器(例如 Redux 的 store)。

雖然有時候你能夠從其餘容器中拆分出容器組件,但這不多見。相反,最好將精力集中在拆分控制器、展現組件和控制組件上,並將剩下的全部都變成你的容器組件。

一些容器組件的示例:

  • 一個 App 組件
  • 由 Redux 的 connect 返回的組件。
  • 由 MobX 的 observer 返回的組件。
  • react-router 的 <Link> 組件(由於它使用上下文並影響環境)。

組件文件

你怎麼稱呼一個不是視圖、控制、控制器或容器的組件?你只是把它叫作組件!很簡單,不是嗎?

一旦你拆分出一個組件,問題就變成了我把它放在哪裏?老實說,答案很大程度上取決於我的喜愛,但有一條規則我認爲很重要:

若是拆分出的組件只在一個父級中使用,那麼它將與父級在同一個文件中

這是爲了儘量容易地拆分組件。建立文件比較麻煩,而且會打斷你的思路。若是你試着將每一個組件放在不一樣的文件中,你很快就會問本身「我真的須要一個新組件嗎?」所以,請將相關的組件放在同一個文件中。

固然,一旦你找到了重用該組件的地方,你可能但願將它移動到單獨的文件中。這就使得把它放到哪一個文件中去成爲一個甜蜜的煩惱了。

性能怎麼樣?

將一個龐大的組件拆分紅多個控制器、展現組件和控制組件,增長了須要運行的代碼總量。這可能會減慢一點點,但不會減慢不少。

故事

我遇到過惟一一次因爲使用太多組件而引發性能問題 —— 我在每一幀上渲染 5000 個網格單元格,每一個單元格都有多個嵌套組件。

關於 React 性能的是,即便你的應用程序有明顯的延遲,問題確定不是出於組件太多。

因此你想使用多少組件均可以

若是沒有拆分……

我在本文中提到了不少規則,因此你可能會驚訝地聽到我其實並不喜歡嚴格的規則。它們一般是錯的,至少在某些狀況下是這樣。因此必需要明確的是:

『能夠』拆分並不意味着『必須』拆分

假設你的目標是讓你的代碼更易於理解和維護,這仍然留下了一個問題:怎樣纔是易於理解?怎樣纔是易於維護?而答案每每取決於誰在問,這就是爲何重構是技術,更是藝術。

有一個具體的例子,考慮下這個組件的設計:

<!DOCTYPE html>
<html>
  <head>
    <title>I'm in a React app!</title>
  </head>
  <body>
    <div id="app"></div>

    <script src="https://unpkg.com/react@15.6.1/dist/react.js"></script>
    <script src="https://unpkg.com/react-dom@15.6.1/dist/react-dom.js"></script>
    <script> // 這裏寫 JavaScript </script>
  </body>
</html>複製代碼
class List extends React.Component {
  renderItem(item, i) {
    return (
      <li key={item.id}> {item.name} </li>
    )
  }

  render() {
    return (
      <ul> {this.props.items.map(this.renderItem)} </ul>
    )
  }
}

ReactDOM.render(
  <List items={[ { id: 'a', name: 'Item 1' }, { id: 'b', name: 'Item 2' } ]} />, document.getElementById('app') )複製代碼

儘管將 renderItem 拆分紅一個單獨的組件是徹底可能的,但這樣作實際上會有什麼好處呢?可能沒有。實際上,在具備多個不一樣組件的文件中,使用 renderItem 方法可能會更容易理解。

請記住:四種類型的組件是當你以爲它們有意義的時候,你可使用的一種模式。它們並非硬性規定。若是你不肯定某些內容是否須要拆分,那就不要拆分,由於即便某些組件比其餘組件更臃腫,世界末日也不會到來。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索