Vue3 組合式函式(Composables)‧ 使用第三方庫 VueUse
簡介
在 Vue 3 中,組合式 API(Composition API) 為開發者提供了更彈性的程式碼組織方式。透過 setup()、ref、computed、watch 等核心函式,我們可以把邏輯切割成可重用的 composable,讓大型專案的維護成本大幅下降。
然而,手寫所有常見的功能(例如防抖、螢幕尺寸偵測、瀏覽器 API 包裝)仍然會造成大量樣板程式碼。這時 VueUse 這個社群維護的第三方函式庫就顯得格外重要:它提供了超過 200 個即插即用的 composable,涵蓋瀏覽器、狀態管理、效能優化等多種需求,讓開發者可以 專注於業務邏輯,而不必重複造輪子。
本文將帶你深入了解如何在 Vue 3 專案中使用 VueUse,從安裝、基本概念到實務範例,最後討論常見陷阱與最佳實踐,幫助你快速上手並在真實專案中獲得效益。
核心概念
1. VueUse 是什麼?
VueUse 是一套 基於 Vue 3 組合式 API 的工具函式集合,官方文件稱它是「Utility-First」的函式庫。它的設計哲學包括:
- 函式即組件:每個 composable 都是一個純函式,返回可直接在
setup()中使用的 reactive 變數或方法。 - 樹搖 (Tree‑shaking) 友好:只要在程式碼中引用的函式會被打包工具保留下來,未使用的部分不會被編譯進最終檔案。
- TypeScript 完整支援:提供精確的型別定義,讓 IDE 能夠提供自動完成與錯誤檢查。
2. 如何安裝與引入
# 使用 npm
npm install @vueuse/core
# 或者使用 yarn
yarn add @vueuse/core
在 Vue 3 專案的 main.js(或 main.ts)中,只需要 一次性安裝:
import { createApp } from 'vue'
import App from './App.vue'
import { VueUsePlugin } from '@vueuse/core'
const app = createApp(App)
app.use(VueUsePlugin) // 讓所有 composable 都可直接在任意組件使用
app.mount('#app')
小技巧:如果你只想引用個別 composable,直接從
@vueuse/core匯入即可,這樣可以減少最終 bundle 大小。
3. 常用的 VueUse composable 分類
| 類別 | 代表性 composable | 功能說明 |
|---|---|---|
| 瀏覽器 API | useFetch、useMediaQuery、useWindowSize |
包裝 fetch、媒體查詢、視窗尺寸等 |
| 狀態管理 | useLocalStorage、useSessionStorage、useStorage |
把資料同步到 localStorage、sessionStorage |
| 效能優化 | useDebounceFn、useThrottleFn、useRafFn |
防抖、節流、RAF 迴圈 |
| 事件處理 | useEventListener、useMouse、useKeyPress |
監聽全域或元素事件 |
| 可視化 | useIntersectionObserver、useResizeObserver |
觀測元素可見度與尺寸變化 |
以下將以實作範例說明最常用的幾個 composable。
程式碼範例
1️⃣ useFetch – 輕鬆取得遠端資料
<script setup>
import { useFetch } from '@vueuse/core'
import { ref } from 'vue'
// 取得 GitHub 使用者資訊
const { data, error, isFetching } = useFetch('https://api.github.com/users/vuejs')
.get()
.json() // 直接將回應解析成 JSON
// 觀察錯誤狀態
watch(error, (e) => {
if (e) console.error('取得資料失敗:', e)
})
</script>
<template>
<div v-if="isFetching">載入中...</div>
<div v-else-if="error">發生錯誤:{{ error.message }}</div>
<div v-else>
<h2>{{ data.login }}</h2>
<img :src="data.avatar_url" alt="avatar" width="120" />
<p>{{ data.bio }}</p>
</div>
</template>
說明:
useFetch內建了abortController、錯誤捕獲與自動重新請求等功能,寫法比原生fetch簡潔許多。
2️⃣ useLocalStorage – 把狀態同步到 LocalStorage
<script setup>
import { useLocalStorage } from '@vueuse/core'
import { computed } from 'vue'
// 設定一個持久化的暗黑模式開關
const isDark = useLocalStorage('dark-mode', false)
// 透過 computed 產生 class
const themeClass = computed(() => (isDark.value ? 'theme-dark' : 'theme-light'))
function toggleTheme() {
isDark.value = !isDark.value
}
</script>
<template>
<div :class="themeClass">
<button @click="toggleTheme">
{{ isDark ? '切換成淺色' : '切換成暗黑' }}
</button>
<p>目前主題:{{ isDark ? '暗黑' : '淺色' }}</p>
</div>
</template>
<style>
.theme-dark { background:#222; color:#fff; }
.theme-light { background:#fff; color:#222; }
</style>
重點:
useLocalStorage會自動把ref的值序列化存入localStorage,頁面重新載入時會自動還原。
3️⃣ useDebounceFn – 防抖輸入框查詢
<script setup>
import { ref } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const query = ref('')
const results = ref([])
// 假設有一個搜尋 API
async function fetchResults(q) {
const res = await fetch(`https://api.example.com/search?q=${encodeURIComponent(q)}`)
const data = await res.json()
results.value = data.items
}
// 使用防抖函式,300ms 後才觸發搜尋
const debouncedSearch = useDebounceFn(() => {
if (query.value.trim()) fetchResults(query.value)
}, 300)
// 監聽 query 變化
watch(query, debouncedSearch)
</script>
<template>
<input v-model="query" placeholder="輸入關鍵字搜尋..." />
<ul>
<li v-for="item in results" :key="item.id">{{ item.title }}</li>
</ul>
</template>
技巧:
useDebounceFn只返回防抖過的函式本身,讓你可以自由在watch、事件或其他地方使用。
4️⃣ useWindowSize – 依視窗尺寸切換佈局
<script setup>
import { useWindowSize } from '@vueuse/core'
import { computed } from 'vue'
const { width } = useWindowSize()
// 當視窗寬度小於 768px 時使用手機佈局
const isMobile = computed(() => width.value < 768)
</script>
<template>
<div v-if="isMobile">
<p>手機版導覽列</p>
<!-- 其他手機專屬 UI -->
</div>
<div v-else>
<p>桌面版導覽列</p>
<!-- 桌面版 UI -->
</div>
</template>
說明:
useWindowSize內部已經使用requestAnimationFrame來優化頻繁的 resize 事件,效能表現比自行監聽resize更好。
5️⃣ useIntersectionObserver – 懶載入圖片
<script setup>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
const imgRef = ref(null)
const loaded = ref(false)
// 當圖片進入視窗時才開始載入
useIntersectionObserver(
imgRef,
([{ isIntersecting }]) => {
if (isIntersecting) loaded.value = true
},
{ rootMargin: '200px' } // 提前 200px 載入
)
</script>
<template>
<img
ref="imgRef"
v-if="loaded"
src="https://picsum.photos/800/600"
alt="Lazy loaded"
/>
<div v-else style="height:300px;background:#eee;">Loading placeholder...</div>
</template>
優點:利用瀏覽器原生
IntersectionObserver,不需要額外的第三方套件,就能實現高效的懶載入。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 過度引入 | 一次性匯入整個 @vueuse/core 可能導致 bundle 膨脹。 |
只匯入需要的函式,或使用 import { useX } from '@vueuse/core',配合 Vite/webpack 的 tree‑shaking。 |
| SSR 相容性 | 部分 composable(如 useWindowSize)依賴瀏覽器全域物件,會在 Server‑Side Rendering 時拋錯。 |
使用 if (process.client) 或 onMounted 包裝,或改用 useWindowSize({ initialWidth: 1024 }) 提供預設值。 |
| 副作用未清理 | useEventListener、useIntersectionObserver 等會自動註冊監聽器,但在組件卸載前忘記清理可能造成記憶體泄漏。 |
大多數 VueUse composable 會在 onUnmounted 自動清理,但若自行建立 watchEffect,記得回傳清理函式。 |
| 錯誤處理不當 | useFetch 本身不會拋出例外,而是把錯誤放在 error 變數。若直接使用 await useFetch(...).json() 可能忽略錯誤。 |
始終檢查 error 或使用 try/catch 包裹自訂的 async 函式。 |
| 重複狀態 | 同時使用 useLocalStorage 與 Vuex/Pinia 保存相同資料,會導致同步困難。 |
選擇單一來源(例如全部使用 Pinia 並在 store 中讀寫 localStorage),或在 composable 中封裝兩者的同步邏輯。 |
Best Practices
- 模組化 composable
- 為每個功能建立專屬檔案,例如
useTheme.js、useSearch.js,保持單一職責原則。
- 為每個功能建立專屬檔案,例如
- 型別安全(若使用 TypeScript)
- 利用 VueUse 已提供的泛型,例如
useLocalStorage<T>('key', defaultValue as T),確保取出的資料類型正確。
- 利用 VueUse 已提供的泛型,例如
- 預設值與懶加載
- 大多數 composable 支援傳入初始值,避免在 SSR 時出現
null或undefined。
- 大多數 composable 支援傳入初始值,避免在 SSR 時出現
- 組合式 API 與 Options API 混用
- 若專案仍保留 Options API,建議在
setup()中使用 VueUse,其他選項保留不變,降低遷移成本。
- 若專案仍保留 Options API,建議在
- 效能監控
- 使用
useRafFn或useThrottleFn包裝頻繁的計算,減少不必要的重新渲染。
- 使用
實際應用場景
1️⃣ Dashboard 儀表板 – 實時資料與持久化設定
- 需求:即時顯示多個 API 回傳的圖表,使用者可以切換暗黑模式、調整刷新間隔,且設定需在刷新或重新登入後保留。
- 解法:
useFetch搭配useIntervalFn(VueUse)每 5 秒自動抓取最新資料。useLocalStorage保存暗黑模式與刷新間隔。useWindowSize讓圖表在不同螢幕尺寸自動調整。
import { useFetch, useIntervalFn, useLocalStorage, useWindowSize } from '@vueuse/core'
const refreshInterval = useLocalStorage('dashboard-interval', 5000)
const { pause, resume } = useIntervalFn(fetchAllData, refreshInterval.value)
function fetchAllData() {
// 同時呼叫多個 API
useFetch('/api/metrics/cpu').json()
useFetch('/api/metrics/memory').json()
}
2️⃣ 電商網站 – 搜尋自動完成與懶載入
- 需求:使用者在搜尋框輸入時即時顯示建議,商品列表需要在滾動時懶載入圖片。
- 解法:
useDebounceFn防抖使用者輸入,減少 API 請求。useIntersectionObserver監測商品卡片進入視窗,觸發圖片載入。
3️⃣ 行動應用 – 手機/平板自適應與權限管理
- 需求:根據裝置尺寸切換 UI,並在首次使用時請求定位權限,結果需保存在
sessionStorage。 - 解法:
useMediaQuery或useWindowSize判斷是否為手機。usePermission(VueUse)簡化瀏覽器 Permission API。useSessionStorage保存定位資訊,避免每次重新請求。
import { usePermission, useSessionStorage, useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 600px)')
const { state: geoPermission, request: requestGeo } = usePermission('geolocation')
const location = useSessionStorage('user-location', null)
watch(geoPermission, async (status) => {
if (status === 'granted') {
navigator.geolocation.getCurrentPosition(pos => {
location.value = pos.coords
})
}
})
總結
VueUse 是 Vue 3 生態系中不可或缺的工具箱,它把繁雜的瀏覽器 API、效能優化與常見需求抽象成簡潔的 composable,讓開發者能在 保持程式碼可讀性與可重用性的同時,快速構建功能完整的應用。
本文從安裝、核心概念、實作範例、常見陷阱到真實應用場景,完整說明了在 Vue3 專案中引入 VueUse 的最佳做法。掌握這些技巧後,你將能:
- 減少樣板程式碼,提升開發效率。
- 提升效能,利用防抖、節流與視窗觀測等功能避免不必要的渲染。
- 加速專案上線,因為大多數功能已經由社群維護與測試,可靠性高。
最後,建議持續關注 VueUse 官方文件與 GitHub Issue,隨時掌握新功能與最佳實踐,讓你的 Vue 3 專案保持在技術前沿。祝開發順利,玩得開心! 🎉