本文 AI 產出,尚未審核

Vue3 Composition API(核心) — 使用 script setup 語法糖

簡介

在 Vue 3 中,Composition API 為大型應用提供了更好的可組合性與可讀性。而自 Vue 3.2 起,官方推出了 script setup 語法糖,讓開發者可以用更少的樣板程式碼,直接在 <script> 標籤中使用 Composition API。
這項語法不僅縮短了檔案長度,還能自動推導 propsemitsexpose 等選項,降低了忘記匯出或錯誤命名的風險。對於 從 Options API 轉型的開發者,或是 新手想快速上手script setup 是一個非常值得掌握的工具。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用場景,讓你能在專案中自信地採用 script setup


核心概念

1. 為什麼要使用 script setup

傳統 Composition API script setup
必須寫 export default { setup() { … } } 直接寫在 <script setup>
需要手動匯出 propsemitexpose 透過 definePropsdefineEmitsdefineExpose 自動推導
變數與函式會被包在 setup() 作用域內 直接在模組層級使用,語法更簡潔
重複的 return 陳述式 不需要 return,所有頂層變數自動暴露給模板

結論script setup 把「寫程式」的阻礙降到最低,讓開發者把注意力集中在 業務邏輯 上。

2. 基本語法

<script setup>
import { ref, computed } from 'vue'

// 定義響應式資料
const count = ref(0)

// 計算屬性
const double = computed(() => count.value * 2)

// 方法
function inc() {
  count.value++
}
</script>

<template>
  <button @click="inc">Count: {{ count }} (×2 = {{ double }})</button>
</template>
  • <script setup> 必須放在單檔元件(.vue)的最上層。
  • 所有在 <script setup> 中宣告的變數、函式,會自動在 template 中可用。
  • 不需要 export default,也不需要 return

3. 使用 definePropsdefineEmits

<script setup>
import { defineProps, defineEmits } from 'vue'

// 定義 Props(支援 TypeScript 或 JSDoc)
const props = defineProps({
  /** 顯示的標題 */
  title: {
    type: String,
    required: true
  },
  /** 初始計數值,預設 0 */
  initial: {
    type: Number,
    default: 0
  }
})

// 定義 Emits
const emit = defineEmits(['update'])

// 內部狀態
const count = ref(props.initial)

function inc() {
  count.value++
  // 向父層發射事件,傳回新值
  emit('update', count.value)
}
</script>

<template>
  <h2>{{ title }}</h2>
  <button @click="inc">Count: {{ count }}</button>
</template>
  • defineProps 會返回一個 只讀props 物件,不能 直接改寫。
  • defineEmits 回傳一個函式,用來觸發自訂事件。

4. defineExpose:向父層公開內部方法

在某些情況下,父元件需要直接呼叫子元件的內部方法(例如 focusreset),可使用 defineExpose

<script setup>
import { ref, defineExpose } from 'vue'

const inputRef = ref(null)

function focus() {
  inputRef.value?.focus()
}

// 只公開 focus 方法
defineExpose({ focus })
</script>

<template>
  <input ref="inputRef" type="text" />
</template>

父元件透過 ref 取得子元件實例後,即可呼叫 focus()

5. 使用 TypeScript(可選)

<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'

interface Props {
  title: string
  initial?: number
}
const props = defineProps<Props>()

const emit = defineEmits<{
  (e: 'update', value: number): void
}>()

const count = ref<number>(props.initial ?? 0)

const double = computed(() => count.value * 2)

function inc(): void {
  count.value++
  emit('update', count.value)
}
</script>

<template>
  <h2>{{ props.title }}</h2>
  <button @click="inc">Count: {{ count }} (×2 = {{ double }})</button>
</template>
  • 使用 lang="ts" 後,IDE 會提供完整的型別提示與錯誤檢查。
  • definePropsdefineEmits 都支援泛型,讓型別更明確。

程式碼範例

以下提供 五個實用範例,展示 script setup 在不同情境的使用方式。

範例 1:簡單的計數器(最小化版)

<script setup>
import { ref } from 'vue'

const count = ref(0)
function inc() { count.value++ }
</script>

<template>
  <button @click="inc">點擊次數:{{ count }}</button>
</template>

重點:不需要 export default,所有變數直接在模板使用。


範例 2:表單驗證(使用 watch

<script setup>
import { ref, watch } from 'vue'

const email = ref('')
const error = ref('')

// 監聽 email 變化,簡易驗證
watch(email, (newVal) => {
  const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  error.value = pattern.test(newVal) ? '' : '信箱格式不正確'
})
</script>

<template>
  <input v-model="email" placeholder="請輸入 Email" />
  <p v-if="error" style="color:red">{{ error }}</p>
</template>

技巧watch 直接在 <script setup> 中使用,省去 setup() 內層的繁冗。


範例 3:父子通訊(Props + Emits)

<!-- ChildComponent.vue -->
<script setup>
import { defineProps, defineEmits, ref } from 'vue'

const props = defineProps({
  label: String,
  modelValue: Number
})
const emit = defineEmits(['update:modelValue'])

function increase() {
  emit('update:modelValue', props.modelValue + 1)
}
</script>

<template>
  <div>
    <span>{{ label }}: {{ modelValue }}</span>
    <button @click="increase">+</button>
  </div>
</template>
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const count = ref(0)
</script>

<template>
  <ChildComponent
    label="計數器"
    :modelValue="count"
    @update:modelValue="count = $event"
  />
  <p>父層顯示:{{ count }}</p>
</template>

說明:使用 v-model 的語法糖 (modelValue + update:modelValue) 在 script setup 仍保持一致。


範例 4:自訂 Hook(Composable)

// useTimer.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useTimer(interval = 1000) {
  const seconds = ref(0)
  let timer = null

  onMounted(() => {
    timer = setInterval(() => {
      seconds.value++
    }, interval)
  })

  onUnmounted(() => {
    clearInterval(timer)
  })

  return { seconds }
}
<!-- TimerComponent.vue -->
<script setup>
import { useTimer } from './useTimer.js'

