漫談組件復用
來自專欄餓了么前端
俗話說「懶是程序員的美德」。在越來越注重前端工程化的今天,「Ctrl+C」、「Ctrl+V」的代碼,雖然用起來一時爽,一旦需要修改就如同面臨火葬場。如何「懶」出效率,是值得思考的問題。減少代碼的拷貝,增加封裝復用能力,實現可維護、可復用的代碼,無疑是我所認為的「懶」的高級境界。鑒於筆者之前使用 React 偏多,進入餓了么後也逐步使用了不少 Vue 進行開發,所以就藉此機會,談談在 React 和 Vue 中各種基於組件的復用與實現方式。
Mixin 混入模式
最原始的一種復用方式應該就是 Mixin。通過將公用邏輯封裝為一個 Mixin,通過注入的方式進行組件間的復用。「ps: 該方式不僅用於組件,也流行於各種 css 預處理器中」。
在 React 中,通過 React.createClass() 方式創建的組件可使用 Mixin 模式,而在 ES6 的偽類模式下,並不支持 Mixin 模式,官方推薦用組合或者高階組件方式實現復用,廢話不多說,使用方式如下:
//mixinconst mixinPart = { mixinFunc() { console.log(this mixin!); return this mixin!; }};const Contacts = React.createClass({ mixins: [mixinPart], render() { return ( <div>{this.mixinFunc()}</div> ); }});// => this mixin!;
而在 Vue 中,使用邏輯類似:
//mixinconst mixinPart = { created() { console.log(this mixin!); return this mixin!; }};const Component = Vue.extend({ mixins: [mixinPart]});const component = new Component(); // => "this mixin!"
Mixin 模式給予組件公共抽象與復用能力,但另一方面也具有大量的局限性。由於 Mixin 是侵入式的,因此修改了 Mixin 相當於修改了原組件。其次,在混入過程中,對於相同鍵值對象與函數的相互覆蓋與合併,容易導致各種意外產生。因此使用過程中必須對 Mixin 內部實現有一定了解。強大的靈活性導致了在大型項目中 Mixin 的難維護。
高階組件
高階組件(High Order Component)這個概念最早是 React 社區提出,借鑒函數式中的高階函數,提出通過傳入一個組件,操作後返回一個新組件的方式進行復用。
在 React 中的使用非常便捷,官方博客中就有相關介紹:
const HOC = (WrappedComponent) => { const HOC_Component = (props) => { return ( <React.Fragment> <WrappedComponent {...props} name="WrappedComponent" /> <div>This comes from HOC Component</div> </React.Fragment> ); }; HOC_Component.displayName = HOC_Component; return HOC_Component;}const Component = (props) => { return <div>This message comes from Component: {props.name}</div>;}const Result = HOC(Component);ReactDOM.render(<Result />, document.getElementById(root));// => This message comes from Component: WrappedComponent// => This comes from HOC Component
Vue 雖然沒有官方示例,但與 React 進行類比,Vue 中的組件最終的展現形式是函數,但在過程中,實際上是一個個對象。因此,Vue 中的高階組件,應當是傳入一個對象,最後傳出一個對應對象。我們可以簡單實現個上例對應的 HOC 功能:
const HOC = (WrappedComponent) => { return { components: { wrapped-component: WrappedComponent }, template: ` <div> <wrapped-component name="WrappedComponent" v-bind="$attrs" /> <div>This comes from HOC Component</div> </div> ` };}const Component = { props: [name], template: <div>This message comes from Component: {{ name }}</div>};new Vue(HOC(Component)).$mount(#root)// => This message comes from Component: WrappedComponent// => This comes from HOC Component
高階組件用途十分廣泛,主要可以分為屬性代理和反向繼承兩種。
屬性代理具體為高階組件可以直接獲取外部傳入的參數,根據需求完成變更後重新傳給被包含的組件。如上例中就在原始 props 基礎上為 WrappedComponent 增加了一個 name 屬性,同時在原始渲染基礎上增添了一行信息渲染。
// 最基本的反向繼承const HOC = (WrappedComponent) => { return class extends WrappedComponent { render() { return super.render(); } }}
反向繼承因為繼承於 WrappedComponent,因而能夠獲取其 state, render 等各種組件數據,從而做到對組件的渲染和 state 狀態等的干預。反向繼承雖然在日常使用中遇到情況較少,但無疑是高階組件中筆者認為的一個閃光點 ( 貌似其它方式中暫時沒有可以替代的方案 )。例如,在 Vue 中有著 keep-alive 作為組件緩存,而在 React 中官方暫無類似功能,應用 data => view 的原則,一個常用的替代實現是進行狀態保存,然後在需要的時候進行狀態還原,在這種情況下,反向繼承就是一個很好的工具。
const withStateCached = (WrappedComponent) => { return class extends WrappedComponent { static getDerivedStateFromProps(nextProps, state) { // 進行數據的存儲等 } componentDidMount() { // 進行緩存數據的讀取 } render() { return super.render(); } }}
在筆者的實際項目中,更多的把高階組件看作是一個組件工廠或者裝飾者模式的應用,例如對一個基礎表格元素進行多次的高階組件的包裝,添加分頁、工具欄等功能,形成一個個更符合具體業務需求的新組件,達到組件復用的目的。當然,高階組件也不是全能的,首先其對於業務耦合度較高,更適合封裝一些日常業務中常用的組件。其次最重要的弊端是因為內部產生的的 Props 值固定,容易被外部傳入值覆蓋。如例子中,當外部也傳入了一個 name 屬性值時,就會根據組件的寫法產生不同的覆蓋方式而導致錯誤。
由於篇幅限制,這裡只粗略地介紹了一些基礎使用,要更深入理解可以參考這篇文章。
渲染屬性/函數子組件
為了解決高階組件存在的問題,一種新的「Render Props」的方案被提出。該方案提供了一個叫做 render 的函數作為 Props 參數傳入,在內部處理完畢後,將所需的組件信息,數據作為 render 的參數傳出,從而實現更加靈活的復用邏輯。
const RenderProps = ({ render, ...props }) => render(props, RenderPropComponent);const Component = () => ( <RenderProps render={(originProps, componentName) => (<div>From {componentName}</div>)} />);ReactDOM.render(<Component />, document.getElementById(root));// => From RenderPropComponent
在該例中,我們通過 render 函數傳入了原 Props 和一個新的 name 屬性,在實際使用中,重新命名 name 為 componentName,由此避開了高階組件的弊端。
由此理念,在 React 中,延伸出函數子組件的概念,將 children 作為函數使用,更加貫徹了一切皆為組件的概念。同時在 [email protected] 版本中,FB 官方的 Context 新 API 的實現也採用了函數子組件的方式。
const RenderProps = ({ children, ...props }) => children(props, name = RenderPropComponent);const Component = () => (<RenderProps> {(originProps, componentName) => (<div>From {componentName}</div>)}</RenderProps>);ReactDOM.render(<Component />, document.getElementById(root));// => From RenderPropComponent
而在 [email protected] 後的版本中,slot-scope 的概念也有點渲染屬性的影子。
const RenderProps = { template: `<div><slot v-bind="{ name: RenderPropComponent }"></slot></div>`};const vm = new Vue({ el: #root, components: { render-props: RenderProps }, template: ` <render-props> <template slot-scope="{ name }"> <div>From Component</div> From {{ name }} </template> </render-props> `});// => From Component// => From RenderPropComponent
組件注入
組件注入(Component Injection)的概念有些類似渲染屬性,都是傳遞一個類似 render 的函數屬性,區別在於組件注入將該函數作為 React 中的無狀態組件使用,而非原始的函數。
const RenderProps = ({ Render, ...props }) => <Render {...props} name=RenderPropComponent />;const Component = () => (<RenderProps Render={({ name }) => (<div>From {name}</div>)} />);ReactDOM.render(<Component />, document.getElementById(root));// => From RenderPropComponent
與渲染屬性相比,組件注入能在 devTool 的組件樹上直觀的展現出內嵌的組件結構。但在另一方面,由於所有屬性都被打包成了 props 傳出,反而失去了渲染屬性的多參數的靈活性。
總結
組件復用的方式如今可謂是多種多樣,筆者認為也並不存在什麼所謂的最佳實踐,結合具體場景和個人喜好的使用不同方案,遠離「Ctrl+C」「Ctrl+V」程序員,才是打造可復用可維護的良好組件、項目的最優選擇。
推薦閱讀:
