本文 AI 產出,尚未審核

Vue3 組合式函式(Composables)

useMouseuseFetchuseDarkMode 完整教學


簡介

在 Vue 3 中,組合式 API(Composition API) 讓我們可以把「狀態」與「行為」抽離成可重用的函式(即 composable),大幅提升程式碼的可讀性與維護性。
本篇文章聚焦於三個常見且實用的 composable:

  1. useMouse – 追蹤滑鼠座標與按鍵狀態
  2. useFetch – 以最小化的方式執行 HTTP 請求、管理 loading / error / data
  3. useDarkMode – 自動偵測系統深色模式、提供手動切換與持久化

透過這三個範例,你將學會 如何自行撰寫、使用與擴充 composable,從而在專案中快速建立乾淨、可測試的功能模組。


核心概念

1. 為什麼要使用 composable?

  • 關注點分離:把與 UI 無關的邏輯(例如 API 請求、滑鼠追蹤)抽成獨立函式,讓 component 只關心「要顯示什麼」。
  • 高度重用:同一個 composable 可以在多個 component 中直接匯入,避免重複程式碼。
  • 易於測試:純函式的特性讓單元測試變得簡單,只要傳入相同的依賴即可得到預期結果。

2. useMouse – 追蹤滑鼠資訊

2.1 基本實作

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)          // 滑鼠 X 座標
  const y = ref(0)          // 滑鼠 Y 座標
  const isDown = ref(false) // 滑鼠左鍵是否按下

  const update = (e) => {
    x.value = e.clientX
    y.value = e.clientY
  }

  const down = () => (isDown.value = true)
  const up = () => (isDown.value = false)

  onMounted(() => {
    window.addEventListener('mousemove', update)
    window.addEventListener('mousedown', down)
    window.addEventListener('mouseup', up)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
    window.removeEventListener('mousedown', down)
    window.removeEventListener('mouseup', up)
  })

  return { x, y, isDown }
}

說明

  • ref 用來建立響應式資料。
  • onMounted / onUnmounted 確保在 component 生命週期內正確掛載與移除事件監聽,避免記憶體洩漏。

2.2 在 component 中使用

<template>
  <div class="mouse-info">
    <p>滑鼠座標:({{ x }}, {{ y }})</p>
    <p>左鍵狀態:{{ isDown ? '按下' : '放開' }}</p>
  </div>
</template>

<script setup>
import { useMouse } from '@/composables/useMouse'

const { x, y, isDown } = useMouse()
</script>

<style scoped>
.mouse-info { font-family: monospace; }
</style>

3. useFetch – 輕量級的資料取得

3.1 基本版(支援 GET)

// composables/useFetch.js
import { ref, watchEffect } from 'vue'

export function useFetch(url, options = {}) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  const fetchData = async () => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url, options)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 當 url 或 options 改變時自動重新抓取
  watchEffect(() => {
    fetchData()
  })

  return { data, error, loading, refetch: fetchData }
}

重點

  • watchEffect 會在 urloptions 變化時重新執行 fetchData,適合 動態參數 的情境。
  • 回傳的 refetch 讓使用者手動觸發重新請求。

3.2 使用範例(顯示 GitHub 使用者資訊)

<template>
  <div v-if="loading">載入中...</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.followers }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const username = ref('vuejs')
const { data, error, loading, refetch } = useFetch(
  () => `https://api.github.com/users/${username.value}`
)
</script>

3.3 支援 POST、PUT 等方法

// 使用範例:送出表單
import { useFetch } from '@/composables/useFetch'

const { data, error, loading, refetch } = useFetch(
  'https://example.com/api/comments',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: 'Hello Vue 3!' })
  }
)

4. useDarkMode – 深色模式偵測與切換

4.1 核心實作

// composables/useDarkMode.js
import { ref, watch, onMounted } from 'vue'

export function useDarkMode(key = 'dark-mode') {
  const isDark = ref(false)

  // 從 localStorage 讀取使用者先前的選擇
  const load = () => {
    const stored = localStorage.getItem(key)
    if (stored !== null) {
      isDark.value = stored === 'true'
    } else {
      // 若無儲存值,使用系統偏好
      isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
    }
    applyClass()
  }

  const applyClass = () => {
    document.documentElement.classList.toggle('dark', isDark.value)
  }

  const toggle = () => {
    isDark.value = !isDark.value
  }

  // 監看變化,寫回 localStorage 並更新 class
  watch(isDark, (val) => {
    localStorage.setItem(key, val)
    applyClass()
  })

  onMounted(load)

  // 監聽系統偏好變化(僅在沒有使用者自行設定時有效)
  const media = window.matchMedia('(prefers-color-scheme: dark)')
  const systemListener = (e) => {
    if (localStorage.getItem(key) === null) {
      isDark.value = e.matches
    }
  }
  media.addEventListener('change', systemListener)

  return { isDark, toggle }
}

