本文 AI 產出,尚未審核

Vue3 Options API(傳統寫法)── Mixins 完全攻略


簡介

在 Vue.js 的開發過程中,程式碼重用 是提升開發效率與維護性的關鍵。Vue 3 仍然支援 Options API,而在 Options API 中最常被提及的重用機制,就是 mixins。mixins 允許我們把多個元件共用的 datacomputedmethods、生命週期鉤子等抽離成獨立的模組,然後在需要的元件中「混入」使用。

對於剛接觸 Vue 的新手,mixins 可能看起來像是魔法般的「自動合併」行為;但只要弄清楚它的原理與限制,就能在 大型專案 中快速避免重複程式碼、統一行為(例如全局錯誤處理、表單驗證等)。本篇文章將從概念、實作、常見陷阱到最佳實踐,完整說明如何在 Vue 3 Options API 中正確運用 mixins,讓你在實務開發中如虎添翼。


核心概念

1. 什麼是 Mixins?

Mixins 本質上是一個普通的 JavaScript 物件,裡面可以定義 Vue 元件支援的任何選項(datamethodscomputedwatch、生命週期鉤子…)。當一個元件 使用 mixins 屬性 時,Vue 會把 mixin 內的選項與元件本身的選項 合併,合併規則如下:

選項類型 合併方式
data 兩者會被 深度合併,若同名屬性會被元件本身的值覆寫
methodscomputedwatch 鍵名為基準,若衝突,元件本身的定義會 覆蓋 mixin
生命週期鉤子(createdmounted…) 串接執行,先執行 mixin 再執行元件本身的鉤子

重點:mixins 只是一種「靜態」的程式碼組合方式,執行時不會產生任何額外的實例或代理(proxy)。

2. 建立與使用 Mixins

2.1 建立一個簡單的 mixin

// src/mixins/loggerMixin.js
export const loggerMixin = {
  // 生命週期:在元件建立時印出訊息
  created() {
    console.log(`[Logger] ${this.$options.name} 已被建立`);
  },

  // 共用的方法
  methods: {
    /** 記錄訊息到 console,支援多種等級 */
    log(message, level = 'info') {
      const prefix = `[${level.toUpperCase()}]`;
      console.log(`${prefix} ${message}`);
    }
  }
};

2.2 在元件中混入

// src/components/HelloWorld.vue
<script>
import { loggerMixin } from '@/mixins/loggerMixin';

export default {
  name: 'HelloWorld',
  mixins: [loggerMixin],

  data() {
    return {
      greeting: 'Hello Vue 3!'
    };
  },

  mounted() {
    // 直接使用 mixin 中的 log 方法
    this.log(this.greeting, 'debug');
  }
};
</script>

在上例中,created 鉤子會先由 loggerMixin 執行,接著再執行 HelloWorld 本身(若有 created)。log 方法則直接掛在元件實例上供使用。

3. 多個 Mixins 與合併順序

Vue 允許在同一個元件中使用 多個 mixin,它們會依照陣列的順序依次合併。下面示範兩個 mixin 同時作用的情況:

// src/mixins/apiMixin.js
export const apiMixin = {
  methods: {
    fetchData(url) {
      return fetch(url).then(res => res.json());
    }
  }
};

// src/mixins/validationMixin.js
export const validationMixin = {
  methods: {
    isEmail(str) {
      const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return re.test(str);
    }
  }
};
// src/components/UserForm.vue
<script>
import { apiMixin } from '@/mixins/apiMixin';
import { validationMixin } from '@/mixins/validationMixin';
import { loggerMixin } from '@/mixins/loggerMixin';

export default {
  name: 'UserForm',
  mixins: [loggerMixin, apiMixin, validationMixin],

  data() {
    return {
      email: '',
      userData: null
    };
  },

  methods: {
    submit() {
      if (!this.isEmail(this.email)) {
        this.log('Email 格式錯誤', 'error');
        return;
      }
      this.fetchData(`/api/user?email=${this.email}`)
        .then(data => {
          this.userData = data;
          this.log('使用者資料已取得', 'info');
        });
    }
  }
};
</script>

說明loggerMixinapiMixinvalidationMixin 依序混入,若它們各自都有同名的 created 鉤子,執行順序會是 loggerMixin → apiMixin → validationMixin → UserForm

4. 透過 this 存取 Mixins 內的屬性

在 Options API 中,mixins 內的 datacomputedmethodsprops 都會掛在同一個 Vue 實例上,直接使用 this 即可存取:

export const timerMixin = {
  data() {
    return {
      seconds: 0,
      timerId: null
    };
  },

  created() {
    this.startTimer();
  },

  beforeUnmount() {
    clearInterval(this.timerId);
  },

  methods: {
    startTimer() {
      this.timerId = setInterval(() => {
        this.seconds++;
      }, 1000);
    }
  }
};
// 任意元件
<script>
import { timerMixin } from '@/mixins/timerMixin';

