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 可以是:
- 物件形式:直接回傳一個物件,鍵值會被自動轉成字串作為注入鍵。
- 函式形式:回傳物件,允許在執行時動態取得
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內建的reactive或ref,或直接回傳this中的data、computed、methods,它們會自動保持響應式。
3. Options API 中的 inject
inject 也是兩種寫法:
- 陣列形式:只指定要注入的鍵,取得的值會直接掛在
this上。 - 物件形式:可以設定別名、預設值,甚至是自訂的
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(如ref、reactive)或是使用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() 函式回傳 computed、ref 或 reactive。 |
| 鍵名衝突 | 多個祖先組件使用相同的字串鍵,子組件無法分辨來源。 | 使用 Symbol 作為鍵,或在鍵名前加上命名空間(例如 app-theme)。 |
| 注入值非響應式 | inject 取得的普通物件不會自動追蹤變化。 |
在提供者端使用 ref / reactive,或在子組件端使用 computed 包裝。 |
| 過度依賴 provide / inject | 把所有全域狀態都塞進 provide,會失去可追蹤性與除錯便利。 | 僅在跨層級共享 UI 狀態或服務 時使用,其他情況考慮 Vuex / Pinia。 |
| 在子組件中修改注入值 | 直接改變注入的物件可能違反單向資料流,且不易追蹤。 | 提供方法(如 setTheme)或使用 Vuex / Pinia 進行集中管理。 |
忘記在子組件中聲明 inject |
直接在模板使用 this.xxx,Vue 會找不到屬性。 |
必須在 inject 內明確列出鍵或別名。 |
最佳實踐清單
- 使用函式提供:保證
this可存取,且能返回 reactive。 - 鍵名唯一化:優先採用
Symbol,或在字串鍵前加入前綴。 - 只提供「服務」或「UI 狀態」:如主題、語言、授權服務。
- 提供方法而非直接修改資料:保持單向資料流。
- 在 TypeScript 中為 inject 加上型別:
inject: { user: { from: 'user', default: () => ({}) } as User },提升開發體驗。 - 測試:在單元測試時,可使用
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,配合
ref、reactive或computed,即可讓子組件取得 響應式 的狀態。 - 使用 Symbol 作為鍵、提供 方法 而非直接修改資料,可避免衝突與維護困難。
- 在實務開發中,把 UI 狀態(主題、語言)或 服務層(授權、驗證)放入 provide / inject,能大幅減少
props的傳遞成本,同時保持組件的 低耦合。
掌握了這套機制,你就能在 Vue 3 的 Options API 專案中,寫出更乾淨、可維護且易於擴充的程式碼。祝開發順利 🚀!