說明

  • 透過 localStorage 保存使用者偏好,避免每次刷新頁面時重置。
  • document.documentElement.classList.toggle('dark', ...) 讓 Tailwind、Bootstrap 或自訂 CSS 能直接根據 .dark class 切換樣式。

4.2 在 UI 中使用

<template>
  <button @click="toggle">
    {{ isDark ? '切換為淺色模式' : '切換為深色模式' }}
  </button>
</template>

<script setup>
import { useDarkMode } from '@/composables/useDarkMode'

const { isDark, toggle } = useDarkMode()
</script>

<style>
/* 簡易深色樣式示例 */
html.dark body {
  background:#121212;
  color:#e0e0e0;
}
</style>

常見陷阱與最佳實踐

陷阱 說明 解決方案
事件未在 onUnmounted 移除 會導致記憶體洩漏或多次觸發同一事件。 確保所有 addEventListener 都對應 removeEventListener,如 useMouse 範例所示。
watchEffect 產生不必要的重抓取 url 為字串常量,watchEffect 每次渲染都會呼叫 fetchData 使用 watch 搭配 immediate: true,或把 url 包在函式中(如 useFetch(() => ...))以避免重複。
深色模式切換時 CSS 沒同步 僅變更 class 而未在 CSS 中正確寫入對應樣式。 確認所有需要變色的樣式都有 .dark 前綴或使用 Tailwind 的 dark: 前置詞。
useFetch 中未處理取消請求 當組件卸載時,仍可能收到已完成的回應,導致錯誤。 使用 AbortControlleronUnmounted 中 abort,或在 watchEffect 內返回清除函式。
localStorage 讀寫同步問題 多個 tab 同時修改會產生競爭條件。 監聽 storage 事件,或在需要時重新讀取。

最佳實踐

  1. 保持 composable 純粹:只做一件事,避免把 UI 相關的邏輯混進去。
  2. 提供可選的參數與回傳:如 useFetch 允許傳入 optionsuseDarkMode 允許自訂 storage key。
  3. 文件化:在專案的 docs/README 中說明 API、返回值與使用限制。
  4. 單元測試:使用 @vue/test-utils 搭配 vitest,模擬 fetchmatchMedia 等全域 API。

實際應用場景

場景 使用哪個 composable 為什麼適合
動態儀表板 – 顯示滑鼠所在位置的即時提示 useMouse 只要在圖表上方顯示座標,避免每個圖表都寫相同的事件監聽。
新聞網站 – 文章列表需要從 API 取得資料,支援下拉刷新 useFetch 只要呼叫 refetch() 即可重新載入,且自動管理 loading / error 狀態。
企業內部系統 – 允許使用者自行切換暗色模式,並在夜間自動切換 useDarkMode 透過 localStorage 保存偏好,同時偵測系統 prefers-color-scheme
即時聊天 – 需要根據滑鼠位置顯示表情選單 useMouse + useDarkMode 結合滑鼠座標與暗色模式,提供一致的 UI 體驗。
資料分析平台 – 大量圖表需要同時從多個 API 抓取資料 多個 useFetch 實例 + 統一錯誤處理 每個圖表獨立管理自己的 loading / error,主畫面只需聚合結果。

總結

  • Composable 是 Vue 3 推薦的程式碼組織方式,能讓邏輯高度模組化、易於測試與重用。
  • useMouseuseFetchuseDarkMode 三個範例分別示範了 事件監聽非同步資料取得系統設定偵測與持久化 的典型應用。
  • 在實作時,要特別注意 生命週期清理依賴的正確追蹤、以及 使用者體驗(loading、error、持久化)
  • 只要遵守「單一職責」與「可測試」的原則,你就能在任何 Vue 3 專案中快速打造出 乾淨、可維護且功能完整 的組合式函式。

下一步:把上述 composable 放入 src/composables/,在實際專案中逐步替換掉散落的程式碼,感受開發效率的明顯提升吧!祝你玩得開心 🎉