export default {
  name: 'Clock',
  mixins: [timerMixin],

  template: `<div>已經運行 {{ seconds }} 秒</div>`
};
</script>

這個範例展示了 狀態共享seconds)以及 生命週期管理createdbeforeUnmount)皆可透過 mixin 完成,元件本身只需要關注 UI。

5. 何時使用 Mixins vs. Composition API

雖然 Vue 3 推薦使用 Composition API 取代 mixins,仍有以下情況適合保留 mixins:

使用情境 為什麼選擇 mixins
既有大型 Options API 專案 直接在現有結構上加入重用模組,成本最低
跨多個元件的簡單功能(如全局 logger、簡易驗證) 只需要少量 methods/created,不必建立完整的 composable
第三方套件提供的 mixin 某些套件(例如 Vue Router v2、Vuex)仍以 mixin 方式擴充功能

程式碼範例

以下提供 五個實務上常見 的 mixin 範例,從基礎到稍微進階,幫助你快速上手。

範例 1️⃣ 全局錯誤處理 Mixin

// src/mixins/errorHandlerMixin.js
export const errorHandlerMixin = {
  data() {
    return {
      errorMessage: ''
    };
  },

  methods: {
    /** 捕捉錯誤並顯示於 UI */
    handleError(err) {
      console.error('[Error]', err);
      this.errorMessage = err.message || '發生未知錯誤';
    },

    /** 清除錯誤訊息 */
    clearError() {
      this.errorMessage = '';
    }
  }
};

在任何需要捕捉例外的元件,只要 mixins: [errorHandlerMixin],就能直接呼叫 this.handleError(err),而 UI 只要綁定 errorMessage 即可。

範例 2️⃣ 表單驗證 Mixin(支援多欄位)

// src/mixins/formValidateMixin.js
export const formValidateMixin = {
  data() {
    return {
      // 以欄位名稱為鍵,值為驗證結果
      validation: {}
    };
  },

  methods: {
    /** 設定欄位驗證結果 */
    setFieldValidity(field, isValid, message = '') {
      this.$set(this.validation, field, { isValid, message });
    },

    /** 檢查所有欄位是否都通過驗證 */
    isFormValid() {
      return Object.values(this.validation).every(v => v.isValid);
    }
  }
};
<!-- 在表單元件中使用 -->
<script>
import { formValidateMixin } from '@/mixins/formValidateMixin';

export default {
  name: 'RegisterForm',
  mixins: [formValidateMixin],

  data() {
    return {
      username: '',
      password: ''
    };
  },

  methods: {
    validateUsername() {
      const ok = this.username.length >= 4;
      this.setFieldValidity('username', ok, ok ? '' : '至少 4 個字元');
    },

    validatePassword() {
      const ok = this.password.length >= 6;
      this.setFieldValidity('password', ok, ok ? '' : '至少 6 個字元');
    },

    submit() {
      this.validateUsername();
      this.validatePassword();
      if (this.isFormValid()) {
        // 送出表單
      }
    }
  }
};
</script>

範例 3️⃣ 跨頁面的 Loading 狀態 Mixin

// src/mixins/loadingMixin.js
export const loadingMixin = {
  data() {
    return {
      isLoading: false
    };
  },

  methods: {
    /** 包裝 async 函式,使其自動切換 loading */
    async withLoading(promiseFn) {
      this.isLoading = true;
      try {
        const result = await promiseFn();
        return result;
      } finally {
        this.isLoading = false;
      }
    }
  }
};
// 使用方式
import { loadingMixin } from '@/mixins/loadingMixin';

export default {
  name: 'ArticleList',
  mixins: [loadingMixin],

  data() {
    return { articles: [] };
  },

  async created() {
    await this.withLoading(async () => {
      this.articles = await fetch('/api/articles').then(r => r.json());
    });
  }
};

範例 4️⃣ 事件總線 (Event Bus) Mixin

注意:在 Vue 3 中已不建議使用全局事件總線,但在遺留系統或簡易原型仍可透過 mixin 快速實作。

// src/mixins/eventBusMixin.js
import { mitt } from 'mitt'; // npm i mitt

const emitter = mitt();

export const eventBusMixin = {
  methods: {
    $emitEvent(event, payload) {
      emitter.emit(event, payload);
    },
    $onEvent(event, handler) {
      emitter.on(event, handler);
    },
    $offEvent(event, handler) {
      emitter.off(event, handler);
    }
  },

  beforeUnmount() {
    // 移除所有註冊的事件(防止記憶體洩漏)
    emitter.all.clear();
  }
};
// A.vue
export default {
  mixins: [eventBusMixin],
  mounted() {
    this.$onEvent('login-success', user => {
      console.log('收到登入成功通知', user);
    });
  }
};

