本文 AI 產出,尚未審核
Vue3 組合式函式(Composables)
useMouse、useFetch、useDarkMode 完整教學
簡介
在 Vue 3 中,組合式 API(Composition API) 讓我們可以把「狀態」與「行為」抽離成可重用的函式(即 composable),大幅提升程式碼的可讀性與維護性。
本篇文章聚焦於三個常見且實用的 composable:
useMouse– 追蹤滑鼠座標與按鍵狀態useFetch– 以最小化的方式執行 HTTP 請求、管理 loading / error / datauseDarkMode– 自動偵測系統深色模式、提供手動切換與持久化
透過這三個範例,你將學會 如何自行撰寫、使用與擴充 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會在url、options變化時重新執行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 能直接根據.darkclass 切換樣式。
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 中未處理取消請求 |
當組件卸載時,仍可能收到已完成的回應,導致錯誤。 | 使用 AbortController 於 onUnmounted 中 abort,或在 watchEffect 內返回清除函式。 |
localStorage 讀寫同步問題 |
多個 tab 同時修改會產生競爭條件。 | 監聽 storage 事件,或在需要時重新讀取。 |
最佳實踐
- 保持 composable 純粹:只做一件事,避免把 UI 相關的邏輯混進去。
- 提供可選的參數與回傳:如
useFetch允許傳入options,useDarkMode允許自訂 storage key。 - 文件化:在專案的
docs/或README中說明 API、返回值與使用限制。 - 單元測試:使用
@vue/test-utils搭配vitest,模擬fetch、matchMedia等全域 API。
實際應用場景
| 場景 | 使用哪個 composable | 為什麼適合 |
|---|---|---|
| 動態儀表板 – 顯示滑鼠所在位置的即時提示 | useMouse |
只要在圖表上方顯示座標,避免每個圖表都寫相同的事件監聽。 |
| 新聞網站 – 文章列表需要從 API 取得資料,支援下拉刷新 | useFetch |
只要呼叫 refetch() 即可重新載入,且自動管理 loading / error 狀態。 |
| 企業內部系統 – 允許使用者自行切換暗色模式,並在夜間自動切換 | useDarkMode |
透過 localStorage 保存偏好,同時偵測系統 prefers-color-scheme。 |
| 即時聊天 – 需要根據滑鼠位置顯示表情選單 | useMouse + useDarkMode |
結合滑鼠座標與暗色模式,提供一致的 UI 體驗。 |
| 資料分析平台 – 大量圖表需要同時從多個 API 抓取資料 | 多個 useFetch 實例 + 統一錯誤處理 |
每個圖表獨立管理自己的 loading / error,主畫面只需聚合結果。 |
總結
- Composable 是 Vue 3 推薦的程式碼組織方式,能讓邏輯高度模組化、易於測試與重用。
useMouse、useFetch、useDarkMode三個範例分別示範了 事件監聽、非同步資料取得、系統設定偵測與持久化 的典型應用。- 在實作時,要特別注意 生命週期清理、依賴的正確追蹤、以及 使用者體驗(loading、error、持久化)。
- 只要遵守「單一職責」與「可測試」的原則,你就能在任何 Vue 3 專案中快速打造出 乾淨、可維護且功能完整 的組合式函式。
下一步:把上述 composable 放入
src/composables/,在實際專案中逐步替換掉散落的程式碼,感受開發效率的明顯提升吧!祝你玩得開心 🎉