本文 AI 產出,尚未審核

Vue3 Options API(傳統寫法)

主題:provide / inject 在 Options API 中的使用


簡介

在大型 Vue 應用程式裡,資料的傳遞往往不只局限於父子層級的直接傳遞。當組件層級變深、或是需要在「兄弟」或「遠端」的組件之間共享狀態時,使用 props / $emit 會變得笨拙且難以維護。Vue 3 為此提供了 provide / inject 這兩個 API,讓開發者可以在「祖先」組件一次性提供資料,讓「任意子孫」組件直接取得,而不必逐層傳遞。

本篇文章聚焦於 Options API(即 Vue 2 風格的寫法)在 Vue 3 中的 provide / inject 使用方式。即使你已經熟悉 Composition API,了解 Options API 的實作細節仍然對於維護舊有專案或在團隊中協作時非常重要。


核心概念

1. 為什麼要使用 provide / inject?

  • 跨層級資料共享:一次性提供,子孫任意取用。
  • 解耦合:提供者不需要知道哪個組件會注入,降低耦合度。
  • 插件與第三方庫:許多 Vue 插件(如 Vue Router、Vuex)內部都是透過 provide / inject 實作的。

注意:provide / inject 並非全域狀態管理(如 Vuex),它的作用範圍僅限於「同一個根組件樹」之內。

2. Options API 中的 provide

在 Options API 中,provide 可以是:

  1. 物件形式:直接回傳一個物件,鍵值會被自動轉成字串作為注入鍵。
  2. 函式形式:回傳物件,允許在執行時動態取得 this(即組件實例),因此可以提供 reactive 資料。
export default {
  // 1. 物件寫法(靜態值)
  provide: {
    foo: 'bar',
    count: 0
  },

  // 2. 函式寫法(可取得 this,支援 reactive)
  provide() {
    return {
      // 直接回傳 this.someData,若是 reactive 會保持響應式
      user: this.currentUser,
      // 使用 Symbol 作為鍵,避免衝突
      [Symbol.for('my-token')]: this.sharedService
    }
  },

  data() {
    return {
      currentUser: { name: 'Alice', role: 'admin' },
      sharedService: new MyService()
    }
  }
}

小技巧:在函式寫法中,若要提供 reactive 的資料,建議使用 Vue 內建的 reactiveref,或直接回傳 this 中的 datacomputedmethods,它們會自動保持響應式。

3. Options API 中的 inject

inject 也是兩種寫法:

  1. 陣列形式:只指定要注入的鍵,取得的值會直接掛在 this 上。
  2. 物件形式:可以設定別名、預設值,甚至是自訂的 from(注入鍵)。
export default {
  // 1. 陣列寫法
  inject: ['foo', 'user'],

  // 2. 物件寫法(支援別名與預設值)
  inject: {
    // 直接使用同名鍵
    count: { from: 'count' },

    // 使用別名,並提供預設值
    myToken: {
      from: Symbol.for('my-token'),
      default: () => ({ /* fallback object */ })
    }
  },

  created() {
    console.log(this.foo)       // 'bar'
    console.log(this.user)      // { name: 'Alice', role: 'admin' }
    console.log(this.count)     // 0
    console.log(this.myToken)   // 實例化的 MyService
  }
}

重點inject 取得的值 不會自動成為響應式,除非提供者本身是 reactive(如 refreactive)或是使用 computed 包裝。若你需要在子組件中修改提供者的狀態,通常會透過 方法注入(在 provide 中提供函式)或使用 Vuex / Pinia 等全域狀態管理。

4. 讓 provide / inject 保持響應式

// 父組件
export default {
  provide() {
    // 使用 ref 包裝,使子組件取得時仍保持響應式
    return {
      counter: this.counterRef
    }
  },
  setup() {
    // Options API 也可以使用 setup,這裡示範 ref 的建立方式
    const counterRef = Vue.ref(0)
    // 其他邏輯...
    return { counterRef }
  }
}
// 子組件
export default {
  inject: ['counter'],
  mounted() {
    // this.counter 是一個 ref,直接使用 .value
    console.log('initial:', this.counter.value) // 0
    this.counter.value++ // 父組件的 counter 會同步更新
  }
}

