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


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

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


隨着 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.

1、容器(Container)與展現(Presentational)組件github


>>>>

概念介紹算法

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

示例編程

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

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 這個組件,當有和外部數據源進行溝通的邏輯。那麼咱們就能夠把這個組件拆成兩部分。redux

一部分專門負責和外部通訊(容器組件),一部分負責UI邏輯(展現組件)。咱們來將上面那個例子拆分看看。設計模式

容器組件:api

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>    )  }}複製代碼

展現組件:bash

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).

2、高階組件


>>>>

概念介紹

當你想複用一個組件的邏輯時,高階組件(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)複製代碼
>>>>

使用注意事項

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

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

使用高階組件後 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';}複製代碼

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

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

一般,你不須要考慮這點。但對 HOC 來講這一點很重要,由於這表明着你不該在組件的 render 方法中對一個組件應用 HOC:

render() {// 每次調用 render 函數都會建立一個新的 EnhancedComponent// EnhancedComponent1 !== EnhancedComponent2  const EnhancedComponent = enhance(MyComponent);// 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做!  return <EnhancedComponent />;}複製代碼
這不只僅是性能問題,從新掛載組件會致使該組件及其全部子組件的狀態丟失。
若是在組件以外建立 HOC,這樣一來組件只會建立一次。所以,每次 render 時都會是同一個組件。通常來講,這跟你的預期表現是一致的。在極少數狀況下,你須要動態調用 HOC.你能夠在組件的生命週期方法或其構造函數中進行調用。
三、務必複製靜態方法
有時在 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';複製代碼

四、Refs 不會被傳遞

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

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

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)

轉載請標明出處「小米雲技術」
相關文章
相關標籤/搜索