react元件抽象通識篇
為什麼會提到一個抽象元件的概念,其實我們稱其為高複用元件更好,因為其實在業務開發中很多時候會有這樣的場景,我們的某部分功能是可以共用給其他部分的,但這部分又不太可能脫離元件或者某個基準資料存在。於是,我們需要將這部分程式碼進行一定的抽象或者說設計。
mixin
混入在其他程式語言中非常常見,在es6的語法中已經提到了裝飾器的語法,其實裝飾器就是混入的基本實現。下面我們實現下js版本的mixin。
function mixins(obj,mixins){ let newObj = obj; newObj.prototype = Object.create(obj.prototype); for(let p in mixins){ if(mixins.hasOwnProperty(p)){ newObj[p] = mixins[p]; } } return newObj; } 複製程式碼
看完之後,發現其實現其實和lodash的assign以及underscore的extend方法非常類似。那麼結合react,之前的方式是我們在react的中可以定義一個mixins陣列共享一些方法。在vue中也有類似的方式。不過由於這種方式會導致不靈活的使用,已經被高階元件所代替。
class App extends React.creatClass({ mixins:[fn1], render(){ } }) 複製程式碼
高階元件
點選跳轉檢視我的另一篇文章:連結
屬性代理
屬性代理是我們最常見的使用方式,它可以將指定的屬性傳入,並返回帶有這些屬性的任意元件。點選檢視我的codesanbox地址:連結
// 包裝元件的容器 import React from "react"; export const MyContainer = WrappedComponent => class extends React.PureComponent { componentWillMount() { this.setState({ type: 1 }); } render() { return <WrappedComponent type={this.state.type} />; } }; // 具體使用 直接用函式封裝傳遞 import React from "react"; import { MyContainer } from "./MyContainer"; class Hoc extends React.PureComponent { constructor(props) { super(props); this.state = {}; } render() { let { type } = this.props; return <div>高階元件{type}</div>; } } export default MyContainer(Hoc); 複製程式碼
控制props
無論我們刪除還是編輯屬性的部分,我們都應該儘可能最高階元件的props做新的命名來防止混淆。例如我們需要新增一個新的prop.於是我們需要保留原有的屬性,這是必要的。這樣使用高階元件就可以使用新的屬性,而原有元件不使用的時候仍然是無損的。(其中物件的拓展符是很方便的,在不確定有哪些屬性或者屬性非常多的時候,非常建議使用這個語法特性)。
// 包裝元件的容器 import React from "react"; export const MyContainer = WrappedComponent => class extends React.PureComponent { componentWillMount() { } render() { const newProps = { text:1 } return <WrappedComponent {...this.props} {...newProps} />; } }; 複製程式碼
通過refs使用引用
在高階元件中,我們可以通過refs來使用WrappedComponent的引用。看上去與上面的控制屬性麼有什麼差別,實際上,每當子元件執行的時候,refs的回撥函式就會執行,它可以方便的呼叫或者讀取例項的props.換一句說法,這裡可以實現呼叫子元件的方法,除了實現部分元件鉤子,還可以根據需求靈活的進行一些方法呼叫。
覺得很沒有想法,找不到什麼場景下會有這種需求,給大家舉個例子,比如容器元件想主動呼叫子元件的某個方法或者讀取其某個值的狀態,在我做業務開發的時候,就有一種場景,使用者在容器元件的某個操作,需要主動重新整理子元件的一些資料,還有執行子元件的一些事件,按照常規方式,是沒有主動觸發這一條的。因為我們的一般的通訊是通過子元件使用父元件的回撥函式來實現的。那麼假如是這種場景,我們直接封裝一個這種需求的高階元件便可,然後在根據不斷變更的需求,去維護固定的一個或者多個高階元件。
// 包裝元件的容器 import React from "react"; export const MyContainer = WrappedComponent => class extends React.PureComponent { proc(WrappedComponentInstance) { WrappedComponentInstance &&WrappedComponentInstance.method(); } render() { const props = Object.assign({},this.props,{ref:this.proc.bind(this)}); return <WrappedComponent {...props}/>; } }; 複製程式碼
抽象state
這一層設計的原因是我們在考慮設計我函式元件還是狀態元件時經常考量的一點,在react的元件設計思想中,我們判斷的核心標準是元件本身是否有狀態,是否需要根據資料的狀態靈活的變化,也就是是否對setState的更新檢視操作有強依賴,是否是多次渲染,如果有,那麼是建議的使用帶狀態元件,否則建議你使用無狀態元件,也就是函式元件。
但是我們在開發某些業務時,發現耦合了太多互動邏輯以及狀態邏輯在元件中,而這些程式碼設計是可重用的。比如我們都是展示使用者資訊,都是點選某個位置,更新使用者資訊,只是展示的位置以及渲染有差異。那麼我們該如何做?那就是抽象出這部分state,原來的元件變為函式元件。(如果你只有一個元件中這樣,可以不必提取,如果出現多個,建議這樣使用高階元件抽象一次)。
// 包裝元件的容器 import React from "react"; export const MyContainer = WrappedComponent => // 在這個元件中完成所有的資料變更 和 互動邏輯,完成後屬性傳遞給渲染元件即可 class extends React.PureComponent { constructor(props){ super(props); this.state ={ //xxx } } method1(){ } method2(){ } render() { const newProps = {}; return <WrappedComponent {...this.props} {...newProps} />; } }; 複製程式碼
使用其他元素包裹元件
實際上我們除了上面的用途,還可以根據自己的需要去靈活的對元件的樣式,外層空間,等任意的自定義。比如我們經常需要對一些元件規定它的大小位置,或者就是指定一些有規律的className.剩下的空間自行發揮,這裡只是提醒大家高階元件有如此的一個使用場景。
// 封裝函式裡的返回class render函式裡 render(){ return <div className="side-bar"> <WrappedComponentclassName="side-bar-content" {...this.props} {...newProps} /> </div>; } 複製程式碼
反向繼承
說的簡單一點就是在封裝高階元件的時候對包裝元件使用繼承。其基本的寫法如下:
const MyContainer = WrapperComp =>( class extends WrapperComp{ render(){ return super.render() } } ) 複製程式碼
這種方法與屬性代理不同,它可以通過super方法來獲取元件的屬性以及方法。下面會詳細說明其帶來的兩個特點:渲染劫持以及state控制。在瞭解這個之前,我們有必要了解下其生命週期以及其會帶來的影響。
備註:同名的方法以及生命週期,如果你再次申明會被覆蓋。你可以通過我寫的hocSuper的例子檢視這個問題。
渲染劫持
說的直白一點就是控制如何渲染原來已經確定好的輸出渲染的某部分,我們在許多業務中其實已經加入了類似的程式碼,比如 hasRight && .只不過現在我們的場景是把這部分的程式碼用在反向繼承中的元件上。它的程式碼可能是下面這樣的。
render(){ return( <div> {hasRight ? return super.render(): <span>無許可權提示文字</span>} </div> ) } 複製程式碼
當然上面的控制看起來非常簡單,沒有什麼華麗的技巧,我們更需要的可能是下面這樣的渲染劫持。拿到渲染的樹之後,我們改變其某些節點的狀態。
render(){ const elementsTree = super.render(); let newProps = {}; const props = Object.assign({},elementsTree.props,newProps); const newElementsTree= React.cloneElement(elementsTree,props,props.children); return newElementsTree } 複製程式碼
控制state
我們可在高階元件中刪除或者修改元件的state,但為了避免一些低階的問題,**我們不建議直接修改甚至刪除其原具有的state,更建議的方式是新建以及重新命名。**如果你不確認原來的元件具有哪些屬性以及方法,可以嘗試著用JSON.stringify來序列化展示,當然更好的方式你可以通過開發工具比如devTool去檢視這些。
元件命名
當我們用高階元件時,我們失去了原來元件的名字,我們可以通過簡單的命名規則為 HOC${getDisplayName(WrappedComponent)}
來實現,其中getDisplayName函式寫法可以參考下面的方式:或可以使用 recompose 庫,它已經幫我們實現了相應的方法。
function getDisplayName(WrappedComponent){ return WrappedComponent.dispalyName || WrappedComponent.name || 'Component'; } // recompose 方法 // Any Recompose module can be imported individually import getDisplayName from 'recompose/getDisplayName' ConnectedComponent.displayName = `connect(${getDisplayName(BaseComponent)})` // Or, even better: import wrapDisplayName from 'recompose/wrapDisplayName' ConnectedComponent.displayName = wrapDisplayName(BaseComponent, 'connect') 複製程式碼
元件引數
有很多時候,我們給高階元件新增一些靈活的引數,而不僅僅是使用元件作為引數,那麼我們多一層巢狀即可實現。
import React, { Component } from 'React'; function HOCFactoryFactory(...params) { // 可以做一些改變 params 的事 return function HOCFactory(WrappedComponent) { return class HOC extends Component { render() { return <WrappedComponent {...this.props} />; } } } //當你使用的時候,可以這麼寫: HOCFactoryFactory(params)(WrappedComponent) // 或者 @HOCFatoryFactory(params) class WrappedComponent extends React.Component{} 複製程式碼
混合與高階元件的對比

高階元件
高階元件屬於函數語言程式設計(functional programming)思想,對於被包裹的元件時不會感知到高階元件的存在,而高階元件返回的元件會在原來的元件之上具有功能增強的效果。
mixin 退出的原因
雖然我們知道mixin被慢慢的廢棄,但是我們還是有必要了解下用這個的問題是那些?而顯然新的高階元件是能解決這些問題的。這也將有益於我們理解一些高階元件設計的優勢。
- 破壞了原有元件的封裝,可能會增加其他的狀態侵入以及可能的混入依賴關係
- 不同的mixins存在的命名衝突
- 增加了元件的複雜性:混入了各種方法以及為生命週期可以添加了不同方法
組合元件開發
它指的是當我們進行一些高階元件的開發的時候,發現很多時候不斷的去調整屬性,同時為了減少對已經在使用的部分,一般是高階元件的屬性都是增加,累加下去會導致配置了很多可能無用的屬性。
元件再分離
也就是將元件進一步細分,每一個元件都可以儘可能的原子化,然後稍高階的元件通過組裝完成我們所看到的一個基本元件。比如下圖理解下:

實際上這種思想,我們也偶爾會使用,只不過沒有形成一些固定的思維設計思路。實際上,不管我們是設計的可重用的元件,還是說就是寫業務元件,頁面元件,我們都應該考慮元件的拆分。讓每個元件內部儘可能的細化,拆分成若干具有單獨解耦的獨立渲染的邏輯或者子元件。
我們在ant的input元件中,可以看到其元件目錄每一個文件上基本的元件都是有入口檔案,若干的小元件拼裝而成。

我們在寫元件的時候也要有這樣的思維模式,比如一個賬單的顯示,原本是這樣的:
// old way render (){ return ( <div> <h2>標題</h2> // 列表資料的渲染 {list.map(item)=>(<div className="m-docItem"> <img src={item.headimg}/> <span>{item.docName}</span> <p>{item.resume}</p> </div>)} </div> ) } //new way as a class fun renderDocItem(list){ return ({ list.map(item)=>(<div className="m-docItem"> <img src={item.headimg}/> <span>{item.docName}</span> <p>{item.resume}</p> </div>) }) } // new way as a single fun comp export const RenderDocItem(props){ const {list}= props; return ({ list.map(item)=>(<div className="m-docItem"> <img src={item.headimg}/> <span>{item.docName}</span> <p>{item.resume}</p> </div>) }) } render(){ return( <div> <h2>標題</h2> // 方法的方式 {this.renderDocItem(list)} // 函式元件的方式 <RenderDocItem list={list}/> </div> ) } 複製程式碼
我們在庫中也經常看到這樣的程式碼維護方式:養成這樣的編碼習慣,會讓你的程式碼可維護性大大的增強。

邏輯再抽象
比如我們針對輸入框的值進行監聽之後執行某個特定的事件,而這個事件本身發現可重用的位置很多,和輸入框本身是沒有重度關聯的,那麼針對這個場景,如果你有強迫症,可以抽象一波。
// 完成 SearchInput 與 List 的互動 const searchDecorator = WrappedComponent => { class SearchDecorator extends Component { constructor(props) { super(props); this.handleSearch = this.handleSearch.bind(this); } handleSearch(keyword) { this.setState({ data: this.props.data, keyword, }); this.props.onSearch(keyword); } render() { const { data, keyword } = this.state; return ( <WrappedComponent {...this.props} data={data} keyword={keyword} onSearch={this.handleSearch} /> ); } } return SearchDecorator; } 複製程式碼
圖解組合元件開發的架構

小結
通過本文希望我們能瞭解到高階元件的一些基本設計思路,能解決的元件組合的痛點。使用react越久我們越會發現,對於一個比較複雜的系統,如果你有特別思考過元件的可重用這個問題,而不僅僅是一個頁面一個元件,附加基本的ui框架,設計好這些元件的組合方式,如何抽離等都是一個很考驗你能力的部分。