本文 AI 產出,尚未審核
Vue3 元件通信:expose() 對父元件公開方法
簡介
在大型 Vue3 專案中,元件之間的溝通往往是開發者最常面對的挑戰之一。
傳統的父子溝通方式(props / emit)已能滿足大部分需求,但當子元件需要提供內部方法給父元件直接呼叫時,卻沒有一個既安全又直觀的機制。
Vue3 在 setup() 中提供了 expose() API,讓子元件可以選擇性地公開自己的方法或屬性,讓父元件透過 ref 取得並呼叫。
本文將深入探討 expose() 的使用時機、語法與實務應用,幫助你在 Component Communication 中寫出更乾淨、可維護的程式碼。
核心概念
1. 為什麼需要 expose()
- 封裝性:子元件的實作細節不應該全部暴露給父元件,僅公開必要的 API 才能保持元件的內部封裝。
- 避免
$parent:在 Vue2 時常會使用$parent或$children直接存取子元件,這會造成耦合、難以重構。expose()提供了官方推薦的方式。 - TypeScript 支援:使用
expose()時,父元件的ref會得到正確的類型提示,提升開發體驗。
2. expose() 的基本語法
在子元件的 setup() 中呼叫 expose(),傳入一個物件,物件的屬性即為要公開的 API:
import { defineComponent, ref, expose } from 'vue'
export default defineComponent({
name: 'Child',
setup(_, { expose }) {
const count = ref(0)
function increment(step = 1) {
count.value += step
}
// 只公開 increment 方法與 count 只讀屬性
expose({
increment,
get count() {
return count.value
}
})
}
})
expose接收的物件可以是 函式、屬性、getter,甚至是 其他 ref。- 若不呼叫
expose(),則父元件只能透過ref取得子元件的 公開屬性(由defineExpose自動推斷的public成員)或根本無法存取。
3. 父元件如何取得子元件的公開方法
父元件需要先建立子元件的 ref,然後在 onMounted(或 watch)中使用:
import { defineComponent, ref, onMounted } from 'vue'
import Child from './Child.vue'
export default defineComponent({
components: { Child },
setup() {
const childRef = ref(null)
onMounted(() => {
// 呼叫子元件公開的 increment 方法
childRef.value?.increment(5)
console.log('子元件目前的 count:', childRef.value?.count)
})
return { childRef }
},
template: `<Child ref="childRef" />`
})
重點:
childRef.value只會有expose()中定義的屬性,未公開的內部變數仍不可直接存取。
4. 多個子元件同時使用 expose()
如果父元件同時渲染多個子元件(例如使用 v-for),可以把每個子元件的 ref 放入陣列或物件中,方便批次操作:
<template>
<div v-for="(item, idx) in list" :key="item.id">
<Child ref="setChildRef" :data="item" />
<button @click="increaseAll(idx)">+1 ({{ idx }})</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const list = ref([{ id: 1 }, { id: 2 }, { id: 3 }])
const childRefs = ref([])
function setChildRef(el) {
if (el) childRefs.value.push(el)
}
function increaseAll(step) {
childRefs.value.forEach(child => child.increment(step))
}
</script>
ref="setChildRef"會把每個子元件的實例依序傳入setChildRef,我們把它們收集在childRefs陣列中,之後即可一次呼叫所有子元件的increment。
5. expose() 與 defineExpose 的差異
expose():在setup()內部動態呼叫,允許根據條件決定要公開哪些 API。defineExpose(Vue 3.3+):在<script setup>中使用,更簡潔的語法,直接在頂層寫defineExpose({ foo, bar })。兩者最終效果相同,只是寫法不同。
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() {
count.value = 0
}
defineExpose({
reset,
get count() {
return count.value
}
})
</script>
程式碼範例
下面提供 5 個實用範例,從最簡單到較進階的情境,協助你快速上手 expose()。
範例 1:最小化公開 focus 方法(表單元件)
// InputField.vue
<template>
<input ref="inputEl" :placeholder="placeholder" />
</template>
<script setup>
import { ref, expose } from 'vue'
const props = defineProps({
placeholder: String
})
const inputEl = ref(null)
function focus() {
inputEl.value?.focus()
}
// 只公開 focus,保持其他實作私有
expose({ focus })
</script>
// Parent.vue
<template>
<InputField ref="fieldRef" placeholder="請輸入姓名" />
<button @click="focusField">聚焦輸入框</button>
</template>
<script setup>
import { ref } from 'vue'
import InputField from './InputField.vue'
const fieldRef = ref(null)
function focusField() {
fieldRef.value?.focus()
}
</script>
說明:父元件只需要聚焦功能,透過
expose()只暴露focus,避免父元件直接操作 DOM。
範例 2:公開多個方法與只讀屬性(計數器)
// Counter.vue
<template>
<div>{{ count }}</div>
<button @click="increment">+1</button>
</template>
<script setup>
import { ref, expose } from 'vue'
const count = ref(0)
function increment(step = 1) {
count.value += step
}
function reset() {
count.value = 0
}
// 只公開需要的 API,count 以 getter 形式只讀
expose({
increment,
reset,
get count() {
return count.value
}
})
</script>
// Dashboard.vue
<template>
<Counter ref="counterRef" />
<button @click="addFive">+5</button>
<button @click="resetAll">重設</button>
<p>目前計數:{{ counterRef?.count }}</p>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Counter from './Counter.vue'
const counterRef = ref(null)
function addFive() {
counterRef.value?.increment(5)
}
function resetAll() {
counterRef.value?.reset()
}
</script>
技巧:使用
getter讓父元件只能讀取count,避免意外修改。
範例 3:條件式公開(根據權限決定 API)
// SecurePanel.vue
<template>
<div v-if="hasAccess">
<slot />
</div>
<div v-else>無權限</div>
</template>
<script setup>
import { ref, expose, computed } from 'vue'
const props = defineProps({
userRole: String
})
const hasAccess = computed(() => props.userRole === 'admin')
function reloadData() {
console.log('重新載入資料')
}
// 僅在有權限時才公開 reloadData
if (hasAccess.value) {
expose({ reloadData })
}
</script>
// AdminPage.vue
<template>
<SecurePanel ref="panelRef" :userRole="role">
<p>機密資訊</p>
</SecurePanel>
<button @click="refresh">手動刷新</button>
</template>
<script setup>
import { ref } from 'vue'
import SecurePanel from './SecurePanel.vue'
const role = 'admin' // 或從 store 取得
const panelRef = ref(null)
function refresh() {
// 若使用者非 admin,panelRef.value 會是 null
panelRef.value?.reloadData()
}
</script>
重點:
expose()可以在 runtime 判斷後才執行,確保只有符合條件的元件才會公開特定方法。