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

玄學醬發表於2017-10-16
本文講的是[譯] 你是如何拆分元件的?,

React 元件會隨著時間的推移而逐步增長。幸好我意識到了這一點,不然我的一些應用程式的元件將變得非常可怕。

但這實際上是一個問題嗎?雖然建立許多隻使用一次的小元件似乎有點奇怪……

在一個大型的 React 應用程式中,擁有大量的元件本身沒有什麼錯。實際上,對於狀態元件,我們當然是希望它們越小越好。

臃腫元件的出現

關於狀態它通常不會很好地分解。如果有多個動作作用於同一狀態,那麼它們都需要放在同一個元件中。狀態可以被改變的方式越多,元件就越大。另外,如果一個元件有影響多個狀態型別的動作,那麼它將變得非常龐大,這是不可避免的。

但即使大型元件不可避免,它們使用起來仍然是非常糟糕的。這就是為什麼你會盡可能地拆分出更小的元件,遵循關注點分離的原則。

當然,說起來容易做起來難。

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

4 種型別的元件

根據我的經驗,有四種型別的元件可以從較大的元件中拆分出來。

檢視元件

有關檢視元件(有些人稱為展示元件)的更多資訊,請參閱 Dan Abramov 的名著 —— 展示元件和容器元件

檢視元件是最簡單的元件型別。它們所做的就是顯示資訊,並通過回撥傳送使用者輸入。它們:

  • 將屬性分發給子元素。
  • 擁有將資料從子元素轉發到父元件的回撥。
  • 通常是函式元件,但如果為了效能,它們需要繫結回撥,則可能是類。
  • 一般不使用生命週期方法,效能優化除外。
  • 直接儲存狀態,除了以 UI 為中心的狀態,例如動畫狀態。
  • 使用 refs 或直接與 DOM 進行互動(因為 DOM 的改變意味著狀態的改變)。
  • 修改環境。它們不應該直接將動作傳送給 redux 的 store 或者呼叫 API 等。
  • 使用 React 上下文。

你可以從較大的元件中拆分出展示元件的一些跡象:

  • 有 DOM 標記或者樣式。
  • 有像列表項這樣重複的部分。
  • 有“看起來”像一個盒子或者區域的內容。
  • JSX 的一部分僅依賴於單個物件作為輸入資料。
  • 有一個具有不同區域的大型展示元件。

可以從較大的元件中拆分出展示元件的一些示例:

  • 為多個子元素執行佈局的元件。
  • 卡片和列表項可以從列表中拆分出來。
  • 欄位可以從表單中拆分出來(將所有的更新合併到一個 onChange 回撥中)。
  • 標記可以從控制元件中拆分出來。

控制元件

控制元件指的是儲存與部分輸入相關的狀態的元件,即跟蹤使用者已發起動作的狀態,而這些狀態還未通過 onChange 回撥產生有效值。它們與展示元件相似,但是:

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

你可以從較大的元件中拆分出控制元件的一些跡象:

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

控制元件的一些示例:

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

你經常會發現你的很多控制元件具有相同的行為,但有不同的展現形式。在這種情況下,通過將展現形式拆分成檢視元件,並作為 theme 或 view 屬性傳入是有意義的。

你可以在 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 方法可能會更容易理解。

請記住:四種型別的元件是當你覺得它們有意義的時候,你可以使用的一種模式。它們並不是硬性規定。如果你不確定某些內容是否需要拆分,那就不要拆分,因為即使某些元件比其他元件更臃腫,世界末日也不會到來。





原文釋出時間為:2017年9月2日

本文來自雲棲社群合作伙伴掘金,瞭解相關資訊可以關注掘金網站。


相關文章