軟件編程有一個重要的原則是 D.R.Y(Don't Repeat Yourself),講的是儘可能複用代碼和邏輯,減小重複。組件擴展能夠避免重複代碼,更易於快速開發和維護。那麼,擴展 Vue 組件的最佳方法是什麼?html
Vue 提供了很多 API 和模式來支持組件複用和擴展,你能夠根據本身的目的和偏好來選擇。vue
本文介紹幾種比較常見的方法和模式,但願對你有所幫助。react
擴展組件是否必要
擴展每每經過繼承基礎組件來達到功能複用的目的。要知道,全部的組件擴展方法都會增長複雜性和額外代碼,有時候還會增長性能消耗。經驗告訴咱們,組合模式優於繼承。編程
所以,在決定擴展組件以前,最好先看看有沒有其餘更簡單的設計模式能完成目標。設計模式
下面幾種模式一般足夠替代擴展組件了:數組
-
props
配合模板邏輯微信 -
slot 插槽app
-
JavaScript 工具函數less
props 配合模板邏輯
最簡單的方法是經過props
結合模板條件渲染,來實現組件的多功能。ide
好比經過 type
屬性:
MyVersatileComponent.vue
<template> <div class="wrapper"> <div v-if="type === 'a'">...</div> <div v-else-if="type === 'b'">...</div> <!--etc etc--> </div> </template> <script> export default { props: { type: String }, ... } </script>
使用組件的時候傳不一樣的type
值就能實現不一樣的結果。
// *ParentComponent.vue*
<template>
<MyVersatileComponent type="a" /> <MyVersatileComponent type="b" /> </template>
若是出現下面兩種狀況,就說明這種模式不適用了,或者用法不對:
-
組件組合模式把狀態和邏輯分解成原子部分,從而讓應用具有可擴展性。若是組件內存在大量條件判斷,可讀性和可維護性就會變差。
-
props 和模板邏輯的本意是讓組件動態化,可是也存在運行時資源消耗。若是你利用這種機制在運行時解決代碼組合問題,那是一種反模式。
slot(插槽)
另外一種可避免組件擴展的方式是利用 slots(插槽),就是讓父組件在子組件內設置自定義內容。
// *MyVersatileComponent.vue* <template> <div class="wrapper"> <h3>Common markup</div> <slot /> </div> </template>
// *ParentComponent.vue* <template> <MyVersatileComponent> <h4>Inserting into the slot</h4> </MyVersatileComponent> </template>
渲染結果:
<div class="wrapper"> <h3>Common markup</div> <h4>Inserting into the slot</h4> </div>
這種模式有一個潛在約束, slot 內的元素從屬於父組件的上下文,在拆分邏輯和狀態時可能不太天然。scoped slot
會更靈活,後面會在無渲染組件一節裏提到。
JavaScript 工具函數
若是隻須要在各組件之間複用獨立的函數,那麼只須要抽取這些 JavaScript 模塊就好了,根本不須要用到組件擴展模式。
JavaScript 的模塊系統是一種很是靈活和健壯的代碼共享方式,因此你應該儘量地依靠它。
MyUtilityFunction.js
export default function () { ... }
MyComponent.vue
import MyUtilityFunction from "./MyUtilityFunction"; export default { methods: { MyUtilityFunction } }
擴展組件的幾種模式
若是你已經考慮過以上幾種簡單的模式,但這些模式還不夠靈活,沒法知足需求。那麼就能夠考慮擴展組件了。
擴展 Vue 組件最流行的方法有如下四種:
-
Composition API
-
mixin
-
高階組件(HOC)
-
無渲染組件
每一種方法都有其優缺點,根據使用場景,或多或少都有適用的部分。
Composition API
組件之間共享狀態和邏輯的最新方案是 Composition API。這是 Vue 3 推出的 API,也能夠在 Vue 2 裏當插件使用。
跟以前在組件定義配置對象裏聲明data
,computed
,methods
等屬性的方式不一樣,Composition API 經過一個 setup
函數聲明和返回這些配置。
好比,用 Vue 2 配置屬性的方式聲明 Counter 組件是這樣的:
Counter.vue
<template> <button @click="increment"> Count is: {{ count }}, double is: {{ double }} </button> <template> <script> export default { data: () => ({ count: 0 }), methods: { increment() { this.count++; } }, computed: { double () { return this.count * 2; } } } </script>
用 Composition API 重構這個組件,功能徹底同樣:
Counter.vue
<template><!--as above--><template> <script> import { reactive, computed } from "vue"; export default { setup() { const state = reactive({ count: 0, double: computed(() => state.count * 2) }); function increment() { state.count++ } return { count, double, increment } } } </script>
用 Composition API 聲明組件的主要好處之一是,邏輯複用和抽取變得很是輕鬆。
進一步重構,把計數器的功能移到 JavaScript 模塊 useCounter.js
中:
useCounter.js
import { reactive, computed } from "vue"; export default function { const state = reactive({ count: 0, double: computed(() => state.count * 2) }); function increment() { state.count++ } return { count, double, increment } }
如今,計數器功能能夠經過setup
函數無縫引入到任意 Vue 組件中:
MyComponent.vue
<template><!--as above--></template> <script> import useCounter from "./useCounter"; export default { setup() { const { count, double, increment } = useCounter(); return { count, double, increment } } } </script>
Composition 函數讓功能模塊化、可重用,是擴展組件最直接和低成本的方式。
Composition API 的缺點
Composition API 的缺點其實不算什麼——可能就是看起來有點囉嗦,而且新的用法對一些 Vue 開發者來講有點陌生。新技術總有個適應的過程,早晚會大面積應用。
mixin
若是你還在用 Vue 2,或者只是喜歡用配置對象的方式定義組件功能,能夠用 mixin 模式。mixin 把公共邏輯和狀態抽取到單獨的對象,跟使用 mixin 的組件內部定義對象合併。
咱們繼續用以前的Counter組件例子,把公共邏輯和狀態放到CounterMixin.js模塊中。
CounterMixin.js
export default { data: () => ({ count: 0 }), methods: { increment() { this.count++; } }, computed: { double () { return this.count * 2; } } }
使用 mixin 也很簡單,只要導入對應模塊並在mixins
數組裏加上變量就行。組件初始化時會把 mixin 對象與組件內部定義對象合併。
MyComponent.vue
import CounterMixin from "./CounterMixin"; export default { mixins: [CounterMixin], methods: { decrement() { this.count--; } } }
選項合併
若是組件內的選項跟 mixin 衝突怎麼辦?
好比,給組件定義一個自帶的increment
方法,哪一個優先級更高呢?
MyComponent.vue
import CounterMixin from "./CounterMixin"; export default { mixins: [CounterMixin], methods: { // 自帶的 `increment`` 方法會覆蓋 mixin 的`increment` 嗎? increment() { ... } } }
這個時候就要說到 Vue 的合併策略了。Vue 有一系列的規則,決定了如何處理同名選項。
一般,組件自帶的選項會覆蓋來自 mixin 的選項。但也有例外,好比同類型的生命週期鉤子,不是直接覆蓋,而是都放進數組,按順序執行。
你也能夠經過 自定義合併策略 改變默認行爲。
mixin 的缺點
做爲擴展組件的一種模式,mixin 對於簡單的場景還算好用,一旦規模擴大,問題就來了。不只須要注意命名衝突問題(尤爲是第三方 mixin),使用了多個 mixin 的組件,很難搞清楚某個功能到底來自於哪裏,定位問題也比較困難。
高階組件
高階組件(HOC)是從 React 借用的概念,Vue 也能使用。
爲了理解這個概念,咱們先拋開組件,看看兩個簡單的 JavaScript 函數,increment
和 double
。
function increment(x) { return x++; } function double(x) { return x * 2; }
假設咱們想給這兩個函數都加一個功能:在控制檯輸出結果。
爲此,咱們能夠用高階函數模式,新建一個 addLogging
函數,接受函數做爲參數,並返回一個帶有新增功能的函數。
function addLogging(fn) { return function(x) { const result = fn(x); console.log("The result is: ", result); return result; }; } const incrementWithLogging = addLogging(increment); const doubleWithLogging = addLogging(double);
組件如何利用這種模式呢?相似地,咱們建立一個高階組件來渲染Counter組件,同時添加一個decrement
方法做爲實例屬性。
實際代碼比較複雜,這裏只給出僞代碼做爲示意:
import Counter from "./Counter"; // 僞代碼 const CounterWithDecrement => ({ render(createElement) { const options = { decrement() { this.count--; } } return createElement(Counter, options); } });
HOC 模式比 mixin 更簡潔,擴展性更好,可是代價是增長了一個包裹組件,實現起來也須要技巧。
無渲染組件
若是須要在多個組件上使用相同的邏輯和狀態,只是展現方式不一樣,那麼就能夠考慮無渲染組件模式。
該模式須要用到兩類組件:邏輯組件用於聲明邏輯和狀態,展現組件用於展現數據。
邏輯組件
仍是回到Counter的例子,假設咱們須要在多個地方重用這個組件,可是展現方式不一樣。
建立一個CounterRenderless.js 用於定義邏輯組件,包含邏輯和狀態,可是不包含模板,而是經過 render
函數聲明 scoped slot
。
scoped slot
暴露三個屬性給父組件使用:狀態count
,方法increment
和計算屬性 double
。
CounterRenderless.js
export default { data: () => ({ count: 0 }), methods: { increment() { this.count++; } }, computed: { double () { return this.count * 2; } }, render() { return this.$scopedSlots.default({ count: this.count, double: this.double, increment: this.toggleState, }) } }
這裏的scoped slot
是這種模式裏邏輯組件的關鍵所在。
展現組件
接下來是展現組件,做爲無渲染組件的使用方,提供具體的展現方式。
全部的元素標籤都包含在scoped slot
裏。能夠看到,這些屬性在使用上跟模板直接放在邏輯組件裏沒什麼兩樣。
CounterWithButton.vue
<template> <counter-renderless slot-scope="{ count, double, increment }"> <div>Count is: {{ count }}</div> <div>Double is: {{ double }}</div> <button @click="increment">Increment</button> </counter-renderless> </template> <script> import CounterRenderless from "./CountRenderless"; export default { components: { CounterRenderless } } </script>
無渲染組件模式很是靈活,也容易理解。可是,它沒有前面那幾種方法那麼通用,可能只有一種應用場景,那就是用於開發組件庫。
模板擴展
上面的 API 也好,設計模式也罷,都有一種侷限性,就是沒法擴展組件的模板。Vue 在邏輯和狀態方面有辦法重用,可是對於模板標籤就無能爲力了。
有一種比較 hack 的方式,就是利用 HTML 預處理器,好比 Pug,來處理模板擴展。
第一步是建立一個基礎模板.pug文件,包含公共的頁面元素。還要包含一個 block input
,做爲模板擴展的佔位符。
BaseTemplate.pug
div.wrapper h3 {{ myCommonProp }} <!--common markup--> block input <!--extended markup outlet -->
爲了能擴展這個模板,須要安裝 Vue Loader 的 Pug 插件。而後就能夠引入基礎模板並利用block input
語法替換佔位部分了:
MyComponent.vue
<template lang="pug"> extends BaseTemplate.pug block input h4 {{ myLocalProp }} <!--gets included in the base template--> </template>
一開始你可能會認爲它跟 slot 的概念是同樣的,可是有個區別,這裏的基礎模板不屬於任何單獨的組件。它在編譯時跟當前組件合併,而不是像 slot 那樣是在運行時替換。
參考資料:
-
https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html
-
https://adamwathan.me/renderless-components-in-vuejs/
順手點「在看」,天天早下班;轉發加關注,共奔小康路~
本文分享自微信公衆號 - 1024譯站(trans1024)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。