本文 AI 產出,尚未審核

Vue3 組合式函式(Composables)‧ 使用第三方庫 VueUse


簡介

在 Vue 3 中,組合式 API(Composition API) 為開發者提供了更彈性的程式碼組織方式。透過 setup()refcomputedwatch 等核心函式,我們可以把邏輯切割成可重用的 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 useFetchuseMediaQueryuseWindowSize 包裝 fetch、媒體查詢、視窗尺寸等
狀態管理 useLocalStorageuseSessionStorageuseStorage 把資料同步到 localStoragesessionStorage
效能優化 useDebounceFnuseThrottleFnuseRafFn 防抖、節流、RAF 迴圈
事件處理 useEventListeneruseMouseuseKeyPress 監聽全域或元素事件
可視化 useIntersectionObserveruseResizeObserver 觀測元素可見度與尺寸變化

以下將以實作範例說明最常用的幾個 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 }) 提供預設值。
副作用未清理 useEventListeneruseIntersectionObserver 等會自動註冊監聽器,但在組件卸載前忘記清理可能造成記憶體泄漏。 大多數 VueUse composable 會在 onUnmounted 自動清理,但若自行建立 watchEffect,記得回傳清理函式。
錯誤處理不當 useFetch 本身不會拋出例外,而是把錯誤放在 error 變數。若直接使用 await useFetch(...).json() 可能忽略錯誤。 始終檢查 error 或使用 try/catch 包裹自訂的 async 函式。
重複狀態 同時使用 useLocalStorage 與 Vuex/Pinia 保存相同資料,會導致同步困難。 選擇單一來源(例如全部使用 Pinia 並在 store 中讀寫 localStorage),或在 composable 中封裝兩者的同步邏輯。

Best Practices

  1. 模組化 composable
    • 為每個功能建立專屬檔案,例如 useTheme.jsuseSearch.js,保持單一職責原則。
  2. 型別安全(若使用 TypeScript)
    • 利用 VueUse 已提供的泛型,例如 useLocalStorage<T>('key', defaultValue as T),確保取出的資料類型正確。
  3. 預設值與懶加載
    • 大多數 composable 支援傳入初始值,避免在 SSR 時出現 nullundefined
  4. 組合式 API 與 Options API 混用
    • 若專案仍保留 Options API,建議在 setup() 中使用 VueUse,其他選項保留不變,降低遷移成本。
  5. 效能監控
    • 使用 useRafFnuseThrottleFn 包裝頻繁的計算,減少不必要的重新渲染。

實際應用場景

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
  • 解法
    • useMediaQueryuseWindowSize 判斷是否為手機。
    • 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 專案保持在技術前沿。祝開發順利,玩得開心! 🎉