技巧:如果你不想在子組件中寫 .value,可以在 inject 時使用 computed 包裝:

export default {
  inject: {
    counter: {
      from: 'counter',
      // 轉成 computed,使其自動解包 .value
      default: () => Vue.computed(() => 0)
    }
  },
  computed: {
    counterValue() {
      return this.counter // 已是解包後的值
    }
  }
}

程式碼範例

以下提供 5 個實務上常見的範例,每個範例都配有說明與註解,幫助你快速上手。

範例 1:最簡單的文字傳遞

// Parent.vue
export default {
  provide: {
    greeting: 'Hello from Parent!'
  }
}
// Child.vue
export default {
  inject: ['greeting'],
  mounted() {
    console.log(this.greeting) // => 'Hello from Parent!'
  }
}

說明:使用物件寫法提供固定字串,子組件透過 inject 直接取得。


範例 2:提供 reactive 的計數器

// CounterProvider.vue
export default {
  data() {
    return { count: 0 }
  },
  provide() {
    // this.count 為 reactive,子組件會自動更新
    return { counter: Vue.computed(() => this.count) }
  },
  methods: {
    inc() { this.count++ }
  },
  template: `
    <button @click="inc">+1</button>
    <slot></slot>
  `
}
// CounterDisplay.vue
export default {
  inject: {
    // 直接注入 computed,保持響應式
    counter: { from: 'counter' }
  },
  template: `<p>目前計數:{{ counter }}</p>`
}

說明:父組件的 count 變動時,子組件的 counter 會即時重新渲染。使用 computed 包裝可避免手動 .value


範例 3:使用 Symbol 避免鍵衝突

// token.js
export const THEME_TOKEN = Symbol('theme')
// ThemeProvider.vue
import { THEME_TOKEN } from './token.js'

export default {
  provide() {
    return {
      [THEME_TOKEN]: Vue.reactive({ mode: 'dark' })
    }
  },
  template: `<slot></slot>`
}
// ThemedButton.vue
import { THEME_TOKEN } from './token.js'

export default {
  inject: {
    theme: { from: THEME_TOKEN }
  },
  template: `
    <button :class="theme.mode">按鈕</button>
  `
}

說明:使用 Symbol 作為鍵可以保證全局唯一,適合大型專案或第三方套件。


範例 4:注入方法(父子雙向互動)

// AuthProvider.vue
export default {
  data() {
    return { user: null }
  },
  provide() {
    return {
      login: this.login,
      logout: this.logout,
      currentUser: Vue.computed(() => this.user)
    }
  },
  methods: {
    login(name) {
      this.user = { name }
    },
    logout() {
      this.user = null
    }
  },
  template: `<slot></slot>`
}
// LoginForm.vue
export default {
  inject: ['login', 'logout', 'currentUser'],
  data() {
    return { nameInput: '' }
  },
  methods: {
    submit() {
      this.login(this.nameInput)
    }
  },
  template: `
    <div v-if="!currentUser">
      <input v-model="nameInput" placeholder="輸入姓名"/>
      <button @click="submit">登入</button>
    </div>
    <div v-else>
      <p>已登入:{{ currentUser.name }}</p>
      <button @click="logout">登出</button>
    </div>
  `
}

說明:父組件提供了 login / logout 方法以及 currentUser 的 reactive 包裝。子組件只需要呼叫方法,即可完成雙向互動。


範例 5:在深層巢狀組件中使用 provide / inject

// App.vue
<template>
  <ThemeProvider>
    <Layout>
      <Content />
    </Layout>
  </ThemeProvider>
