本文 AI 產出,尚未審核

Vue3 Options API(傳統寫法)─ extends 完全指南

簡介

在 Vue 3 中,Options API 仍是許多既有專案與新手入門的首選寫法。除了 datamethodscomputed 等常見選項外,extends 也是一個相當實用但常被忽略的功能。extends 讓我們可以把 多個元件的選項 合併到同一個元件中,達到「混入」的效果,同時保持 Options API 的直觀結構。

為什麼要學 extends

  1. 重複程式碼減少:把共用的資料、方法或生命週期鉤子抽離成基礎元件,讓子元件只關注自身邏輯。
  2. 維護成本降低:更新共用行為只需要修改一次基礎元件。
  3. 提升可讀性:子元件的 export default {} 只呈現與自身相關的選項,結構更清晰。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,一步步帶你掌握 extends 在 Vue3 Options API 中的正確使用方式。


核心概念

1. extends 是什麼?

在 Options API 中,extends 允許一個元件 繼承另一個元件的選項(options)。它的行為類似於 JavaScript 的 Object.assign,但會特別處理 Vue 的生命週期鉤子與合併策略(例如 datamethodscomputed)。

export default {
  // 直接把 baseComponent 的所有選項混入本元件
  extends: baseComponent,
  // 之後仍可以自行定義或覆寫
  data() { return { /* ... */ } },
  methods: { /* ... */ }
}

注意extends 只會「合併」選項,不會建立原型鏈。換句話說,子元件仍是獨立的 Vue 實例,只是把父元件的選項「拷貝」過來。

2. 合併策略(Merge Strategies)

Vue 為不同的選項提供了不同的合併規則:

選項 合併方式
data 子元件的 data覆蓋 父元件的同名屬性(若有衝突)。兩者都會被呼叫,返回值以子元件為主。
methodscomputed 兩者會 合併,若同名則子元件的會覆寫父元件。
生命週期鉤子(createdmounted…) 串接 執行,父元件先執行,接著子元件。
watchpropscomponents 同樣會 合併,衝突時子元件優先。

了解這些策略能幫助我們預測 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 直接繼承 BaseComponentcountincrement,不需要再次宣告,保持程式碼乾淨。

範例 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>

執行順序

  1. LoggerMixin createdMyComponent created
  2. LoggerMixin mountedMyComponent mounted

這證明了 生命週期會依序串接,不會相互覆蓋。

範例 4:結合 computedwatch

// 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 同名屬性會 直接覆寫 父元件的值,可能導致意外的狀態遺失。
  • 最佳實踐:在基礎元件中盡量使用 命名空間(例如 baseCountbaseConfig),或在子元件 data 中使用 ... 展開父層資料再自行調整。
data() {
  return {
    ...this.$options.extends.data.call(this), // 取得父層資料
    count: 0 // 自己的 count
  };
}

2. 生命週期的執行順序

  • 陷阱:誤以為子元件的 created 會取代父元件,結果兩者都被呼叫,導致副作用重複(例如 API 請求)。
  • 最佳實踐:在基礎元件的生命週期中僅執行共用邏輯(如設定監聽、初始化),避免在子元件中再次呼叫相同的 API。若需要條件式執行,可在父元件內檢查 this.$options.name

3. extendsmixins 同時使用

  • 陷阱:合併策略會變得複雜,尤其是 watchmethods 同名時的優先順序不易預測。
  • 最佳實踐:盡量 保持單一來源(要麼 extends,要麼 mixins),或明確在文件中註明哪個選項的優先級。

4. 模板繼承的限制

  • 陷阱extends 不會自動把父元件的 template 合併到子元件,除非子元件在 template 中手動使用父元件(如範例 5)。
  • 最佳實踐:若需要共用 UI,將共用的 markup 抽成 子元件(如 CardBase),再在子元件中 import 並使用,保持模板的可讀性。

5. TypeScript 與 extends

  • 陷阱:在 TS 中,extends 的型別推斷不會自動合併,可能導致編譯錯誤。
  • 最佳實踐:使用 interfacetype 手動擴充子元件的型別,或改用 defineComponent 搭配 ComponentOptions 來明確聲明。
import { ComponentOptions } from 'vue';
import BaseComponent from './BaseComponent';

interface MyComponent extends ComponentOptions<Vue> {}
export default {
  extends: BaseComponent,
  // ...其他屬性
} as MyComponent;

實際應用場景

場景 為何使用 extends 範例簡述
表單驗證共用 多個表單元件需要相同的驗證規則與錯誤顯示邏輯 建立 FormBase 包含 dataerrors)、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 中強大的 繼承工具,能讓我們把共用的 datamethodscomputed、生命週期等選項一次性帶入子元件。
  • 了解 Vue 的 合併策略,特別是 data 的覆寫與生命週期的串接順序,是避免衝突的關鍵。
  • 在實務開發中,extends 最適合用於 單一基礎元件的重用(如 UI 框架、表單驗證、API 抽象層),而非取代 mixins 的多重混入需求。
  • 使用時務必注意資料衝突、生命週期重複執行、模板繼承與 TypeScript 型別等常見陷阱,並遵循 命名空間、單一來源、明確註解 的最佳實踐。

掌握了 extends 後,你的 Vue3 專案將會變得更 模組化、可維護且易於擴充。祝開發順利,寫出更乾淨、更高效的程式碼!