Vue Composition API
Composition API 是 Vue 3 引入的一組新 API,提供了一種更靈活的方式來組織元件邏輯。它不是要取代 Options API,而是提供另一種選擇,特別適合處理複雜的元件邏輯和程式碼重用。
為什麼需要 Composition API?
Options API 的限制
在 Options API 中,元件邏輯是按照選項類型分組的:
<script>
export default {
data() {
return {
// 功能 A 的資料
userList: [],
userLoading: false,
// 功能 B 的資料
todoList: [],
todoLoading: false
}
},
computed: {
// 功能 A 的計算屬性
activeUsers() { /* ... */ },
// 功能 B 的計算屬性
completedTodos() { /* ... */ }
},
methods: {
// 功能 A 的方法
fetchUsers() { /* ... */ },
// 功能 B 的方法
addTodo() { /* ... */ }
},
mounted() {
// 混合了功能 A 和 B 的初始化
this.fetchUsers()
this.fetchTodos()
}
}
</script>
問題在於:
- 邏輯分散:同一個功能的相關程式碼散佈在不同的選項中
- 難以重用:很難將某個功能的邏輯提取出來重用
- TypeScript 支援有限:
this的型別推斷有困難
Composition API 的解決方案
使用 Composition API,可以將相關的邏輯組織在一起:
<script setup>
import { ref, computed, onMounted } from 'vue'
// 功能 A:使用者管理
const userList = ref([])
const userLoading = ref(false)
const activeUsers = computed(() => userList.value.filter(u => u.active))
async function fetchUsers() {
userLoading.value = true
userList.value = await api.getUsers()
userLoading.value = false
}
// 功能 B:待辦事項
const todoList = ref([])
const todoLoading = ref(false)
const completedTodos = computed(() => todoList.value.filter(t => t.done))
function addTodo(text) {
todoList.value.push({ text, done: false })
}
// 初始化
onMounted(() => {
fetchUsers()
})
</script>
setup 函式
Composition API 的核心是 setup 函式,它是元件的入口點:
<script>
import { ref, onMounted } from 'vue'
export default {
props: ['title'],
setup(props, context) {
// props 是響應式的
console.log(props.title)
// context 包含 attrs, slots, emit, expose
const { attrs, slots, emit, expose } = context
const count = ref(0)
function increment() {
count.value++
}
onMounted(() => {
console.log('mounted')
})
// 返回的內容會暴露給模板
return {
count,
increment
}
}
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
<script setup> 語法糖
<script setup> 是 setup 函式的語法糖,更簡潔且效能更好:
<script setup>
import { ref, onMounted } from 'vue'
// 頂層變數會自動暴露給模板
const count = ref(0)
function increment() {
count.value++
}
onMounted(() => {
console.log('mounted')
})
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
<script setup> 的優點
- 更少的樣板程式碼:不需要寫 return
- 更好的 IDE 支援:更準確的型別推斷
- 更好的執行效能:編譯時優化
- 自動註冊元件:import 的元件可以直接使用
Composition API 核心函式
響應式
<script setup>
import { ref, reactive, computed, readonly } from 'vue'
// ref - 包裝任何值
const count = ref(0)
// reactive - 物件的響應式
const state = reactive({
name: 'Vue',
version: 3
})
// computed - 計算屬性
const doubled = computed(() => count.value * 2)
// readonly - 只讀
const readonlyState = readonly(state)
</script>
生命週期
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
onBeforeMount(() => console.log('before mount'))
onMounted(() => console.log('mounted'))
onBeforeUpdate(() => console.log('before update'))
onUpdated(() => console.log('updated'))
onBeforeUnmount(() => console.log('before unmount'))
onUnmounted(() => console.log('unmounted'))
</script>
監聽
<script setup>
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Vue')
// watch - 明確指定監聽目標
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} -> ${newVal}`)
})
// watchEffect - 自動追蹤依賴
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
})
</script>
依賴注入
<script setup>
import { provide, inject } from 'vue'
// 提供
provide('theme', 'dark')
// 注入
const theme = inject('theme', 'light') // 'light' 是預設值
</script>
定義元件 API
在 <script setup> 中,有幾個特殊的編譯器巨集:
defineProps
<script setup>
// 執行時宣告
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
// TypeScript 型別宣告
const props = defineProps<{
title: string
count?: number
}>()
// 有預設值的 TypeScript 寫法
const props = withDefaults(defineProps<{
title: string
count?: number
}>(), {
count: 0
})
</script>
defineEmits
<script setup>
// 執行時宣告
const emit = defineEmits(['update', 'delete'])
// TypeScript 型別宣告
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
// Vue 3.3+ 更簡潔的語法
const emit = defineEmits<{
update: [value: string]
delete: [id: number]
}>()
// 使用
emit('update', 'new value')
</script>
defineExpose
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// 明確暴露給父元件
defineExpose({
count,
increment
})
</script>
defineModel(Vue 3.4+)
<script setup>
// 定義 v-model
const model = defineModel()
// 有預設值
const model = defineModel({ default: '' })
// 具名 v-model
const title = defineModel('title')
</script>
<template>
<input :value="model" @input="model = $event.target.value">
</template>
defineOptions(Vue 3.3+)
<script setup>
// 定義元件選項(如 name、inheritAttrs)
defineOptions({
name: 'MyComponent',
inheritAttrs: false
})
</script>
defineSlots(Vue 3.3+)
<script setup lang="ts">
// 定義插槽的型別
const slots = defineSlots<{
default(props: { item: string }): any
header(): any
}>()
</script>
Options API vs Composition API
| 特性 | Options API | Composition API |
|---|---|---|
| 組織方式 | 按選項類型 | 按邏輯功能 |
| 程式碼重用 | Mixins(有問題) | Composables |
| TypeScript | 有限支援 | 完整支援 |
| 學習曲線 | 較低 | 較高 |
| 適用場景 | 小型元件 | 複雜邏輯、大型專案 |
可以混用嗎?
可以!在同一個專案甚至同一個元件中都可以混用:
<script>
export default {
data() {
return {
optionsData: 'from options'
}
},
setup() {
const setupData = ref('from setup')
return { setupData }
}
}
</script>
但建議選擇一種風格並保持一致。
Composition API 最佳實踐
1. 使用 <script setup>
除非有特殊需求,否則優先使用 <script setup>。
2. 按功能組織程式碼
<script setup>
// ===== 使用者相關 =====
const user = ref(null)
const isLoading = ref(false)
async function fetchUser() { /* ... */ }
// ===== 購物車相關 =====
const cart = ref([])
const cartTotal = computed(() => /* ... */)
function addToCart(item) { /* ... */ }
</script>
3. 提取可重用邏輯到 Composables
// useUser.js
export function useUser() {
const user = ref(null)
const isLoading = ref(false)
async function fetchUser(id) {
isLoading.value = true
user.value = await api.getUser(id)
isLoading.value = false
}
return { user, isLoading, fetchUser }
}
<script setup>
import { useUser } from './useUser'
const { user, isLoading, fetchUser } = useUser()
</script>
4. 善用 TypeScript
<script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
const users = ref<User[]>([])
const selectedId = ref<number | null>(null)
const selectedUser = computed(() =>
users.value.find(u => u.id === selectedId.value)
)
</script>
遷移建議
如果你有現有的 Options API 專案:
- 新元件使用 Composition API
- 逐步遷移複雜元件
- 提取通用邏輯到 Composables
- 不需要強制遷移所有元件
Composition API 和 Options API 可以共存,根據需求選擇即可。
更多程式碼重用的技巧請參考 Composables。