Vue3 Options API(傳統寫法)─ extends 完全指南
簡介
在 Vue 3 中,Options API 仍是許多既有專案與新手入門的首選寫法。除了 data、methods、computed 等常見選項外,extends 也是一個相當實用但常被忽略的功能。extends 讓我們可以把 多個元件的選項 合併到同一個元件中,達到「混入」的效果,同時保持 Options API 的直觀結構。
為什麼要學
extends?
- 重複程式碼減少:把共用的資料、方法或生命週期鉤子抽離成基礎元件,讓子元件只關注自身邏輯。
- 維護成本降低:更新共用行為只需要修改一次基礎元件。
- 提升可讀性:子元件的
export default {}只呈現與自身相關的選項,結構更清晰。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,一步步帶你掌握 extends 在 Vue3 Options API 中的正確使用方式。
核心概念
1. extends 是什麼?
在 Options API 中,extends 允許一個元件 繼承另一個元件的選項(options)。它的行為類似於 JavaScript 的 Object.assign,但會特別處理 Vue 的生命週期鉤子與合併策略(例如 data、methods、computed)。
export default {
// 直接把 baseComponent 的所有選項混入本元件
extends: baseComponent,
// 之後仍可以自行定義或覆寫
data() { return { /* ... */ } },
methods: { /* ... */ }
}
注意:
extends只會「合併」選項,不會建立原型鏈。換句話說,子元件仍是獨立的 Vue 實例,只是把父元件的選項「拷貝」過來。
2. 合併策略(Merge Strategies)
Vue 為不同的選項提供了不同的合併規則:
| 選項 | 合併方式 |
|---|---|
data |
子元件的 data 會 覆蓋 父元件的同名屬性(若有衝突)。兩者都會被呼叫,返回值以子元件為主。 |
methods、computed |
兩者會 合併,若同名則子元件的會覆寫父元件。 |
生命週期鉤子(created、mounted…) |
會 串接 執行,父元件先執行,接著子元件。 |
watch、props、components |
同樣會 合併,衝突時子元件優先。 |
了解這些策略能幫助我們預測 extends 後的行為,避免意外覆寫或遺漏。
3. 何時使用 extends 而非 mixins?
| 特性 | extends |
mixins |
|---|---|---|
| 單一繼承 | 僅能指定一個基礎元件 | 可同時加入多個 mixin |
| 合併順序 | 父 → 子(較直觀) | mixin → 元件(順序較彈性) |
| 命名衝突 | 子元件較易掌控覆寫 | 需要自行注意 mixin 的優先權 |
| 可讀性 | 類似「類別繼承」的概念,較易理解 | 多個 mixin 可能造成選項散落 |
如果你希望 明確的單一繼承關係,或是想把 整個元件作為基礎(包含 template、樣式),extends 是更合適的選擇。
程式碼範例
以下提供 5 個實用範例,從最簡單的資料繼承到結合生命週期與自訂事件。
範例 1:最簡單的資料與方法繼承
// BaseComponent.js
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
// Counter.vue
<script>
import BaseComponent from './BaseComponent.js';
export default {
name: 'Counter',
extends: BaseComponent,
// 只需要額外的 UI 邏輯
template: `
<div>
<p>計數:{{ count }}</p>
<button @click="increment">+1</button>
</div>
`
};
</script>
說明:
Counter直接繼承BaseComponent的count與increment,不需要再次宣告,保持程式碼乾淨。
範例 2:覆寫 data 中的屬性
// BaseUser.js
export default {
data() {
return {
name: 'Anonymous',
role: 'guest'
};
}
};
// AdminUser.vue
<script>
import BaseUser from './BaseUser.js';
export default {
name: 'AdminUser',
extends: BaseUser,
data() {
// 只改寫 name,role 仍保留原本的值
return {
name: 'Admin'
};
},
template: `<p>{{ name }} ({{ role }})</p>`
};
</script>
結果:
AdminUser會顯示Admin (guest),因為role沒被覆寫。
範例 3:合併生命週期鉤子
// LoggerMixin.js
export default {
created() {
console.log('LoggerMixin created');
},
mounted() {
console.log('LoggerMixin mounted');
}
};
// MyComponent.vue
<script>
import LoggerMixin from './LoggerMixin.js';
export default {
name: 'MyComponent',
extends: LoggerMixin,
created() {
console.log('MyComponent created');
},
mounted() {
console.log('MyComponent mounted');
},
template: `<div>Hello Vue3</div>`
};
</script>
執行順序:
LoggerMixin created→MyComponent createdLoggerMixin mounted→MyComponent mounted
這證明了 生命週期會依序串接,不會相互覆蓋。
範例 4:結合 computed 與 watch
// PriceBase.js
export default {
data() {
return {
price: 100,
taxRate: 0.1
};
},
computed: {
total() {
return this.price * (1 + this.taxRate);
}
},
watch: {
price(newVal) {
console.log(`price changed to ${newVal}`);
}
}
};
// DiscountedPrice.vue
<script>
import PriceBase from './PriceBase.js';
export default {
name: 'DiscountedPrice',
extends: PriceBase,
data() {
return {
discount: 0.2
};
},
computed: {
// 覆寫 total,加入折扣邏輯
total() {
const baseTotal = this.price * (1 + this.taxRate);
return baseTotal * (1 - this.discount);
}
},
template: `
<div>
<p>原價總計:{{ price * (1 + taxRate) }}</p>
<p>折扣後總計:{{ total }}</p>
</div>
`
};
</script>
重點:
computed.total在子元件被 覆寫,而watch.price仍會被保留,因為watch會合併。
範例 5:把完整元件當作基礎(含 template)
// CardBase.vue
<template>
<div class="card">
<header class="card-header"><slot name="header"></slot></header>
<section class="card-body"><slot></slot></section>
</div>
</template>
<script>
export default {
name: 'CardBase',
props: {
elevation: {
type: Number,
default: 1
}
},
computed: {
elevationClass() {
return `elevation-${this.elevation}`;
}
}
};
</script>
<style scoped>
.card { border: 1px solid #ddd; border-radius: 4px; }
.elevation-1 { box-shadow: 0 1px 3px rgba(0,0,0,.1); }
.elevation-2 { box-shadow: 0 4px 6px rgba(0,0,0,.15); }
</style>
// InfoCard.vue
<script>
import CardBase from './CardBase.vue';
export default {
name: 'InfoCard',
extends: CardBase,
// 只需要額外的 props 與樣式
props: {
title: String,
content: String
},
template: `
<CardBase :elevation="2">
<template #header>{{ title }}</template>
{{ content }}
</CardBase>
`
};
</script>
說明:
InfoCard繼承了CardBase的完整結構、樣式與計算屬性,只在template中套用CardBase並提供自己的 slot 內容。這是 組件重用 的典型寫法。
常見陷阱與最佳實踐
1. 資料衝突時的覆寫行為
- 陷阱:子元件的
data同名屬性會 直接覆寫 父元件的值,可能導致意外的狀態遺失。 - 最佳實踐:在基礎元件中盡量使用 命名空間(例如
baseCount、baseConfig),或在子元件data中使用...展開父層資料再自行調整。
data() {
return {
...this.$options.extends.data.call(this), // 取得父層資料
count: 0 // 自己的 count
};
}
2. 生命週期的執行順序
- 陷阱:誤以為子元件的
created會取代父元件,結果兩者都被呼叫,導致副作用重複(例如 API 請求)。 - 最佳實踐:在基礎元件的生命週期中僅執行共用邏輯(如設定監聽、初始化),避免在子元件中再次呼叫相同的 API。若需要條件式執行,可在父元件內檢查
this.$options.name。
3. extends 與 mixins 同時使用
- 陷阱:合併策略會變得複雜,尤其是
watch、methods同名時的優先順序不易預測。 - 最佳實踐:盡量 保持單一來源(要麼
extends,要麼mixins),或明確在文件中註明哪個選項的優先級。
4. 模板繼承的限制
- 陷阱:
extends不會自動把父元件的template合併到子元件,除非子元件在template中手動使用父元件(如範例 5)。 - 最佳實踐:若需要共用 UI,將共用的 markup 抽成 子元件(如
CardBase),再在子元件中import並使用,保持模板的可讀性。
5. TypeScript 與 extends
- 陷阱:在 TS 中,
extends的型別推斷不會自動合併,可能導致編譯錯誤。 - 最佳實踐:使用
interface或type手動擴充子元件的型別,或改用defineComponent搭配ComponentOptions來明確聲明。
import { ComponentOptions } from 'vue';
import BaseComponent from './BaseComponent';
interface MyComponent extends ComponentOptions<Vue> {}
export default {
extends: BaseComponent,
// ...其他屬性
} as MyComponent;
實際應用場景
| 場景 | 為何使用 extends |
範例簡述 |
|---|---|---|
| 表單驗證共用 | 多個表單元件需要相同的驗證規則與錯誤顯示邏輯 | 建立 FormBase 包含 data(errors)、methods.validate,子表單 extends 後只需定義欄位 |
| 統一版面框架 | 多頁面使用相同的 header、sidebar、footer 結構 | 把框架抽成 LayoutBase.vue,各頁面 extends 後在 template 裡放入 <router-view> 或自訂 slot |
| API 呼叫封裝 | 多個元件需要相同的資料取得流程(loading、error 處理) | DataFetcherBase 包含 fetchData 方法與 loading 狀態,子元件只需提供 API URL |
| 多語系字串 | 多個元件共用相同的文字資源 | I18nBase 定義 computed 文字映射,子元件 extends 後直接使用 this.t('key') |
| 自訂 UI 元件 | 複雜的 UI(如卡片、表格)在多處使用且需微調樣式 | CardBase.vue(如上範例)作為基礎,子元件 extends 後只改變 props 或 slot 內容 |
透過 extends,上述情境的 重複程式碼可以大幅減少,同時保持每個子元件的 獨立性與可測試性。
總結
extends是 Vue3 Options API 中強大的 繼承工具,能讓我們把共用的data、methods、computed、生命週期等選項一次性帶入子元件。- 了解 Vue 的 合併策略,特別是
data的覆寫與生命週期的串接順序,是避免衝突的關鍵。 - 在實務開發中,
extends最適合用於 單一基礎元件的重用(如 UI 框架、表單驗證、API 抽象層),而非取代mixins的多重混入需求。 - 使用時務必注意資料衝突、生命週期重複執行、模板繼承與 TypeScript 型別等常見陷阱,並遵循 命名空間、單一來源、明確註解 的最佳實踐。
掌握了 extends 後,你的 Vue3 專案將會變得更 模組化、可維護且易於擴充。祝開發順利,寫出更乾淨、更高效的程式碼!