Vue3 Composition API(核心) — 使用 script setup 語法糖
簡介
在 Vue 3 中,Composition API 為大型應用提供了更好的可組合性與可讀性。而自 Vue 3.2 起,官方推出了 script setup 語法糖,讓開發者可以用更少的樣板程式碼,直接在 <script> 標籤中使用 Composition API。
這項語法不僅縮短了檔案長度,還能自動推導 props、emits、expose 等選項,降低了忘記匯出或錯誤命名的風險。對於 從 Options API 轉型的開發者,或是 新手想快速上手,script setup 是一個非常值得掌握的工具。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用場景,讓你能在專案中自信地採用 script setup。
核心概念
1. 為什麼要使用 script setup
| 傳統 Composition API | script setup |
|---|---|
必須寫 export default { setup() { … } } |
直接寫在 <script setup> 中 |
需要手動匯出 props、emit、expose |
透過 defineProps、defineEmits、defineExpose 自動推導 |
變數與函式會被包在 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. 使用 defineProps 與 defineEmits
<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:向父層公開內部方法
在某些情況下,父元件需要直接呼叫子元件的內部方法(例如 focus、reset),可使用 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 會提供完整的型別提示與錯誤檢查。 defineProps、defineEmits都支援泛型,讓型別更明確。
程式碼範例
以下提供 五個實用範例,展示 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)。 |
| 重複名稱衝突 | ref、computed、function 與 props 同名會被覆寫。 |
避免 使用相同名稱,或在 defineProps 中使用別名 (props: { foo: { type: String, required: true } })。 |
| 模板中找不到變數 | 變數在 <script setup> 中被 export 或 return,但忘記在模板中使用 {{}}。 |
所有在 <script setup> 頂層聲明的變數都自動暴露,不需要 return。若使用 defineExpose,僅在父層可見。 |
| TypeScript 型別遺失 | 未為 defineProps、defineEmits 指定泛型,IDE 無法提供提示。 |
使用 interface / type 為 defineProps、defineEmits 明確指定型別。 |
懶加載時缺少 fallback |
使用 defineAsyncComponent 卻未包裹 <Suspense>,會出現空白或錯誤。 |
為每個 async component 包裹 <Suspense>,提供 fallback UI。 |
最佳實踐清單
- 保持單一職責:每個
.vue檔案只做一件事,使用 composable 抽離共用邏輯。 - 使用
defineProps的 JSDoc:即使不寫 TypeScript,也能得到提示。 - 盡量使用
ref而非reactive:ref更易於解構與傳遞。 - 在大型專案中採用
script setup+ TypeScript:型別安全能防止許多執行時錯誤。 - 避免在
setup內部直接操作 DOM:使用onMounted、ref結合模板指令,保持響應式。
實際應用場景
1. 表單與驗證
在企業內部系統中,表單往往包含大量欄位與即時驗證。使用 script setup 搭配 watch、computed,可以把每個欄位的驗證邏輯寫成 獨立 composable,如 useFieldValidator.js,讓表單元件保持乾淨。
2. 動態儀表板
儀表板常需根據使用者權限載入不同的圖表。defineAsyncComponent + <Suspense> 在 script setup 中非常簡潔,配合 v-if 動態切換,能減少首屏載入時間。
3. 複雜的父子溝通
在大型 SPA 中,父子元件的雙向資料流常會變得錯綜複雜。利用 defineProps、defineEmits、defineExpose,可以明確界定 API,避免「props 洩漏」或「事件命名衝突」的問題。
4. 多語系(i18n)切換
使用 script setup 搭配 vue-i18n 的 useI18n composable,能在同一檔案內完成語系切換、文字插值與日期格式化,讓多語系支援更易維護。
5. 服務端渲染(SSR)
script setup 與 vite、nuxt3 完全相容,支援 SSR。在 Nuxt 3 中,<script setup> 仍是預設寫法,開發者只要關注資料取得(useAsyncData)與 SEO 標籤,即可快速完成 SEO 友好的頁面。
總結
script setup 為 Vue 3 的 Composition API 帶來了 語法糖,讓開發者可以:
- 減少樣板:不必手寫
export default、setup()、return。 - 自動推導:
props、emits、expose皆可透過專用函式聲明。 - 提升可讀性:所有頂層變數直接供模板使用,專注於業務邏輯。
- 兼容 TypeScript:提供完整型別支援,適合大型專案。
在實務開發中,將 組件邏輯抽離成 composable、使用 defineExpose 讓父層可安全呼叫子元件方法、以及 結合 async component 與 Suspense 以提升效能,都是值得採納的最佳實踐。
只要掌握上述概念與範例,你就能在 Vue 3 專案中自如運用 script setup,寫出 乾淨、可維護、效能佳 的前端程式碼。祝開發順利,期待看到你用 script setup 打造的精彩應用!