Vue.js2026年2月23日
Vue.js 3 Composition API 深度解析
從實際專案角度分析 Vue 3 的 Composition API,如何讓程式碼更具可維護性和重用性。
Vue 3 的 Composition API 是一個革命性的變化,它為我們提供了更靈活、更可組合的方式來組織組件邏輯。在開發 The Lonesome Era 網站的過程中,我深度使用了 Composition API,本文將分享實戰中的心得與最佳實踐。
為什麼選擇 Composition API?
在 Vue 2 的 Options API 中,我們經常遇到以下問題:
- 邏輯分散:相關的邏輯被分散在 data、methods、computed 等不同選項中
- 複用困難:跨組件的邏輯複用需要依賴 mixins,容易產生命名衝突
- 類型推斷:TypeScript 支援不夠完善
- 大型組件:隨著功能增加,組件變得難以維護
Composition API 通過函數式的方式解決了這些問題,讓我們能夠:
按照邏輯關注點組織程式碼,而非按照選項類型組織
核心概念深度解析
ref vs reactive:響應式系統的雙子星
理解 ref 和 reactive 的差異是掌握 Composition API 的關鍵:
import { ref, reactive, computed, watch } from 'vue'
export default {
setup() {
// ref:用於基本類型和單一值
const count = ref(0)
const message = ref('Hello Vue 3')
// reactive:用於物件和陣列
const state = reactive({
user: {
name: 'The Lonesome Era',
email: 'argoskenny@gmail.com'
},
posts: [],
isLoading: false
})
// ref 需要通過 .value 存取
console.log(count.value) // 0
count.value++
// reactive 直接存取屬性
console.log(state.user.name) // 'The Lonesome Era'
state.isLoading = true
return {
count,
message,
state
}
}
}
實戰技巧:選擇 ref 還是 reactive?
在實際開發中,我遵循以下原則:
- 基本類型:使用
ref - 物件狀態:使用
reactive - 表單資料:使用
reactive更方便 - API 響應:根據資料結構選擇
// ✅ 推薦的模式
const useUserData = () => {
// 簡單狀態使用 ref
const isLoading = ref(false)
const error = ref(null)
// 複雜物件使用 reactive
const userData = reactive({
profile: {},
preferences: {},
statistics: {}
})
return {
isLoading,
error,
userData
}
}
組合式函數:邏輯複用的藝術
Composition API 最大的優勢在於邏輯複用。讓我們看看如何創建可複用的組合式函數:
響應式狀態管理
// composables/useResponsiveDesign.js
import { ref, computed, onMounted, onUnmounted } from 'vue'
export function useResponsiveDesign() {
const windowWidth = ref(window.innerWidth)
const updateWidth = () => {
windowWidth.value = window.innerWidth
}
onMounted(() => {
window.addEventListener('resize', updateWidth)
})
onUnmounted(() => {
window.removeEventListener('resize', updateWidth)
})
const isMobile = computed(() => windowWidth.value <= 768)
const isTablet = computed(() => windowWidth.value > 768 && windowWidth.value <= 1024)
const isDesktop = computed(() => windowWidth.value > 1024)
const deviceType = computed(() => {
if (isMobile.value) return 'mobile'
if (isTablet.value) return 'tablet'
return 'desktop'
})
return {
windowWidth,
isMobile,
isTablet,
isDesktop,
deviceType
}
}
API 資料獲取
// composables/useApi.js
import { ref, reactive } from 'vue'
export function useApi() {
const loading = ref(false)
const error = ref(null)
const request = async (url, options = {}) => {
loading.value = true
error.value = null
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
return {
loading,
error,
request
}
}
// 特定 API 的組合式函數
export function useArticles() {
const { loading, error, request } = useApi()
const articles = ref([])
const fetchArticles = async () => {
try {
const data = await request('/api/articles')
articles.value = data
} catch (err) {
console.error('Failed to fetch articles:', err)
}
}
const createArticle = async (articleData) => {
try {
const newArticle = await request('/api/articles', {
method: 'POST',
body: JSON.stringify(articleData)
})
articles.value.push(newArticle)
return newArticle
} catch (err) {
console.error('Failed to create article:', err)
throw err
}
}
return {
articles,
loading,
error,
fetchArticles,
createArticle
}
}
實戰案例:The Lonesome Era 網站架構
讓我們看看 Composition API 在實際專案中的應用。以下是網站主要組件的簡化版本:
主應用組件
// App.vue (setup 部分)
import { ref, computed, onMounted, watch } from 'vue'
import { useResponsiveDesign } from './composables/useResponsiveDesign'
import { useNavigation } from './composables/useNavigation'
import { useArticles } from './composables/useArticles'
export default {
setup() {
// 響應式設計
const { isMobile, isTablet, deviceType } = useResponsiveDesign()
// 導航狀態
const {
currentSection,
mobileMenuOpen,
changeSection,
toggleMobileMenu
} = useNavigation()
// 文章資料
const { articles, loading, fetchArticles } = useArticles()
// 根據裝置類型調整文章顯示數量
const displayedArticles = computed(() => {
const maxArticles = isMobile.value ? 3 : 5
return articles.value.slice(0, maxArticles)
})
// 監聽裝置變化,自動關閉手機選單
watch(deviceType, (newType, oldType) => {
if (oldType === 'mobile' && newType !== 'mobile') {
mobileMenuOpen.value = false
}
})
// 初始化
onMounted(() => {
fetchArticles()
})
return {
// 響應式狀態
currentSection,
mobileMenuOpen,
isMobile,
isTablet,
deviceType,
// 資料
displayedArticles,
loading,
// 方法
changeSection,
toggleMobileMenu
}
}
}
導航邏輯組合式函數
// composables/useNavigation.js
import { ref, nextTick } from 'vue'
export function useNavigation() {
const currentSection = ref('home')
const mobileMenuOpen = ref(false)
const sections = ['home', 'projects', 'articles', 'observations', 'about']
const changeSection = async (section) => {
if (!sections.includes(section)) {
console.warn(`Invalid section: ${section}`)
return
}
currentSection.value = section
mobileMenuOpen.value = false
// 更新 URL
updateURL(section)
// 滾動到頂部
await nextTick()
scrollToTop()
// 頁面分析追蹤
trackPageView(section)
}
const toggleMobileMenu = () => {
mobileMenuOpen.value = !mobileMenuOpen.value
// 防止背景滾動
document.body.style.overflow = mobileMenuOpen.value ? 'hidden' : ''
}
const updateURL = (section) => {
if (history.pushState) {
history.pushState(null, null, `#${section}`)
} else {
window.location.hash = section
}
}
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
})
}
const trackPageView = (section) => {
// Google Analytics 或其他分析工具
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_MEASUREMENT_ID', {
page_title: `${section} | The Lonesome Era`,
page_location: window.location.href
})
}
}
return {
currentSection,
mobileMenuOpen,
sections,
changeSection,
toggleMobileMenu
}
}
進階技巧與最佳實踐
1. 使用 computed 進行資料轉換
// 複雜的計算屬性
const useProjectsFilter = (projects) => {
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('date')
const filteredProjects = computed(() => {
let result = projects.value
// 搜尋過濾
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(project =>
project.title.toLowerCase().includes(query) ||
project.description.toLowerCase().includes(query) ||
project.tags.some(tag => tag.toLowerCase().includes(query))
)
}
// 分類過濾
if (selectedCategory.value !== 'all') {
result = result.filter(project =>
project.category === selectedCategory.value
)
}
// 排序
result.sort((a, b) => {
switch (sortBy.value) {
case 'date':
return new Date(b.date) - new Date(a.date)
case 'title':
return a.title.localeCompare(b.title)
case 'popularity':
return b.views - a.views
default:
return 0
}
})
return result
})
return {
searchQuery,
selectedCategory,
sortBy,
filteredProjects
}
}
2. 生命週期鉤子的靈活運用
// 組合多個生命週期邏輯
const usePageAnalytics = () => {
const startTime = ref(Date.now())
const pageViews = ref(0)
onMounted(() => {
// 記錄頁面載入時間
startTime.value = Date.now()
pageViews.value++
// 發送頁面瀏覽事件
sendAnalyticsEvent('page_view', {
page: window.location.pathname,
timestamp: startTime.value
})
})
onUnmounted(() => {
// 計算停留時間
const duration = Date.now() - startTime.value
sendAnalyticsEvent('page_leave', {
page: window.location.pathname,
duration: duration
})
})
return {
pageViews
}
}
const useKeyboardShortcuts = () => {
const handleKeydown = (event) => {
// ESC 鍵關閉選單
if (event.key === 'Escape') {
// 發送自定義事件
document.dispatchEvent(new CustomEvent('close-menu'))
}
// Ctrl/Cmd + K 開啟搜尋
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault()
document.dispatchEvent(new CustomEvent('open-search'))
}
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
}
3. 錯誤處理與邊界情況
// 強健的錯誤處理
const useAsyncData = (fetchFn, options = {}) => {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const retryCount = ref(0)
const {
immediate = true,
maxRetries = 3,
retryDelay = 1000
} = options
const execute = async (...args) => {
loading.value = true
error.value = null
try {
const result = await fetchFn(...args)
data.value = result
retryCount.value = 0
return result
} catch (err) {
error.value = err
// 自動重試
if (retryCount.value < maxRetries) {
retryCount.value++
setTimeout(() => {
execute(...args)
}, retryDelay * retryCount.value)
}
throw err
} finally {
loading.value = false
}
}
const retry = () => {
retryCount.value = 0
execute()
}
if (immediate) {
execute()
}
return {
data,
loading,
error,
retryCount,
execute,
retry
}
}
效能優化策略
1. 適當使用 shallowRef 和 shallowReactive
import { shallowRef, shallowReactive } from 'vue'
// 當只需要響應式根層級時
const largeDataSet = shallowRef([])
const config = shallowReactive({
theme: 'dark',
language: 'zh-TW',
features: {} // 不需要深度響應式
})
2. 使用 markRaw 避免不必要的響應式
import { markRaw } from 'vue'
// 第三方庫實例不需要響應式
const chart = markRaw(new Chart(canvas, config))
const map = markRaw(new google.maps.Map(element, options))
與 TypeScript 的完美結合
Composition API 對 TypeScript 的支援非常出色:
// 類型安全的組合式函數
interface User {
id: number
name: string
email: string
}
interface ApiResponse {
data: T
message: string
status: number
}
export function useUser() {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchUser = async (id: number): Promise => {
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${id}`)
const result: ApiResponse = await response.json()
user.value = result.data
return result.data
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
return null
} finally {
loading.value = false
}
}
return {
user: readonly(user),
loading: readonly(loading),
error: readonly(error),
fetchUser
}
}
總結
Composition API 不僅僅是一個新的 API,它代表了一種新的思維方式:
- 邏輯組合:按功能而非選項組織程式碼
- 可重用性:通過組合式函數實現邏輯複用
- 類型安全:更好的 TypeScript 支援
- 靈活性:適應各種複雜場景
- 可測試性:純函數更容易測試
在 The Lonesome Era 的開發過程中,Composition API 讓我能夠:
創建更清晰、更可維護的程式碼結構,同時保持 Vue 的簡潔性和響應式特性
如果你還在使用 Options API,我強烈建議嘗試 Composition API。它不會讓你失望,反而會讓你愛上 Vue 3 的強大與靈活。
想要看到更多 Vue 3 實戰技巧?歡迎探索我們的其他技術文章,或者查看 The Lonesome Era 的完整原始碼實作。