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>

問題在於:

  1. 邏輯分散:同一個功能的相關程式碼散佈在不同的選項中
  2. 難以重用:很難將某個功能的邏輯提取出來重用
  3. 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> 的優點

  1. 更少的樣板程式碼:不需要寫 return
  2. 更好的 IDE 支援:更準確的型別推斷
  3. 更好的執行效能:編譯時優化
  4. 自動註冊元件: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 APIComposition 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 專案:

  1. 新元件使用 Composition API
  2. 逐步遷移複雜元件
  3. 提取通用邏輯到 Composables
  4. 不需要強制遷移所有元件

Composition API 和 Options API 可以共存,根據需求選擇即可。

更多程式碼重用的技巧請參考 Composables