const { seconds } = useTimer(500) // 每 0.5 秒遞增
</script>

<template>
  <p>已經過 {{ seconds }} 秒</p>
</template>

重點script setup 完全支援 Composable,讓組件與邏輯分離更自然。


範例 5:動態匯入與 Suspense

<script setup>
import { defineAsyncComponent, ref } from 'vue'

// 動態載入子元件
const AsyncComp = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

const show = ref(false)
</script>

<template>
  <button @click="show = !show">
    {{ show ? '隱藏' : '顯示' }} HeavyComponent
  </button>

  <Suspense>
    <template #default>
      <AsyncComp v-if="show" />
    </template>
    <template #fallback>
      <p>載入中…</p>
    </template>
  </Suspense>
</template>

說明script setup 內使用 defineAsyncComponent,搭配 <Suspense> 可實現懶加載與 loading 狀態。


常見陷阱與最佳實踐

陷阱 可能的問題 解決方式 / 最佳實踐
忘記加 setup 直接在 <script> 中寫 Composition API,會產生 setup is not defined 錯誤。 一定 使用 <script setup>,或在普通 <script> 中寫 export default { setup() { … } }
props 被誤改 props只讀,直接賦值會拋出錯誤。 若需要可變的本地副本,使用 const local = ref(props.xxx),或使用 toRefs(props)
重複名稱衝突 refcomputedfunctionprops 同名會被覆寫。 避免 使用相同名稱,或在 defineProps 中使用別名 (props: { foo: { type: String, required: true } })。
模板中找不到變數 變數在 <script setup> 中被 exportreturn,但忘記在模板中使用 {{}} 所有在 <script setup> 頂層聲明的變數都自動暴露,不需要 return。若使用 defineExpose,僅在父層可見。
TypeScript 型別遺失 未為 definePropsdefineEmits 指定泛型,IDE 無法提供提示。 使用 interface / typedefinePropsdefineEmits 明確指定型別。
懶加載時缺少 fallback 使用 defineAsyncComponent 卻未包裹 <Suspense>,會出現空白或錯誤。 為每個 async component 包裹 <Suspense>,提供 fallback UI。

最佳實踐清單

  1. 保持單一職責:每個 .vue 檔案只做一件事,使用 composable 抽離共用邏輯。
  2. 使用 defineProps 的 JSDoc:即使不寫 TypeScript,也能得到提示。
  3. 盡量使用 ref 而非 reactiveref 更易於解構與傳遞。
  4. 在大型專案中採用 script setup + TypeScript:型別安全能防止許多執行時錯誤。
  5. 避免在 setup 內部直接操作 DOM:使用 onMountedref 結合模板指令,保持響應式。

實際應用場景

1. 表單與驗證

在企業內部系統中,表單往往包含大量欄位與即時驗證。使用 script setup 搭配 watchcomputed,可以把每個欄位的驗證邏輯寫成 獨立 composable,如 useFieldValidator.js,讓表單元件保持乾淨。

2. 動態儀表板

儀表板常需根據使用者權限載入不同的圖表。defineAsyncComponent + <Suspense>script setup 中非常簡潔,配合 v-if 動態切換,能減少首屏載入時間。

3. 複雜的父子溝通

在大型 SPA 中,父子元件的雙向資料流常會變得錯綜複雜。利用 definePropsdefineEmitsdefineExpose,可以明確界定 API,避免「props 洩漏」或「事件命名衝突」的問題。

4. 多語系(i18n)切換

使用 script setup 搭配 vue-i18nuseI18n composable,能在同一檔案內完成語系切換、文字插值與日期格式化,讓多語系支援更易維護。

5. 服務端渲染(SSR)

script setupvitenuxt3 完全相容,支援 SSR。在 Nuxt 3 中,<script setup> 仍是預設寫法,開發者只要關注資料取得(useAsyncData)與 SEO 標籤,即可快速完成 SEO 友好的頁面。


總結

script setup 為 Vue 3 的 Composition API 帶來了 語法糖,讓開發者可以:

  • 減少樣板:不必手寫 export defaultsetup()return
  • 自動推導propsemitsexpose 皆可透過專用函式聲明。
  • 提升可讀性:所有頂層變數直接供模板使用,專注於業務邏輯。
  • 兼容 TypeScript:提供完整型別支援,適合大型專案。

在實務開發中,將 組件邏輯抽離成 composable使用 defineExpose 讓父層可安全呼叫子元件方法、以及 結合 async component 與 Suspense 以提升效能,都是值得採納的最佳實踐。

只要掌握上述概念與範例,你就能在 Vue 3 專案中自如運用 script setup,寫出 乾淨、可維護、效能佳 的前端程式碼。祝開發順利,期待看到你用 script setup 打造的精彩應用!