</template>
// ThemeProvider.vue(同範例 3)
// Layout.vue
export default {
  template: `<section><slot /></section>`
}
// Content.vue
export default {
  inject: {
    theme: { from: THEME_TOKEN }
  },
  template: `
    <article :style="{ background: theme.mode === 'dark' ? '#222' : '#fff' }">
      這是深層子組件,仍能取得 theme
    </article>
  `
}

說明:即使 Content.vue 距離提供者有三層之遠,仍然可以直接 inject,證明 provide / inject 的「跨層級」特性。


常見陷阱與最佳實踐

陷阱 說明 解決方式 / 最佳實踐
忘記使用函式形式提供 reactive 直接在 provide 物件裡放 data,子組件取得後會是 靜態值,不會更新。 使用 provide() 函式回傳 computedrefreactive
鍵名衝突 多個祖先組件使用相同的字串鍵,子組件無法分辨來源。 使用 Symbol 作為鍵,或在鍵名前加上命名空間(例如 app-theme)。
注入值非響應式 inject 取得的普通物件不會自動追蹤變化。 在提供者端使用 ref / reactive,或在子組件端使用 computed 包裝。
過度依賴 provide / inject 把所有全域狀態都塞進 provide,會失去可追蹤性與除錯便利。 僅在跨層級共享 UI 狀態或服務 時使用,其他情況考慮 Vuex / Pinia。
在子組件中修改注入值 直接改變注入的物件可能違反單向資料流,且不易追蹤。 提供方法(如 setTheme)或使用 Vuex / Pinia 進行集中管理。
忘記在子組件中聲明 inject 直接在模板使用 this.xxx,Vue 會找不到屬性。 必須在 inject 內明確列出鍵或別名。

最佳實踐清單

  1. 使用函式提供:保證 this 可存取,且能返回 reactive。
  2. 鍵名唯一化:優先採用 Symbol,或在字串鍵前加入前綴。
  3. 只提供「服務」或「UI 狀態」:如主題、語言、授權服務。
  4. 提供方法而非直接修改資料:保持單向資料流。
  5. 在 TypeScript 中為 inject 加上型別inject: { user: { from: 'user', default: () => ({}) } as User },提升開發體驗。
  6. 測試:在單元測試時,可使用 provide 在測試套件中模擬服務,避免依賴真實的子組件結構。

實際應用場景

場景 為何適合使用 provide / inject
全局主題切換(dark / light) 主題設定只需要在根組件提供一次,所有子組件都能直接 inject,不必把 theme 逐層傳遞。
多語系(i18n)服務 文字翻譯函式 t(key) 可在根組件 provide,子組件只要 inject 即可使用。
表單驗證規則 在表單容器提供驗證規則或錯誤訊息收集服務,子欄位直接注入,保持表單結構的彈性。
動態權限檢查 授權服務(如 can('edit'))在根組件提供,子組件根據權限顯示/隱藏 UI。
第三方 UI 套件 如 Element Plus、Vuetify 的主題或配置,內部多使用 provide / inject,開發者亦可自行擴充。
嵌套的彈窗 / Dialog 系統 父彈窗提供關閉或確認的回呼函式,子彈窗直接 inject,避免層層傳參。

總結

  • provide / inject 是 Vue 3 中跨層級共享資料的利器,即使在 Options API(傳統寫法)中,也能透過簡潔的語法完成。
  • 透過 函式形式的 provide,配合 refreactivecomputed,即可讓子組件取得 響應式 的狀態。
  • 使用 Symbol 作為鍵、提供 方法 而非直接修改資料,可避免衝突與維護困難。
  • 在實務開發中,把 UI 狀態(主題、語言)或 服務層(授權、驗證)放入 provide / inject,能大幅減少 props 的傳遞成本,同時保持組件的 低耦合

掌握了這套機制,你就能在 Vue 3 的 Options API 專案中,寫出更乾淨、可維護且易於擴充的程式碼。祝開發順利 🚀!