React : 展示組件 & 容器組件 附案例與視頻

寫React應用時,我發現了一種簡單而有效的模式。如果你也寫過一陣子React,或許你也已經發現它了。對於這種模式,這篇文章講得不錯,不過我還想再補充幾點。

如果把組件 分爲以下兩類,對組件的複用和理解會更容易一些。我這兩類組件稱爲 展示組件容器組件。也有叫“胖的&瘦的”、“聰明的&笨的”、“包含狀態的的&純的”、“Screens and Components”的等等說法,這些說法並不完全一致,但核心理念大概相同。

展示組件的特性:

  • 負責外觀的展示
  • 可能同時包含展示組件 & 容器組件,通常帶有自身的 DOM標籤 和 樣式屬性
  • 通常可以通過 this.props.children 包含組件
  • 不依賴應用中的其他組件,如FLUX的 actions 或 stores
  • 對於如何加載、修改數據,不做具體規定
  • 僅通過props來接收數據 和 回調函數
  • 幾乎沒有自身的狀態(就是有,也是UI狀態,而不是數據)
  • 除非組件需要狀態、生命週期鉤子(lifecycle hooks)、或者性能優化,否則一般寫爲函數式組件
  • 例如 Page, Sidebar, Story, UserInfo, List.

容器組件的特性

  • 負責功能的實現
  • 可能同時包含展示組件 & 容器組件,但通常自身不帶有任何DOM標籤(起包裹作用的div除外),也不帶有任何樣式屬性
  • 向 展示組件 和 其他容器組件 提供數據和行爲/方法
  • 調用Flux的actions,並將其作爲回調函數,提供給展示組件
  • 通常是包含狀態的,因爲經常把它們作爲數據源使用。
  • 通常不是手寫的,而是用高階組件生成的。(高階組件如React Redux 的connect() ,Relay的createContainer() 或者 Flux Utils的 Container.create() 等)
  • 例如:UserPage, FollowersSidebar, StoryContainer, FollowedUserList.

爲了讓這種區分更加明顯,我會把這兩類組件放到不同的目錄裏。

這麼做的好處

  • 兩類組件各司其職。由此,你對該APP/UI的理解會更加深入。
  • 更好的複用性。對完全不同的狀態源,你可以使用同一個展示組件,並將其變爲不同的、可進一步複用的容器組件。
  • 展示組件其實就是APP的“調色板”。你可以把它們放到一個單獨的頁面上,交給設計師,隨便他怎麼折騰,APP的邏輯和功能都不會受到一絲影響。你可以在這個頁面上進行screenshot regression測試。
  • 迫使你從APP裏“提煉”出“佈局組件”,如Sidebar, Page, ContextMenu等。由此,你將不得不使用this.props.children,而非在若干容器組件內 重複使用一套佈局相關的代碼。

請注意,組件並不一定需要生成DOM。它們只需要提供UI之間的分界與組合關係。

好好利用這一點。

何時引入容器組件?

在剛開始寫APP的時候,我建議你只寫展示組件。先就這麼寫着,總會有一個時刻,你將注意到,有太多的屬性需要傳遞給中間層的組件。有些組件根本用不上這些屬性,傳給它們的目的,僅僅是爲了能繼續向下傳遞屬性。而且,當子組件需要更多的數據時,你不得不重寫中間層的組件。當你意識到這些時,就是引入容器組件的好時機。通過使用容器組件,無需途徑組件樹中其他無關的組件,就可以直接將數據和方法屬性傳給末端的葉子組件中。

這種重構的過程是漸進的,別想着一步到位。隨着你對這種模式日復一日地練習,對於何時使用容器組件,你會慢慢培養出一種直覺。這種感覺就像你知道啥時候應該抽象出函數一樣。我在蛋頭網(egghead)上的免費系列課程也於此會有所幫助。

其他的分類方法

需要注意的是,展示組件 和 容器組件 之間的區別,並非是技術上的,而是在用途上的。理解這一點很重要。

作爲對比,這裏列舉一些相關(但不同)的技術上的區別

  • 有狀態和無狀態。有的組件使用 React 的 setState() 方法,有的組件則不用。儘管容器組件多是有狀態的,展示組件多是無狀態的,但是這並非硬性規定。展示組件也可以是有狀態的,容器組件也可以是無狀態的。

  • 類和函數。 從 React 0.14 開始 ,組件既可以聲明爲類,也可以聲明爲函數。雖然函數式組件更容易定義,但是它們缺乏某些當前只有類組件纔有的功能。在未來,這些限制可能會漸漸取消,但是目前確實是存在的。因爲函數式組件更容易理解,我建議你一般用函數式組件就好,除非你需要狀態、生命週期鉤子或性能優化等目前 類組件獨有的功能。

  • 純和不純。有人說,只要拿到相同的屬性(props)和狀態,就能返回相同的結果,那麼該組件就是純組件。純組件既可以被定義爲類,也可以被定義爲函數,既可以是有狀態,也可以是無狀態的。純組件的另一個重要特徵是,它們不會依賴於屬性(props) 或者狀態的深層變化(deep mutations),所以它們的渲染性能可以在 shouldComponentUpdate() 鉤子中通過 shallow comparison 來優化。目前只有類可以定義 shouldComponentUpdate() ,以後可能會放寬限制。

不論是展示組件,還是容器組件,都可能是上面所列舉的任意一種。以我的經驗看,展示組件多是無狀態的純函數,而容器組件多是有狀態的純類。不過,這並非規定,而是經驗之談。我確實見過完全相反,但在特定條件下成立的例子。

別把展示組件/容器組件的這種分類方法視作教條。有時候其實無所謂,有時候又難以分辨。如果你對某個組件屬於展示組件還是容器組件舉棋不定,別急,或許還沒到下結論的時候。

例子

Michael Chan這一篇真的說到點子上了。

延伸閱讀

譯者注

注1

一個具體的組件拆分案例

在作者推薦的Michael Chan這一篇文章裏,有一個具體的例子,對理解本文頗有脾益,摘錄於下:

A component like this would be rejected in code review for having both a presentation and data concern:

// CommentList.js

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { comments: [] }
  }

  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }

  render() {
    return <ul> {this.state.comments.map(renderComment)} </ul>;
  }

  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}

It would then be split into two components. The first is like a traditional template, concerned only with presentation, and the second is tasked with fetching data and rendering the related view component.

// CommentList.js

class CommentList extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <ul> {this.props.comments.map(renderComment)} </ul>;
  }

  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}
// CommentListContainer.js

class CommentListContainer extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }

  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }

  render() {
    return <CommentList comments={this.state.comments} />;
  }
}

In the updated example, CommentListContainer could shed JSX pretty simply.

render() {
  return React.createElement(CommentList, { comments: this.state.comments });
}

注2

Dan結合案例講解這兩種組件的視頻課

注3

一個對展示組件進行簡化以及傳參的例子

原文鏈接

Presentational and Container Components

發佈了49 篇原創文章 · 獲贊 40 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章