本文 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 判斷後才執行,確保只有符合條件的元件才會公開特定方法。


範例 4:使用 defineExpose