// B.vue
export default {
  mixins: [eventBusMixin],
  methods: {
    login() {
      // 假設登入成功後
      const user = { name: 'Alice' };
      this.$emitEvent('login-success', user);
    }
  }
};

範例 5️⃣ 多語系 (i18n) Helper Mixin

// src/mixins/i18nMixin.js
export const i18nMixin = {
  methods: {
    /** 依照目前語系取得對應文字 */
    $t(key) {
      // 假設有全局的 i18n 物件
      return this.$root.$i18n ? this.$root.$i18n.t(key) : key;
    }
  }
};
<!-- 任意元件 -->
<template>
  <button>{{ $t('button.save') }}</button>
</template>

<script>
import { i18nMixin } from '@/mixins/i18nMixin';
export default {
  name: 'SaveButton',
  mixins: [i18nMixin]
};
</script>

常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
名稱衝突 Mixins 與元件本身同名的 datamethodscomputed 會被元件覆寫,導致行為不易追蹤。 命名規範:在 mixin 中使用前綴(如 mixinXxx)或將功能拆成多個小 mixin。
生命週期執行順序不明 多個 mixin 同時提供同一生命週期鉤子,執行順序依陣列順序,容易產生隱蔽的副作用。 明確排序:將重要的 side‑effect 放在最後一個 mixin,或在鉤子內自行調整執行順序。
過度依賴全域狀態 在 mixin 中直接使用 this.$storethis.$router,會讓 mixin 變成「耦合」的黑盒子。 注入式設計:將需要的依賴作為參數傳入(例如 init(store)),或使用 Composition API 重構。
測試困難 Mixins 隱藏在元件背後,單元測試時必須額外載入或 mock。 分離測試:為每個 mixin 撰寫獨立的測試檔,並在元件測試中只驗證 UI 行為。
記憶體洩漏 在 mixin 中註冊 setIntervalevent listeners,但未在 beforeUnmount 清除。 在生命週期內清理:一定在 beforeUnmount(或 unmounted)裡釋放資源。

最佳實踐總結

  1. 保持單一職責:每個 mixin 只解決一件事(例如「錯誤處理」或「Loading」),避免變成「萬用工具箱」。
  2. 使用前綴或命名空間:減少名稱衝突,例如 logInfomixinLogInfo
  3. 盡量在 created/mounted 前完成資料初始化,避免在 mounted 之後才執行重要邏輯。
  4. 在大型專案中逐步遷移到 Composition API:可以先保留核心的 mixin,未來再以 composable 替代。
  5. 文件化每個 mixin:在專案的 docs/mixins/ 目錄下建立說明文件,列出提供的屬性、使用方式與注意事項。

實際應用場景

場景 典型需求 為何選擇 Mixins
全局 API 錯誤攔截 所有 AJAX 請求失敗時顯示統一的錯誤提示。 只要在 errorHandlerMixin 中提供 handleError,所有元件只要混入即可。
多頁面表單共用驗證 登入、註冊、忘記密碼等表單需相同的欄位驗證邏輯。 formValidateMixin 把驗證規則抽離,減少重複程式碼。
長輪詢或即時資料 多個元件需要每 5 秒自動刷新資料。 timerMixin 可在 created 中啟動計時器,在 beforeUnmount 中自動清除。
全局 Loading 指示 在任何非同步操作期間顯示全局 loading spinner。 loadingMixinwithLoading 包裝所有 async 呼叫,統一管理 UI。
跨元件訊息傳遞 不同層級的元件需要互相通知(例如「使用者已登入」)。 eventBusMixin 讓元件以事件方式解耦溝通。

總結

Mixins 是 Vue 3 Options API 中 最直觀、最快速 的程式碼重用手段。透過將共用的 datamethodscomputed、生命週期鉤子等抽離成獨立模組,我們可以:

  • 減少重複程式碼,提升維護效率。
  • 統一行為(如錯誤處理、Loading、表單驗證)。
  • 在既有 Options API 專案中快速導入,不必一次性改寫為 Composition API。

然而,mixins 也有 名稱衝突、執行順序不易掌控 等缺點,使用時必須遵守命名規範、保持單一職責、在生命週期內妥善清理資源。對於大型或長期維護的專案,建議逐步將關鍵功能遷移至 Composable(Composition API)以獲得更好的型別支援與可測試性。

掌握了本文的概念與範例,你就能在 Vue 3 Options API 中自信地運用 mixins,為專案帶來更乾淨、更具可讀性的程式碼結構。祝開發順利,持續寫出高品質的 Vue 應用!