Vue3 課程 – 錯誤處理與除錯
主題:template 中的錯誤顯示策略
簡介
在 Vue3 應用程式中,template 是開發者與使用者互動的第一層介面。當資料或狀態異常時,若沒有適當的錯誤顯示策略,使用者很容易看到「空白」或「未預期」的畫面,進而失去信任感。
本篇文章將說明 在 Vue3 的 template 中,如何以結構化、可維護且使用者友善的方式呈現錯誤訊息。從基本的條件渲染、全域錯誤捕捉,到自訂錯誤組件與 TypeScript 型別防護,我們會一步步帶你建立完整的錯誤顯示機制,讓你的介面在任何例外情況下仍能保持可用性與一致性。
核心概念
1️⃣ 基本的條件渲染 – v-if / v-show
最直接的錯誤呈現方式是使用 v-if 或 v-show 依據錯誤狀態切換 UI。這種方式適合 單一欄位或局部錯誤,例如表單驗證失敗。
<template>
<form @submit.prevent="onSubmit">
<input v-model="email" type="email" placeholder="請輸入 Email" />
<!-- 只在 emailError 為真時顯示錯誤訊息 -->
<p v-if="emailError" class="error-msg">{{ emailError }}</p>
<button type="submit">送出</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const email = ref('')
const emailError = ref('')
// 簡易驗證範例
function onSubmit() {
if (!email.value.includes('@')) {
emailError.value = 'Email 必須包含 @ 符號'
} else {
emailError.value = ''
// 送出資料...
}
}
</script>
<style scoped>
.error-msg { color: red; margin-top: 4px; }
</style>
重點:
v-if會在條件為false時完全移除元素,適合不需要保留 DOM 結構的情況;v-show則僅切換display,適合頻繁切換的場景。
2️⃣ 錯誤收集與集中管理 – 使用 ref/reactive 物件
在較大的表單或多個 API 請求時,單一變數的錯誤訊息會變得難以維護。此時可將錯誤資訊集中在一個 錯誤物件 中,並搭配 Object.keys 產生動態錯誤列表。
<template>
<form @submit.prevent="handleSubmit">
<div v-for="field in fields" :key="field.name" class="field-group">
<label :for="field.name">{{ field.label }}</label>
<input
:id="field.name"
v-model="form[field.name]"
:type="field.type"
/>
<!-- 依據 errors[field.name] 是否有值顯示錯誤 -->
<p v-if="errors[field.name]" class="error-msg">
{{ errors[field.name] }}
</p>
</div>
<button type="submit">送出</button>
</form>
</template>
<script setup>
import { reactive, ref } from 'vue'
const fields = [
{ name: 'username', label: '使用者名稱', type: 'text' },
{ name: 'password', label: '密碼', type: 'password' },
{ name: 'email', label: 'Email', type: 'email' }
]
const form = reactive({
username: '',
password: '',
email: '',
})
const errors = reactive({
username: '',
password: '',
email: '',
})
function handleSubmit() {
// 先清空錯誤
Object.keys(errors).forEach(k => (errors[k] = ''))
// 基本驗證(實務上可改成 Yup、Vuelidate 等套件)
if (!form.username) errors.username = '請輸入使用者名稱'
if (form.password.length < 6) errors.password = '密碼長度至少 6 位'
if (!form.email.includes('@')) errors.email = 'Email 格式不正確'
// 若無錯誤則送出
if (!Object.values(errors).some(msg => msg)) {
// 呼叫 API...
console.log('送出資料', form)
}
}
</script>
<style scoped>
.field-group { margin-bottom: 12px; }
.error-msg { color: #d93025; font-size: 0.9rem; }
</style>
技巧:利用
reactive建立的errors物件,可在任何地方直接修改,Vue 會自動追蹤變化,省去大量ref的寫法。
3️⃣ 全域錯誤捕捉 – errorCaptured 與全域錯誤處理器
有時候錯誤不是由使用者輸入導致,而是 元件渲染或生命週期 中的例外。Vue3 提供 errorCaptured 鉤子讓子元件的錯誤可以被父層捕獲,進一步顯示統一的錯誤 UI。
<!-- ParentComponent.vue -->
<template>
<div class="app">
<ErrorBanner v-if="globalError" :message="globalError" @close="globalError = ''" />
<ChildComponent />
</div>
</template>
<script setup>
import { ref } from 'vue'
import ErrorBanner from './ErrorBanner.vue'
import ChildComponent from './ChildComponent.vue'
const globalError = ref('')
// 捕獲子元件錯誤
function onError(err, instance, info) {
console.error('捕獲錯誤:', err, info)
globalError.value = err.message || '發生未知錯誤'
// 回傳 false 讓錯誤不再向上冒泡(視需求決定)
return false
}
</script>
<!-- 使用全域錯誤捕捉 -->
<script>
export default {
// Vue 3 Options API 寫法
errorCaptured(err, instance, info) {
return this.$setupState.onError(err, instance, info)
}
}
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<h2>子元件</h2>
<button @click="triggerError">觸發錯誤</button>
</div>
</template>
<script setup>
function triggerError() {
// 故意拋出錯誤,測試 errorCaptured
throw new Error('子元件執行時發生錯誤')
}
</script>
<!-- ErrorBanner.vue -->
<template>
<div class="error-banner">
<span>{{ message }}</span>
<button @click="$emit('close')">✕</button>
</div>
</template>
<script setup>
defineProps({
message: { type: String, required: true }
})
</script>
<style scoped>
.error-banner {
background: #fdecea;
color: #b71c1c;
padding: 12px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
實務建議:將
ErrorBanner放在根元件(如App.vue)或布局層,讓所有子頁面共用同一套錯誤顯示樣式,提升使用者體驗。
4️⃣ 自訂錯誤組件 + 插槽 – 彈性化的錯誤呈現
若專案需要 多種錯誤樣式(例如表單錯誤、系統錯誤、網路斷線),可以建立一個通用的 ErrorBox 組件,並透過插槽讓呼叫端自訂內容。
<!-- ErrorBox.vue -->
<template>
<transition name="fade">
<div v-if="visible" class="error-box" role="alert">
<slot name="icon">
<!-- 預設圖示 -->
<svg class="icon" viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20..." /></svg>
</slot>
<div class="msg">
<slot>{{ message }}</slot>
</div>
<button class="close" @click="close">✕</button>
</div>
</transition>
</template>
<script setup>
import { ref, watch } from 'vue'
defineProps({
modelValue: { type: Boolean, default: false },
message: { type: String, default: '' }
})
const visible = ref(false)
watch(() => modelValue, v => (visible.value = v))
function close() {
visible.value = false
// 使用 v-model 讓外層同步狀態
emit('update:modelValue', false)
}
</script>
<style scoped>
.error-box {
display: flex;
align-items: center;
background: #ffebee;
color: #c62828;
padding: 10px 16px;
border-radius: 4px;
margin: 8px 0;
}
.icon { width: 20px; height: 20px; margin-right: 8px; }
.close { background: transparent; border: none; margin-left: auto; cursor: pointer; }
.fade-enter-active, .fade-leave-active { transition: opacity .3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
使用方式:
<template>
<ErrorBox v-model="showError" message="系統暫時無法連線">
<template #icon>
<!-- 自訂圖示 -->
<svg class="icon" viewBox="0 0 24 24"><path d="M12 4a8 8 0 0 1 0 16..." /></svg>
</template>
<template #default>
<strong>網路問題:</strong> 請檢查您的網路連線後再重試。
</template>
</ErrorBox>
<button @click="showError = true">測試錯誤框</button>
</template>
<script setup>
import { ref } from 'vue'
import ErrorBox from '@/components/ErrorBox.vue'
const showError = ref(false)
</script>
優點:錯誤框的外觀與行為完全抽離於業務邏輯,讓 UI 團隊可以獨立調整樣式,開發者只負責傳遞訊息與控制顯示。
5️⃣ 與 TypeScript 搭配的錯誤類型安全
在大型專案中,錯誤型別的統一 能大幅降低因錯誤訊息不一致或拼寫錯誤導致的 UI bug。以下示範如何透過 TypeScript 定義錯誤代碼與對應訊息,並在 template 中使用 computed 產生最終顯示文字。
// errorTypes.ts
export enum ApiErrorCode {
NETWORK = 'NETWORK',
UNAUTHORIZED = 'UNAUTHORIZED',
VALIDATION = 'VALIDATION',
UNKNOWN = 'UNKNOWN'
}
export const errorMessages: Record<ApiErrorCode, string> = {
[ApiErrorCode.NETWORK]: '網路連線失敗,請稍後再試',
[ApiErrorCode.UNAUTHORIZED]: '登入資訊已失效,請重新登入',
[ApiErrorCode.VALIDATION]: '資料格式錯誤,請檢查輸入內容',
[ApiErrorCode.UNKNOWN]: '發生未知錯誤,請聯繫客服'
}
<!-- ApiErrorDisplay.vue -->
<template>
<ErrorBox v-model="visible" :message="displayMessage" />
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ApiErrorCode, errorMessages } from '@/utils/errorTypes'
import ErrorBox from '@/components/ErrorBox.vue'
interface Props {
errorCode?: ApiErrorCode
}
const props = defineProps<Props>()
const visible = ref(false)
const displayMessage = computed(() => {
if (!props.errorCode) return ''
return errorMessages[props.errorCode] ?? errorMessages[ApiErrorCode.UNKNOWN]
})
// 當 errorCode 改變時自動開啟錯誤框
watch(() => props.errorCode, (code) => {
visible.value = !!code
})
</script>
實務意義:當後端回傳錯誤碼時,只需要把
errorCode傳給ApiErrorDisplay,不必在每個元件手動寫字串,保持訊息的一致性與可維護性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
1. 直接在 template 中使用 try/catch |
Vue 的 template 只能寫表達式,無法放 try/catch,會導致編譯錯誤。 |
把可能拋錯的程式碼搬到 setup 或 methods 中,並在那裡捕捉後以 ref/reactive 形式回傳錯誤狀態。 |
| 2. 錯誤訊息未被響應式追蹤 | 若直接修改普通物件的屬性(非 reactive),Vue 不會更新 UI。 |
使用 reactive 或 ref 包裝錯誤物件,或透過 set(Vue2)/Vue.set(Vue3 的 reactive 仍需)確保屬性是響應式的。 |
| 3. 多重條件渲染導致 UI 閃爍 | 同時使用 v-if、v-else-if、v-else 產生頻繁的 DOM 重建。 |
優先使用 v-show 或將錯誤訊息抽成子組件,減少父層的重繪成本。 |
| 4. 全域錯誤捕捉過度使用 | 把所有錯誤都交給 errorCaptured,會掩蓋本該在 UI 層處理的表單驗證錯誤。 |
僅在 非預期(如程式錯誤、網路斷線)時使用全域捕捉,表單驗證仍建議使用局部 v-if。 |
| 5. 錯誤訊息硬寫在組件內 | 隨著多語系需求或訊息變更,需要大量手動修改。 | 把錯誤文字抽成 i18n(如 vue-i18n)或錯誤代碼映射表,保持 UI 與文字分離。 |
最佳實踐總結:
- 錯誤狀態要響應式:
ref/reactive為首選。 - 錯誤顯示與業務邏輯分離:使用自訂錯誤組件或插槽。
- 盡量統一錯誤訊息來源:錯誤代碼 + i18n / 常量表。
- 只在必要時使用全域捕捉,避免把可預期錯誤吞掉。
- 為每個錯誤提供可操作的回饋(如「重新整理」或「聯絡客服」按鈕),提升使用者體驗。
實際應用場景
🎯 場景一:表單驗證失敗的即時提示
在電商結帳頁面,使用者必須填寫收件人資訊。若欄位不符合規則,立即在對應輸入框下方顯示錯誤,並在提交時阻止 API 呼叫。
- 使用 集中管理的
errors物件,配合v-for動態渲染錯誤列表。 - 搭配 自訂
ErrorBox,讓錯誤樣式與整體 UI 保持一致。
🎯 場景二:API 回傳 401(未授權)時的全域提醒
使用者在多頁面中操作時,若 token 過期,所有 AJAX 請求會回傳 401。此時:
- 在 Axios 攔截器 捕捉 401,設定全域
authError為true。 - 在根層
App.vue中使用ErrorBanner,顯示「登入已失效,請重新登入」的訊息並提供「立即前往登入」按鈕。
🎯 場景三:網路斷線或服務不可用的 fallback UI
當使用者的裝置失去網路或伺服器異常,整個 SPA 仍需保持可操作:
- 利用 全域錯誤捕捉 +
ErrorBox,在errorCaptured中判斷err.message是否為網路相關。 - 同時在 Vue Router 的
beforeEach中檢查navigator.onLine,若離線則導向「離線模式」頁面。
🎯 場景四:多語系錯誤訊息的統一管理
大型 SaaS 平台支援中、英、日三種語系:
- 定義 錯誤代碼(
ApiErrorCode)與 i18n 資源檔(en.json、zh-TW.json、ja.json)。 - 在 API 呼叫失敗後,根據回傳的錯誤碼取出當前語系的訊息,交給
ApiErrorDisplay組件渲染。
總結
在 Vue3 中,錯誤顯示策略不僅是 UI 的細節,更是提升使用者信任與產品品質的關鍵。本文從最基礎的 v-if 條件渲染,延伸到全域錯誤捕捉、自訂錯誤組件、TypeScript 型別安全與多語系管理,提供了 完整、可維護且實務導向的錯誤處理框架。
記住:
- 錯誤狀態必須是響應式的,才能保證 UI 即時更新。
- 錯誤呈現與業務邏輯要分離,使用組件化、插槽或全域錯誤處理器。
- 統一錯誤訊息來源(代碼 + i18n)能減少維護成本,並支援多語系需求。
透過上述技巧,你可以在任何 Vue3 專案中快速建立 友善、可靠且易於擴充的錯誤顯示機制,讓使用者即使在遇到問題時,也能得到清晰的指引與解決方案。祝開發順利,錯誤處理不再是痛點! 🚀