你想知道的React組件設計模式這裏都有(上)

640?wx_fmt=gif640?wx_fmt=gif

本文梳理了容器與展示組件、高階組件、render props這三類React組件設計模式

往期回顧:HBaseCon Asia 2019 Track 3 概要回顧

640?wx_fmt=gif

 

隨着 React 的發展,各種組件設計模式層出不窮。React 官方文檔也有不少相關文章,但是組織稍顯凌亂,本文就組件的設計模式這一角度,從問題出發,爲大家梳理了常見的設計模式。看完這篇文章後,你將能得心應手地處理絕大多數 React 組件的使用問題。開始之前先解釋一下什麼是設計模式。所謂模式,是指在某些場景下,針對某類問題的某種通用的解決方案。本文所闡述的設計模式並不是編程通用的設計模式,如大家熟悉的單例模式、工廠模式等等。而是在設計 React 組件時的一些解決方案與技巧,包括:(1)容器與展示組件 (2)高階組件 (3)render props (4)context 模式 (5)組合組件(6)繼承爲了更好的理解,你可以將相應源碼下載下來查看:(https://github.com/imalextu/learn-react-patterns)由於內容較多,分兩篇進行。上篇先介紹:(1)容器與展示組件 (2)高階組件 (3)render props.

一、容器(Container)與展示(Presentational)組件

概念介紹

我們先介紹一個較爲簡單的使用模式,那就是容器組件與展示組件。這種模式還有很多種稱呼,如胖組件和瘦組件、有狀態組件和無狀態組件、聰明組件和傻瓜組件等等。名稱很多,但想要闡述的本質都一樣,就是當組件與外部數據進行交互時,我們可以把組件拆爲兩部分:容器組件:主要負責同外部數據進行交互(通信),譬如與 Redux 等進行數據綁定、通過普通的 fetch 獲取數據等等。展示組件:只根據自身 state 及接收自父組件的 props 做渲染,並不直接與外部數據源進行溝通。>>>>

示例

我們來看一個簡單的例子。構造一個組件,該組件的作用是獲取文本並將其展示出來。

export default class GetText extends React.Component {  state = {    text: null,  }    componentDidMount() {    fetch('https://api.com/',      { headers: { Accept: 'application/json' } }).then(response => {        return response.json()      }).then(json => {        this.setState({ text: json.joke })      })  }
  render() {    return (      <div>        <div>外部獲取的數據:{this.state.text}</div>        <div>UI代碼</div>      </div>    )  }}

看到上面 GetText 這個組件,當有和外部數據源進行溝通的邏輯。那麼我們就可以把這個組件拆成兩部分。

一部分專門負責和外部通信(容器組件),一部分負責UI邏輯(展示組件)。我們來將上面那個例子拆分看看。

容器組件:export default class GetTextContainer extends React.Component {

  state = {    text: null,  }    componentDidMount() {    fetch('https://api.com/',      { headers: { Accept: 'application/json' } }).then(response => {        return response.json()      }).then(json => {        this.setState({ text: json.joke })      })  }
  render() {    return (      <div>        <GetTextPresentational text={this.state.text}/>      </div>    )  }}

展示組件:

export default class GetTextPresentational extends React.Component {  render() {    return (      <div>        <div>外部獲取的數據:{this.props.text}</div>        <div>UI代碼</div>      </div>    )  }}

具體代碼可見:src/pattern1(http://t.cn/AiYbWWak)>>>>

模式所解決的問題

軟件設計中有一個原則,叫做“責任分離”(Separation of Responsibility),即讓一個模塊的責任儘量少,如果發現一個模塊功能過多,就應該拆分爲多個模塊,讓一個模塊都專注於一個功能,這樣更利於代碼的維護。
容器展示組件這個模式所解決的問題在於,當我們切換數據獲取方式時,只需在容器組件修改相應邏輯即可,展示組件無需做改動。比如現在我們獲取數據源是通過普通的 fetch 請求,那麼將來改成 redux 或者 mobx 作爲數據源,我們只需聚焦到容器組件去修改相應邏輯即可,展示組件可完全不變,展示組件有了更高的可複用性。但該模式的缺點也在於將一個組件分成了兩部分,增加了代碼跳轉的成本。並不是說組件包含從外部獲取數據,就要將其拆成容器組件與展示組件。拆分帶來的好處和劣勢需要你自己去權衡。想對這種模式深入瞭解,可以詳見這篇文章:Presentational and Container Components(http://t.cn/RqMyfwV).

二、高階組件

概念介紹

當你想複用一個組件的邏輯時,高階組件(HOC)和渲染回調(render props)就派上用場了。我們先來介紹高階組件,高階組件本質是利用一個函數,該函數接收 React 組件作爲參數,並返回新的組件。我們肯定碰到過很多需要複用業務邏輯的情況,比如我們有一個女性電商網站,所有的組件都要先判定用戶爲女性纔開放展示。比如在 List 組件,是男性則提示不對男性開放,是女性則展示具體服裝列表。而在 ShoppingCart 組件,同樣的一段邏輯,是男性則提示不對男性開放,是女性則展示相應購物車。>>>>

示例

前面我們已經說過了,高階組件其實是利用一個函數,接受 React 組件作爲參數,然後返回新的組件。

我們這邊新建一個 judgeWoman 函數,接受具體的展示組件,然後判斷是否是女性,const judgeWoman = (Component) => {

  const NewComponent = (props) => {    // 判斷是否是女性用戶    let isWoman = Math.random() > 0.5 ? true : false    if (isWoman) {      const allProps = { add: '高階組件增加的屬性', ...props }      return <Component {...allProps} />;    } else {      return <span>女士專用,男士無權瀏覽</span>;    }  }  return NewComponent;};

再將 List 和 ShoppingCart 兩個組件作爲參數傳入這個函數。至此,我們就得到了兩個加強過的組件 WithList 和 WithShoppingCart.判斷是否是女性的這段邏輯得到了複用。

const List = (props) => {  return (    <div>      <div>女士列表頁</div>      <div>{props.add}</div>    </div>  )}const WithList = judgeWoman(List)
const ShoppingCart = (props) => {  return (    <div>      <div>女士購物頁</div>      <div>{props.add}</div>    </div>  )}const WithShoppingCart = judgeWoman(ShoppingCart)

上面是一個簡單的例子,我們還可以給這個函數傳入多個組件。比如我們傳入兩個組件,第一個是女性看到的組件,第二個是男性看到的組件。可複用性是不是更強大了呢?​​​​​​​

const judgeWoman = (Woman,Man) => {  const NewComponent = (props) => {    // 判斷是否是女性用戶    let isWoman = Math.random() > 0.5 ? true : false    if (isWoman) {      const allProps = { add: '高階組件增加的屬性', ...props }      return <Woman {...allProps} />;    } else {      return <Man/>    }  }  return NewComponent;};

更爲強大的是,由於函數返回的也是組件,那麼高階組件是可以嵌套進行使用的!比如我們先判斷性別,再判斷年齡。

 

const withComponet =judgeAge(judgeWoman(ShoppingCart))

具體代碼可見 src/pattern2(http://t.cn/AiYbYy5g)

模式所解決的問題

同樣的邏輯我們總不能重複寫多次。高階組件起到了抽離共通邏輯的作用。同時高階組件的嵌套使用使得代碼複用更加靈活了。

react-redux 就使用了該模式,看到下面的代碼,是不是很熟悉?connect(mapStateToProps, mapDispatchToProps)生成了高階組件函數,該函數接受 TodoList 作爲參數。最後返回了 VisibleTodoList 這個高階組件。

import { connect } from 'react-redux'
const VisibleTodoList = connect(  mapStateToProps,  mapDispatchToProps)(TodoList)>>>>

使用注意事項

高階組件雖好,我們使用起來卻要注意如下點。

  1、包裝顯示名稱以便輕鬆調試

使用高階組件後 debug 會比較麻煩。當 React 渲染出錯的時候,靠組件的 displayName 靜態屬性來判斷出錯的組件類。HOC 創建的容器組件會與任何其他組件一樣,會顯示在 React Developer Tools 中。爲了方便調試,我們需要選擇一個顯示名稱,以表明它是 HOC 的產物。

最常見的方式是用 HOC 包住被包裝組件的顯示名稱。比如高階組件名爲withSubscription,並且被包裝組件的顯示名稱爲 CommentList,顯示名稱應該爲WithSubscription(CommentList):

function withSubscription(WrappedComponent) {  class WithSubscription extends React.Component {/* ... */}  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;  return WithSubscription;}
function getDisplayName(WrappedComponent) {  return WrappedComponent.displayName || WrappedComponent.name || 'Component';}

  2、不要在 render 方法中使用 HOC

React 的 diff 算法(稱爲協調)使用組件標識來確定它是應該更新現有子樹還是將其丟棄並掛載新子樹。 如果從 render 返回的組件與前一個渲染中的組件相同(===),則 React 通過將子樹與新子樹進行區分來遞歸更新子樹。 如果它們不相等,則完全卸載前一個子樹。

通常,你不需要考慮這點。但對 HOC 來說這一點很重要,因爲這代表着你不應在組件的 render 方法中對一個組件應用 HOC:render() {

  // 每次調用 render 函數都會創建一個新的 EnhancedComponent  // EnhancedComponent1 !== EnhancedComponent2  const EnhancedComponent = enhance(MyComponent);  // 這將導致子樹每次渲染都會進行卸載,和重新掛載的操作!  return <EnhancedComponent />;}

這不僅僅是性能問題,重新掛載組件會導致該組件及其所有子組件的狀態丟失。如果在組件之外創建 HOC,這樣一來組件只會創建一次。因此,每次 render 時都會是同一個組件。一般來說,這跟你的預期表現是一致的。在極少數情況下,你需要動態調用 HOC.你可以在組件的生命週期方法或其構造函數中進行調用。  3、務必複製靜態方法有時在 React 組件上定義靜態方法很有用。例如,Relay 容器暴露了一個靜態方法 getFragment 以方便組合 GraphQL 片段。但是,當你將 HOC 應用於組件時,原始組件將使用容器組件進行包裝。這意味着新組件沒有原始組件的任何靜態方法。// 定義靜態函數

WrappedComponent.staticMethod = function() {/*...*/}// 現在使用 HOCconst EnhancedComponent = enhance(WrappedComponent);
// 增強組件沒有 staticMethodtypeof EnhancedComponent.staticMethod === 'undefined' // true

爲了解決這個問題,你可以在返回之前把這些方法拷貝到容器組件上:function enhance(WrappedComponent) {

  class Enhance extends React.Component {/*...*/}  // 必須準確知道應該拷貝哪些方法 :(  Enhance.staticMethod = WrappedComponent.staticMethod;  return Enhance;}

但要這樣做,你需要知道哪些方法應該被拷貝。你可以使用 hoist-non-react-statics 自動拷貝所有非 React 靜態方法:

import hoistNonReactStatic from 'hoist-non-react-statics';function enhance(WrappedComponent) {  class Enhance extends React.Component {/*...*/}  hoistNonReactStatic(Enhance, WrappedComponent);  return Enhance;}

除了導出組件,另一個可行的方案是再額外導出這個靜態方法。// 使用這種方式代替...

MyComponent.someFunction = someFunction;export default MyComponent;
// ...單獨導出該方法...export { someFunction };
// ...並在要使用的組件中,import 它們import MyComponent, { someFunction } from './MyComponent.js';

  4、Refs 不會被傳遞

雖然高階組件的約定是將所有 props 傳遞給被包裝組件,但這對於 Refs 並不適用。那是因爲 ref 實際上並不是一個 prop , 就像 key 一樣,它是由 React 專門處理的。如果將 ref 添加到 HOC 的返回組件中,則 ref 引用指向容器組件,而不是被包裝組件。

這個問題的解決方案是通過使用 React.forwardRef API(React 16.3 中引入)。

三、Render props

概念介紹

術語“render props”是指一種在 React 組件之間使用一個值爲函數的prop來共享代碼的簡單技術。同高階組件一樣,render props的引入也是爲了解決複用業務邏輯。同高階組件中舉的例子一樣,我們看看使用render props要如何實現。>>>>

示例

具有 render props 的組件預期子組件是一個函數,它所做的就是把子組件當做函數調用,調用參數就是傳入的 props,然後把返回結果渲染出來。<Provider>

   {props => <List add={props.add} />}</Provider>

我們具體看下Provider組件是如何定義的。通過這段代碼props.children(allProps),我們調用了傳入的函數。​​​​​​​

const Provider = (props) => {  // 判斷是否是女性用戶  let isWoman = Math.random() > 0.5 ? true : false  if (isWoman) {    const allProps = { add: '高階組件增加的屬性', ...props }    return props.children(allProps)  } else {    return <div>女士專用,男士無權瀏覽</div>;  }}

好像 render props 能做的高階組件也都能做到啊,而且高階組件更容易理解,是否render props 沒啥用呢?我們來看一下 render props 更強大的一個點:對於新增的 props 更加靈活。假設我們的 List 組件接受的是 plus 屬性,ShoppingCart 組件接受的是 add 屬性,我們可以直接這樣寫,無需變動 List 組件以及 Provider 本身。使用高階組件達到相同效果就要複雜很多。​​​​​​

<Provider>  {props => {    const { add } = props    return < List plus={add} />  }}</Provider>
<Provider>  {props => <ShoppingCart add={props.add} />}</Provider>

對於 render props 的使用可以不侷限在利用 children,組件任意的 prop 屬性都可以達到相同效果,比如我們用 test 這個 prop 實現上面相同的效果。​​​​​​​

const Provider = (props) => {  // 判斷是否是女性用戶  let isWoman = Math.random() > 0.5 ? true : false  if (isWoman) {    const allProps = { add: '高階組件增加的屬性', ...props }    return props.test(allProps)  } else {    return <div>女士專用,男士無權瀏覽</div>;  }}const ExampleRenderProps = () => {  return (    <div>      <Provider test={props => <List add={props.add} />} />
      <Provider test={props => <ShoppingCart add={props.add} />} />    </div>  )}

具體代碼可見src/pattern3(http://t.cn/AiYG7916)>>>>

模式所解決的問題

和高階組件一樣,render props 起到了抽離共通邏輯的作用。同時 render props 可以高度定製傳入組件所需要的屬性。我們熟悉的 react router 以及我們下一篇將要介紹的 context 模式都有使用 render props.>>>>

使用注意事項

將 Render Props 與 React.PureComponent 一起使用時要小心!如果你在 Provider 屬性中創建函數,那麼使用 render props 會抵消使用React.PureComponent 帶來的優勢。因爲淺比較 props 的時候總會得到 false,並且在這種情況下每一個 render 對於 render props 將會生成一個新的值。

例如,繼續我們之前使用的 <List> 組件,如果 List 繼承自 React.PureComponent 而不是 React.Component,我們的例子看起來就像這樣:​​​​​​​

class ExampleRenderProps extends React.Component {  render() {    return (      <div>        {/*            這是不好的!            每個渲染的 `test` prop的值將會是不同的。        */}        <Provider test={props => <List add={props.add} />} />      </div>    )  }}

在這樣例子中,每次 <ExampleRenderProps> 渲染,它會生成一個新的函數作爲 <List test> 的 prop,因而在同時也抵消了繼承自 React.PureComponent 的 <List> 組件的效果!爲了繞過這一問題,有時你可以定義一個 prop 作爲實例方法,類似這樣:​​​​​​​

class ExampleRenderProps extends React.Component {  renderList=()=>{    return <List add={props.add} />  }  render() {    return (      <div>        <Provider test={this.renderList} />      </div>    )  }}

如果你無法靜態定義 prop(例如,因爲你需要關閉組件的 props 和/或 state),則 <List> 應該擴展自React.Component.

小結


其實要說的在 react 官方文檔基本都能看到,但官方文檔組織稍顯凌亂。讀者也可在讀完這篇文章後具體去查找相應官方教程。
參考文檔:

  • React官方文檔

      (http://t.cn/AiYGz4Na)

  • React Component Patterns

      (http://t.cn/EvsJ8gj)

  • React實戰:設計模式和最佳實踐

      (http://t.cn/EUy09Ml)

  • Presentational and Container Components

     (http://t.cn/RqMyfwV)
640?wx_fmt=png

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章