
Vue3 Frontend
- 93 installs
- 2 repo stars
- Updated April 3, 2026
- eva813/vue3-skills
Helps with frontend development tasks.
About
vue3-frontend is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted coding.
- vue3-frontend
- Frontend Development
- AI-coding skill
Vue3 Frontend by the numbers
- 93 all-time installs (skills.sh)
- Ranked #1,078 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/eva813/vue3-skills --skill vue3-frontendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 93 |
|---|---|
| repo stars | ★ 2 |
| Last updated | April 3, 2026 |
| Repository | eva813/vue3-skills ↗ |
What it does
Helps with frontend development tasks.
Files
Vue 3 Frontend Development Skill
全面的 Vue 3 前端開發技能,提供元件範本、最佳實踐指南、遷移協助和常見模式。
Core Capabilities
1. Component Development
建立新的 Vue 3 元件,使用 <script setup> 語法和 Composition API。
Available templates:
assets/component-templates/BasicComponent.vue- 基本元件範本assets/component-templates/FormComponent.vue- 完整的表單元件(含驗證)assets/component-templates/DataTable.vue- 資料表格元件(含排序、分頁、搜尋)assets/component-templates/Modal.vue- 模態框元件(含完整功能)
Usage:
# 複製範本到專案
cp assets/component-templates/BasicComponent.vue src/components/YourComponent.vue2. Composables Development
建立可重用的邏輯抽象。
Available templates:
assets/composable-templates/useFetch.js- API 請求封裝assets/composable-templates/useLocalStorage.js- localStorage 同步狀態
Usage:
# 複製到專案
cp assets/composable-templates/useFetch.js src/composables/3. Vue 2 to Vue 3 Migration
協助將 Vue 2 專案遷移到 Vue 3。
Migration workflow: 1. Read references/migration-guide.md for breaking changes 2. Identify deprecated APIs in existing code 3. Apply migration patterns from the guide 4. Test thoroughly
Key migration areas:
- Global API (Vue.use → app.use)
- Reactivity system (Vue.set → direct assignment)
- v-model syntax (value/input → modelValue/update:modelValue)
- Lifecycle hooks (destroyed → unmounted)
- Filters removal (use computed/methods)
- Event Bus replacement (use mitt/Pinia)
4. Code Review and Optimization
檢查和優化 Vue 3 程式碼品質。
Review checklist:
- Use
<script setup>for better performance - Proper ref/reactive usage
- Computed vs methods
- v-show vs v-if
- Key usage in v-for
- Component splitting
- Props validation
- Error handling
Reference Documentation
Essential References
Always read these when working on specific tasks:
Migration tasks:
references/migration-guide.md- Complete Vue 2 to Vue 3 migration guide with all breaking changes
Composition API usage:
references/composition-api.md- Comprehensive Composition API reference (ref, reactive, computed, watch, lifecycle, etc.)
Best practices:
references/best-practices.md- Vue 3 best practices covering component design, reactivity, performance, code organization, error handling, TypeScript
Common patterns:
references/common-patterns.md- Form handling, data fetching, list rendering, modals, state management, routing, i18n
When to Read Each Reference
Before writing any component: 1. Check best-practices.md for component design patterns 2. Review relevant patterns in common-patterns.md
Before migration: 1. Read entire migration-guide.md 2. Reference composition-api.md for new API syntax
When implementing features:
- Forms →
common-patterns.mdForm Handling section - Data fetching →
common-patterns.mdData Fetching section - State management →
common-patterns.mdState Management section - i18n →
common-patterns.mdi18n section
Common Development Workflows
Workflow 1: Create a New Component
# 1. Choose appropriate template
view assets/component-templates/
# 2. Read best practices
view references/best-practices.md
# 3. Copy and customize template
cp assets/component-templates/BasicComponent.vue src/components/MyComponent.vue
# 4. Follow naming conventions (PascalCase)
# 5. Define props with validation
# 6. Use <script setup> syntax
# 7. Keep component focused and smallWorkflow 2: Migrate Vue 2 Code to Vue 3
# 1. Read migration guide first
view references/migration-guide.md
# 2. Identify patterns to migrate
# - Options API → Composition API
# - this.$set → direct assignment
# - filters → computed/methods
# - Event Bus → mitt/Pinia
# 3. Apply changes systematically
# 4. Test each change
# 5. Update dependenciesWorkflow 3: Implement Data Fetching
# 1. Review data fetching patterns
view references/common-patterns.md
# Look for "Data Fetching Patterns" section
# 2. Choose approach:
# - Custom composable (useFetch template)
# - VueUse (@vueuse/core)
# - TanStack Query
# 3. Implement with error handling and loading statesWorkflow 4: Optimize Performance
# 1. Review performance section
view references/best-practices.md
# Look for "Performance Optimization" section
# 2. Check for issues:
# - Using methods instead of computed
# - Unnecessary v-if usage
# - Missing keys in v-for
# - Large components without splitting
# - No lazy loading
# 3. Apply optimizations:
# - Use computed for derived data
# - v-show for frequent toggles
# - defineAsyncComponent for heavy components
# - Virtual scrolling for large listsWorkflow 5: Form Development
# 1. Use form template or VeeValidate pattern
view assets/component-templates/FormComponent.vue
view references/common-patterns.md
# Look for "Form Handling Patterns" section
# 2. Implement validation
# 3. Handle errors and loading states
# 4. Add success/error feedbackQuick Reference Commands
Component Templates
# List all templates
ls -la assets/component-templates/
# Copy specific template
cp assets/component-templates/[TemplateName].vue src/components/Composable Templates
# List all composables
ls -la assets/composable-templates/
# Copy specific composable
cp assets/composable-templates/[composableName].js src/composables/Read Documentation
# Migration guide
view references/migration-guide.md
# Composition API reference
view references/composition-api.md
# Best practices
view references/best-practices.md
# Common patterns
view references/common-patterns.mdBest Practices Summary
Component Design
- Always use
<script setup>for better performance and DX - Define props with full validation
- Use composables for reusable logic
- Keep components small and focused (< 200 lines)
- Extract complex logic into composables
Reactivity
- Use
ref()for primitives - Use
reactive()for objects - Use
computed()for derived data - Use
watch()when you need old values - Use
watchEffect()for automatic dependency tracking
Performance
- Use
computed()not methods for template calculations - Use
v-showfor frequent toggles - Use
v-iffor initial render conditions - Always use unique
keyinv-for - Lazy load heavy components with
defineAsyncComponent() - Use virtual scrolling for large lists
Code Organization
src/
├── assets/ # Static resources
├── components/ # Reusable components
│ ├── common/ # Base components
│ ├── layout/ # Layout components
│ └── features/ # Feature components
├── composables/ # Reusable logic
├── stores/ # Pinia stores
├── router/ # Router config
├── views/ # Page components
├── utils/ # Utility functions
└── services/ # API servicesTypeScript Support
When using TypeScript:
- Define prop types with interfaces
- Use generics in composables
- Type emits properly
- Use
defineComponentwhen needed
See best-practices.md TypeScript Integration section for details.
Common Gotchas
1. Destructuring reactive objects loses reactivity
- Use
toRefs()or access properties directly
2. Direct array/object mutation in Vue 2 style
- Vue 3 doesn't need
$set, direct mutation works
3. Forgetting .value with refs
- Remember: refs need
.valuein<script>, not in<template>
4. Using reactive() for primitives
- Use
ref()for primitives,reactive()for objects
5. Not cleaning up side effects
- Always clean up in
onBeforeUnmount()or usewatchEffect()cleanup
Additional Resources
- Official Vue 3 docs: https://vuejs.org/
- Vue 3 Migration guide: https://v3-migration.vuejs.org/
- Composition API FAQ: https://vuejs.org/guide/extras/composition-api-faq.html
- VueUse: https://vueuse.org/ (utility composables)
- Pinia: https://pinia.vuejs.org/ (state management)
Tips for Using This Skill
1. Always start with references: Read relevant documentation before coding 2. Use templates as starting points: Don't write from scratch if a template exists 3. Follow the workflows: They embody best practices 4. Check best practices regularly: Internalize the patterns 5. When stuck: Check common-patterns.md for similar examples
import { ref, unref } from 'vue'
/**
* 通用的資料獲取 composable
* @param {string | Ref<string>} url - API URL
* @param {object} options - Fetch 選項
* @returns {object} - { data, error, loading, execute, refetch }
*/
export function useFetch(url, options = {}) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const execute = async (customUrl = null) => {
loading.value = true
error.value = null
try {
const targetUrl = customUrl || unref(url)
const response = await fetch(targetUrl, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
data.value = result
if (options.onSuccess) {
options.onSuccess(result)
}
return result
} catch (e) {
error.value = e
console.error('Fetch error:', e)
if (options.onError) {
options.onError(e)
}
throw e
} finally {
loading.value = false
}
}
const refetch = () => execute()
// 如果 immediate 為 true,立即執行
if (options.immediate !== false) {
execute()
}
return {
data,
error,
loading,
execute,
refetch
}
}
/**
* POST 請求的簡化版本
*/
export function usePost(url, options = {}) {
return useFetch(url, {
method: 'POST',
...options,
immediate: false
})
}
/**
* PUT 請求的簡化版本
*/
export function usePut(url, options = {}) {
return useFetch(url, {
method: 'PUT',
...options,
immediate: false
})
}
/**
* DELETE 請求的簡化版本
*/
export function useDelete(url, options = {}) {
return useFetch(url, {
method: 'DELETE',
...options,
immediate: false
})
}import { ref, watch } from 'vue'
/**
* 與 localStorage 同步的響應式狀態
* @param {string} key - localStorage key
* @param {any} defaultValue - 預設值
* @returns {Ref} - 響應式引用
*/
export function useLocalStorage(key, defaultValue = null) {
// 讀取初始值
const readValue = () => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : defaultValue
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error)
return defaultValue
}
}
const storedValue = ref(readValue())
// 寫入 localStorage
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue.value) : value
storedValue.value = valueToStore
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
// 移除項目
const removeValue = () => {
try {
window.localStorage.removeItem(key)
storedValue.value = defaultValue
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error)
}
}
// 監聽其他頁籤的變化
const handleStorageChange = (e) => {
if (e.key === key && e.newValue !== null) {
storedValue.value = JSON.parse(e.newValue)
}
}
window.addEventListener('storage', handleStorageChange)
// 清理
const stop = () => {
window.removeEventListener('storage', handleStorageChange)
}
return {
value: storedValue,
setValue,
removeValue,
stop
}
}
/**
* 簡化版本 - 直接返回 ref
*/
export function useStorage(key, defaultValue = null) {
const { value, setValue } = useLocalStorage(key, defaultValue)
// 監聽 ref 變化並自動儲存
watch(value, (newValue) => {
setValue(newValue)
}, { deep: true })
return value
}Vue 3 Best Practices
Component Design
1. 使用 <script setup> 語法
更簡潔、效能更好、更好的 TypeScript 支援。
<!-- ✅ 推薦 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<!-- ❌ 避免 (除非必要) -->
<script>
export default {
setup() {
const count = ref(0)
return { count }
}
}
</script>2. Props 定義要明確且嚴格
<script setup>
// ✅ 推薦 - 明確型別和驗證
const props = defineProps({
title: {
type: String,
required: true
},
likes: {
type: Number,
default: 0,
validator: (value) => value >= 0
},
status: {
type: String,
enum: ['draft', 'published', 'archived']
}
})
// ❌ 避免 - 過於寬鬆
const props = defineProps(['title', 'likes', 'status'])
</script>3. 使用 Composables 封裝可重用邏輯
// ✅ 推薦 - 建立可重用的 composable
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
return { data, error, loading, fetchData }
}
// 在元件中使用
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, error, loading, fetchData } = useFetch('/api/users')
</script>4. 適當的元件拆分
<!-- ❌ 避免 - 單一元件過於龐大 -->
<template>
<div>
<!-- 100+ 行的複雜模板 -->
</div>
</template>
<!-- ✅ 推薦 - 拆分成更小的元件 -->
<template>
<div>
<UserHeader :user="user" />
<UserProfile :user="user" />
<UserActions :user="user" @update="handleUpdate" />
</div>
</template>Reactivity Best Practices
1. 選擇正確的響應式 API
// ✅ 基本型別使用 ref
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)
// ✅ 複雜物件使用 reactive
const user = reactive({
name: 'EVA',
role: 'Frontend Engineer',
settings: {
theme: 'dark',
notifications: true
}
})
// ❌ 避免 - 基本型別使用 reactive
const state = reactive({
count: 0 // 應該用 ref(0)
})
// ❌ 避免 - 需要重新賦值的物件使用 reactive
let state = reactive({ data: [] })
state = { data: newData } // 失去響應性!
// ✅ 應該用 ref
const state = ref({ data: [] })
state.value = { data: newData } // OK2. 避免直接解構響應式物件
const user = reactive({
name: 'EVA',
age: 25
})
// ❌ 避免 - 失去響應性
const { name, age } = user
// ✅ 使用 toRefs
import { toRefs } from 'vue'
const { name, age } = toRefs(user)
// ✅ 或使用 computed
import { computed } from 'vue'
const userName = computed(() => user.name)3. 正確使用 watch
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
// ✅ 需要舊值時使用 watch
watch(count, (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`)
})
// ✅ 不需要舊值,自動追蹤依賴時使用 watchEffect
watchEffect(() => {
console.log(`Count is ${count.value}`)
})
// ✅ 觀察物件的特定屬性
const user = reactive({ name: 'EVA', age: 25 })
watch(
() => user.name,
(newName) => {
console.log(`Name changed to ${newName}`)
}
)
// ❌ 避免 - 在 watch 中執行副作用但沒有清理
watch(source, () => {
const timer = setInterval(() => {}, 1000)
// 缺少清理邏輯!
})
// ✅ 正確清理副作用
watch(source, (newVal, oldVal, onCleanup) => {
const timer = setInterval(() => {}, 1000)
onCleanup(() => {
clearInterval(timer)
})
})Performance Optimization
1. 使用 computed 而非 method
<script setup>
import { ref, computed } from 'vue'
const items = ref([1, 2, 3, 4, 5])
// ✅ 推薦 - computed 會快取結果
const filteredItems = computed(() => {
return items.value.filter(item => item > 2)
})
// ❌ 避免 - method 每次都會重新執行
const getFilteredItems = () => {
return items.value.filter(item => item > 2)
}
</script>
<template>
<!-- ✅ 使用 computed -->
<div v-for="item in filteredItems" :key="item">{{ item }}</div>
<!-- ❌ 每次渲染都會執行 -->
<div v-for="item in getFilteredItems()" :key="item">{{ item }}</div>
</template>2. 使用 v-show vs v-if
<template>
<!-- ✅ 頻繁切換使用 v-show -->
<div v-show="isVisible">Content</div>
<!-- ✅ 初始渲染條件使用 v-if -->
<div v-if="hasPermission">Admin Panel</div>
<!-- ✅ 互斥條件使用 v-if/v-else-if/v-else -->
<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else>Other</div>
</template>3. 正確使用 key
<template>
<!-- ✅ v-for 必須使用唯一的 key -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- ❌ 避免使用 index 作為 key (除非列表是靜態的) -->
<div v-for="(item, index) in items" :key="index">
{{ item.name }}
</div>
<!-- ✅ 強制重新渲染時使用 key -->
<UserProfile :key="userId" :user-id="userId" />
</template>4. 延遲載入大型元件
// ✅ 使用 defineAsyncComponent
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
// ✅ 帶載入和錯誤狀態
const HeavyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})5. 使用 KeepAlive 快取元件
<template>
<!-- ✅ 快取動態元件 -->
<KeepAlive :max="10">
<component :is="currentComponent" />
</KeepAlive>
<!-- ✅ 快取路由元件 -->
<router-view v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</router-view>
<!-- ✅ 條件快取 -->
<KeepAlive :include="['ComponentA', 'ComponentB']">
<component :is="currentComponent" />
</KeepAlive>
</template>6. 虛擬滾動處理大列表
<script setup>
// 使用 vue-virtual-scroller 或類似函式庫
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const items = ref([/* 10000+ items */])
</script>
<template>
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>
</template>Code Organization
1. 檔案結構
src/
├── assets/ # 靜態資源
├── components/ # 通用元件
│ ├── common/ # 基礎元件 (Button, Input)
│ ├── layout/ # 版面元件 (Header, Footer)
│ └── features/ # 功能元件
├── composables/ # 可重用邏輯
├── stores/ # Pinia stores
├── router/ # 路由設定
├── views/ # 頁面元件
├── utils/ # 工具函數
├── services/ # API 服務
├── types/ # TypeScript 型別定義
└── constants/ # 常數定義2. 元件命名規範
<!-- ✅ 使用 PascalCase -->
<script setup>
import UserProfile from '@/components/UserProfile.vue'
import TheHeader from '@/components/layout/TheHeader.vue'
</script>
<template>
<TheHeader />
<UserProfile />
</template>
<!-- 檔案命名 -->
<!-- ✅ 推薦 -->
UserProfile.vue
TheHeader.vue
BaseButton.vue
<!-- ❌ 避免 -->
userprofile.vue
header.vue
button.vue3. Composables 命名規範
// ✅ 使用 use 前綴
export function useAuth() { }
export function useFetch() { }
export function useLocalStorage() { }
// ❌ 避免
export function auth() { }
export function fetch() { }Error Handling
1. 全域錯誤處理
// main.js
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
console.error('Global error:', err)
console.error('Component:', instance)
console.error('Error info:', info)
// 發送到錯誤追蹤服務
// trackError(err, { component: instance, info })
}2. 元件內錯誤處理
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err, instance, info) => {
error.value = err.message
console.error('Captured error:', err)
// 返回 false 停止錯誤傳播
return false
})
</script>
<template>
<div v-if="error" class="error">
{{ error }}
</div>
<slot v-else />
</template>3. Async/Await 錯誤處理
<script setup>
import { ref } from 'vue'
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e.message
console.error('Fetch error:', e)
} finally {
loading.value = false
}
}
</script>TypeScript Integration
1. 為 Props 定義型別
<script setup lang="ts">
interface Props {
title: string
count?: number
items: Array<{
id: number
name: string
}>
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
</script>2. 為 Emits 定義型別
<script setup lang="ts">
interface Emits {
(e: 'update', id: number, value: string): void
(e: 'delete', id: number): void
}
const emit = defineEmits<Emits>()
emit('update', 1, 'new value')
</script>3. 為 Composables 定義型別
// composables/useFetch.ts
import { ref, Ref } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
loading: Ref<boolean>
fetchData: () => Promise<void>
}
export function useFetch<T>(url: string): UseFetchReturn<T> {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return { data, error, loading, fetchData }
}Testing Considerations
1. 可測試的元件設計
<script setup>
import { ref, computed } from 'vue'
// ✅ 推薦 - 邏輯抽離到 composable
import { useCounter } from '@/composables/useCounter'
const { count, increment } = useCounter()
// ✅ Props 和 emits 明確定義
const props = defineProps({
initialValue: {
type: Number,
default: 0
}
})
const emit = defineEmits(['change'])
</script>
<!-- ✅ 簡單的模板邏輯 -->
<template>
<div>
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</template>2. 使用依賴注入方便測試
// ✅ 使用 provide/inject
// app.js
app.provide('api', apiService)
// component.vue
const api = inject('api')
// 測試時可以輕鬆 mockVue 3 Common Patterns
Form Handling Patterns
1. 基本表單處理
<script setup>
import { reactive, ref } from 'vue'
// 使用 reactive 處理表單資料
const formData = reactive({
username: '',
email: '',
password: '',
agreeToTerms: false
})
// 或使用多個 ref
const username = ref('')
const email = ref('')
const password = ref('')
const errors = ref({})
const loading = ref(false)
const validateForm = () => {
errors.value = {}
if (!formData.username) {
errors.value.username = '請輸入使用者名稱'
}
if (!formData.email.includes('@')) {
errors.value.email = '請輸入有效的 Email'
}
return Object.keys(errors.value).length === 0
}
const handleSubmit = async () => {
if (!validateForm()) return
loading.value = true
try {
await submitForm(formData)
// 成功處理
} catch (error) {
// 錯誤處理
} finally {
loading.value = false
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div>
<input v-model="formData.username" type="text" />
<span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>
<div>
<input v-model="formData.email" type="email" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<button type="submit" :disabled="loading">
{{ loading ? '送出中...' : '送出' }}
</button>
</form>
</template>2. 使用 VeeValidate (推薦)
<script setup>
import { useForm, useField } from 'vee-validate'
import * as yup from 'yup'
const schema = yup.object({
username: yup.string().required('請輸入使用者名稱'),
email: yup.string().email('請輸入有效的 Email').required('請輸入 Email'),
password: yup.string().min(8, '密碼至少 8 個字元').required('請輸入密碼')
})
const { handleSubmit, errors } = useForm({
validationSchema: schema
})
const { value: username } = useField('username')
const { value: email } = useField('email')
const { value: password } = useField('password')
const onSubmit = handleSubmit(async (values) => {
console.log('Form values:', values)
await submitForm(values)
})
</script>
<template>
<form @submit="onSubmit">
<div>
<input v-model="username" type="text" />
<span class="error">{{ errors.username }}</span>
</div>
<div>
<input v-model="email" type="email" />
<span class="error">{{ errors.email }}</span>
</div>
<button type="submit">送出</button>
</form>
</template>Data Fetching Patterns
1. 使用 Composable 封裝
// composables/useApi.js
import { ref } from 'vue'
export function useApi() {
const loading = ref(false)
const error = ref(null)
const execute = async (apiCall) => {
loading.value = true
error.value = null
try {
const result = await apiCall()
return result
} catch (e) {
error.value = e
throw e
} finally {
loading.value = false
}
}
return {
loading,
error,
execute
}
}
// 在元件中使用
<script setup>
import { ref } from 'vue'
import { useApi } from '@/composables/useApi'
const { loading, error, execute } = useApi()
const users = ref([])
const fetchUsers = async () => {
users.value = await execute(() => fetch('/api/users').then(r => r.json()))
}
onMounted(fetchUsers)
</script>2. VueUse useFetch (推薦)
<script setup>
import { useFetch } from '@vueuse/core'
const { data, error, isFetching } = useFetch('/api/users').json()
// 延遲執行
const { data, execute } = useFetch('/api/users', { immediate: false }).json()
// 帶 refetch
const { data, refetch } = useFetch('/api/users').json()
</script>
<template>
<div v-if="isFetching">載入中...</div>
<div v-else-if="error">錯誤: {{ error }}</div>
<div v-else>
<div v-for="user in data" :key="user.id">
{{ user.name }}
</div>
</div>
</template>3. TanStack Query (Vue Query)
<script setup>
import { useQuery, useMutation } from '@tanstack/vue-query'
// 查詢
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json())
})
// 變更
const mutation = useMutation({
mutationFn: (newUser) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser)
}),
onSuccess: () => {
refetch()
}
})
const addUser = () => {
mutation.mutate({ name: 'New User' })
}
</script>List Rendering Patterns
1. 帶分頁的列表
<script setup>
import { ref, computed } from 'vue'
const items = ref([/* 大量資料 */])
const currentPage = ref(1)
const pageSize = ref(10)
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return items.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(items.value.length / pageSize.value)
})
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
</script>
<template>
<div>
<div v-for="item in paginatedItems" :key="item.id">
{{ item.name }}
</div>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一頁</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button @click="nextPage" :disabled="currentPage === totalPages">下一頁</button>
</div>
</div>
</template>2. 無限滾動
<script setup>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
const items = ref([])
const page = ref(1)
const loading = ref(false)
const hasMore = ref(true)
const loadMore = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
const newItems = await fetchItems(page.value)
if (newItems.length === 0) {
hasMore.value = false
} else {
items.value.push(...newItems)
page.value++
}
loading.value = false
}
const target = ref(null)
useIntersectionObserver(
target,
([{ isIntersecting }]) => {
if (isIntersecting) {
loadMore()
}
}
)
</script>
<template>
<div>
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<div ref="target" v-if="hasMore">
載入更多...
</div>
</div>
</template>3. 搜尋和過濾
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: 'Apple', category: 'fruit' },
{ id: 2, name: 'Banana', category: 'fruit' },
{ id: 3, name: 'Carrot', category: 'vegetable' }
])
const searchQuery = ref('')
const selectedCategory = ref(null)
const filteredItems = computed(() => {
return items.value.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesCategory = !selectedCategory.value || item.category === selectedCategory.value
return matchesSearch && matchesCategory
})
})
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="搜尋..." />
<select v-model="selectedCategory">
<option :value="null">全部分類</option>
<option value="fruit">水果</option>
<option value="vegetable">蔬菜</option>
</select>
<div v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</div>
</div>
</template>Modal/Dialog Patterns
1. 基本 Modal
<!-- Modal.vue -->
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
modelValue: Boolean,
title: String
})
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
const close = () => {
emit('update:modelValue', false)
}
const confirm = () => {
emit('confirm')
close()
}
// 按 Esc 關閉
const handleKeydown = (e) => {
if (e.key === 'Escape') close()
}
watch(() => props.modelValue, (isOpen) => {
if (isOpen) {
document.addEventListener('keydown', handleKeydown)
} else {
document.removeEventListener('keydown', handleKeydown)
}
})
</script>
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="modelValue" class="modal-overlay" @click.self="close">
<div class="modal-content">
<div class="modal-header">
<h3>{{ title }}</h3>
<button @click="close">×</button>
</div>
<div class="modal-body">
<slot />
</div>
<div class="modal-footer">
<button @click="close">取消</button>
<button @click="confirm">確認</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
</style>
<!-- 使用 -->
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const showModal = ref(false)
const handleConfirm = () => {
console.log('Confirmed!')
}
</script>
<template>
<button @click="showModal = true">開啟 Modal</button>
<Modal v-model="showModal" title="確認操作" @confirm="handleConfirm">
<p>你確定要執行此操作嗎?</p>
</Modal>
</template>2. 程式化 Modal (useModal)
// composables/useModal.js
import { ref, h, render } from 'vue'
import Modal from '@/components/Modal.vue'
export function useModal() {
const open = (options) => {
return new Promise((resolve) => {
const container = document.createElement('div')
document.body.appendChild(container)
const close = (result) => {
render(null, container)
document.body.removeChild(container)
resolve(result)
}
const vnode = h(Modal, {
...options,
modelValue: true,
onConfirm: () => close(true),
onCancel: () => close(false),
'onUpdate:modelValue': (val) => {
if (!val) close(false)
}
})
render(vnode, container)
})
}
return { open }
}
// 使用
<script setup>
import { useModal } from '@/composables/useModal'
const modal = useModal()
const handleDelete = async () => {
const confirmed = await modal.open({
title: '確認刪除',
content: '確定要刪除此項目嗎?'
})
if (confirmed) {
// 執行刪除
}
}
</script>State Management Patterns (Pinia)
1. 基本 Store
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const user = ref(null)
const token = ref(localStorage.getItem('token'))
// Getters
const isLoggedIn = computed(() => !!user.value)
const userName = computed(() => user.value?.name || 'Guest')
// Actions
const login = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const data = await response.json()
user.value = data.user
token.value = data.token
localStorage.setItem('token', data.token)
}
const logout = () => {
user.value = null
token.value = null
localStorage.removeItem('token')
}
return {
user,
token,
isLoggedIn,
userName,
login,
logout
}
})
// 在元件中使用
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const handleLogin = async () => {
await userStore.login({ email, password })
}
</script>2. Store 組合
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
const items = ref([])
const addItem = (item) => {
if (!userStore.isLoggedIn) {
alert('請先登入')
return
}
items.value.push(item)
}
return { items, addItem }
})Routing Patterns
1. 路由守衛
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { requiresGuest: true }
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next('/login')
} else if (to.meta.requiresGuest && userStore.isLoggedIn) {
next('/dashboard')
} else {
next()
}
})
export default router2. 巢狀路由和版面配置
// router/index.js
const routes = [
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: 'about',
name: 'About',
component: () => import('@/views/About.vue')
}
]
},
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'users',
component: () => import('@/views/admin/Users.vue')
}
]
}
]
// layouts/DefaultLayout.vue
<template>
<div>
<Header />
<router-view />
<Footer />
</div>
</template>i18n Patterns
1. Vue I18n 設定
// i18n/index.js
import { createI18n } from 'vue-i18n'
import zh from './locales/zh-TW.json'
import en from './locales/en.json'
const i18n = createI18n({
legacy: false,
locale: localStorage.getItem('locale') || 'zh-TW',
fallbackLocale: 'en',
messages: {
'zh-TW': zh,
'en': en
}
})
export default i18n
// locales/zh-TW.json
{
"welcome": "歡迎",
"greeting": "你好, {name}",
"items": "沒有項目 | 1 個項目 | {count} 個項目"
}
// 在元件中使用
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const changeLanguage = (lang) => {
locale.value = lang
localStorage.setItem('locale', lang)
}
</script>
<template>
<div>
<h1>{{ t('welcome') }}</h1>
<p>{{ t('greeting', { name: 'EVA' }) }}</p>
<p>{{ t('items', 5) }}</p>
<button @click="changeLanguage('zh-TW')">中文</button>
<button @click="changeLanguage('en')">English</button>
</div>
</template>Debounce/Throttle Patterns
<script setup>
import { ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'
const searchQuery = ref('')
const results = ref([])
// Debounce - 延遲執行,適合搜尋輸入
const debouncedSearch = useDebounceFn(async (query) => {
results.value = await searchApi(query)
}, 500)
// Throttle - 限制執行頻率,適合滾動事件
const throttledScroll = useThrottleFn(() => {
console.log('Scrolling...')
}, 200)
</script>
<template>
<input
v-model="searchQuery"
@input="debouncedSearch(searchQuery)"
placeholder="搜尋..."
/>
<div @scroll="throttledScroll">
<!-- 滾動內容 -->
</div>
</template>Vue 3 Composition API Reference
Reactivity APIs
ref()
為基本型別建立響應式引用。
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello')
// 讀取值
console.log(count.value) // 0
// 更新值
count.value++
// 在 template 中會自動解包,不需要 .value使用時機: 基本型別 (string, number, boolean) 或需要重新賦值的物件。
reactive()
為物件建立深層響應式代理。
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'EVA',
role: 'Frontend Engineer'
}
})
// 直接存取和修改
state.count++
state.user.name = 'Eva Chen'
// ❌ 不能重新賦值,會失去響應性
state = { count: 1 } // 錯誤!
// ✅ 應該修改屬性
Object.assign(state, { count: 1 })使用時機: 複雜物件結構,不需要重新賦值的情況。
computed()
建立計算屬性。
import { ref, computed } from 'vue'
const count = ref(0)
// 唯讀 computed
const doubleCount = computed(() => count.value * 2)
// 可寫 computed
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})watch()
觀察響應式資料變化。
import { ref, watch } from 'vue'
const count = ref(0)
// 觀察單一源
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
// 觀察多個源
watch([count, message], ([newCount, newMsg], [oldCount, oldMsg]) => {
console.log('Multiple sources changed')
})
// 深度觀察
const state = reactive({ nested: { count: 0 } })
watch(
() => state.nested,
(newValue) => {
console.log('Nested changed')
},
{ deep: true }
)
// 立即執行
watch(
source,
callback,
{ immediate: true }
)watchEffect()
自動追蹤依賴並執行副作用。
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
watchEffect(() => {
// 自動追蹤 count 和 message
console.log(`Count: ${count.value}, Message: ${message.value}`)
})
// 清理副作用
watchEffect((onCleanup) => {
const timer = setTimeout(() => {}, 1000)
onCleanup(() => {
clearTimeout(timer)
})
})watch vs watchEffect:
watch: 需明確指定要觀察的資料,可訪問舊值watchEffect: 自動追蹤依賴,立即執行,無法訪問舊值
Lifecycle Hooks
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
export default {
setup() {
// setup() 本身就相當於 beforeCreate 和 created
onBeforeMount(() => {
console.log('Component is about to mount')
})
onMounted(() => {
console.log('Component mounted')
// DOM 已經可用
})
onBeforeUpdate(() => {
console.log('Component is about to update')
})
onUpdated(() => {
console.log('Component updated')
})
onBeforeUnmount(() => {
console.log('Component is about to unmount')
// 清理工作
})
onUnmounted(() => {
console.log('Component unmounted')
})
// Keep-alive 元件專用
onActivated(() => {
console.log('Component activated')
})
onDeactivated(() => {
console.log('Component deactivated')
})
// 錯誤捕獲
onErrorCaptured((err, instance, info) => {
console.error('Error captured:', err)
return false // 停止錯誤傳播
})
}
}Component APIs
defineProps()
定義元件 props (僅在 <script setup> 中可用)。
<script setup>
// 基本用法
const props = defineProps(['title', 'likes'])
// 型別定義 (TypeScript)
const props = defineProps<{
title: string
likes: number
}>()
// 帶預設值 (TypeScript)
interface Props {
title?: string
likes?: number
}
const props = withDefaults(defineProps<Props>(), {
title: 'Default Title',
likes: 0
})
// 執行時驗證
const props = defineProps({
title: String,
likes: {
type: Number,
required: true,
default: 0,
validator: (value) => value >= 0
},
status: {
type: String,
enum: ['draft', 'published']
}
})
</script>defineEmits()
定義元件事件 (僅在 <script setup> 中可用)。
<script setup>
// 基本用法
const emit = defineEmits(['update', 'delete'])
// 型別定義 (TypeScript)
const emit = defineEmits<{
update: [id: number, value: string]
delete: [id: number]
}>()
// 執行時驗證
const emit = defineEmits({
update: (id, value) => {
if (typeof id !== 'number') {
console.warn('Invalid id type')
return false
}
return true
}
})
// 使用
emit('update', 1, 'new value')
</script>defineExpose()
暴露元件內部方法給父元件 (僅在 <script setup> 中可用)。
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 只暴露這些給父元件
defineExpose({
count,
increment
})
</script>useSlots() & useAttrs()
訪問 slots 和 attrs。
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
// 檢查 slot 是否存在
const hasDefaultSlot = !!slots.default
const hasHeaderSlot = !!slots.header
// 訪問屬性
console.log(attrs.class)
console.log(attrs.style)
</script>Dependency Injection
provide() / inject()
跨層級元件通訊。
// 祖先元件
<script setup>
import { provide, ref } from 'vue'
const theme = ref('light')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
// 提供響應式資料
provide('theme', theme)
provide('updateTheme', updateTheme)
// 使用 Symbol 避免衝突
export const ThemeSymbol = Symbol()
provide(ThemeSymbol, theme)
</script>
// 後代元件
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const updateTheme = inject('updateTheme')
// 提供預設值
const theme = inject('theme', 'light')
// 使用 Symbol
import { ThemeSymbol } from './parent'
const theme = inject(ThemeSymbol)
</script>Template Refs
<script setup>
import { ref, onMounted } from 'vue'
// 單一元素 ref
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
// 元件 ref (需要 defineExpose)
const childRef = ref(null)
onMounted(() => {
childRef.value.someMethod()
})
// v-for 中的 ref
const itemRefs = ref([])
onMounted(() => {
itemRefs.value.forEach(el => {
console.log(el)
})
})
</script>
<template>
<input ref="inputRef" />
<ChildComponent ref="childRef" />
<div v-for="item in items" :ref="el => itemRefs.push(el)">
{{ item }}
</div>
</template>Composables (可重用邏輯)
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
return {
count,
doubleCount,
increment,
decrement
}
}
// 在元件中使用
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubleCount, increment, decrement } = useCounter(10)
</script>Advanced Reactivity APIs
toRef() / toRefs()
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({
name: 'EVA',
age: 25
})
// 為單一屬性建立 ref
const nameRef = toRef(state, 'name')
nameRef.value = 'Eva Chen' // state.name 也會更新
// 為所有屬性建立 ref
const { name, age } = toRefs(state)
name.value = 'Eva Chen' // state.name 也會更新unref()
import { ref, unref } from 'vue'
const count = ref(10)
unref(count) // 10
unref(5) // 5 (如果不是 ref,返回原值)isRef() / isReactive() / isReadonly()
import { ref, reactive, readonly, isRef, isReactive, isReadonly } from 'vue'
const count = ref(0)
const state = reactive({})
const readonlyState = readonly(state)
isRef(count) // true
isReactive(state) // true
isReadonly(readonlyState) // trueshallowRef() / shallowReactive()
淺層響應式,只有根層級是響應式的。
import { shallowRef, shallowReactive } from 'vue'
// 淺層 ref - 只有 .value 的變化會觸發更新
const state = shallowRef({ count: 0 })
state.value.count++ // 不會觸發更新
state.value = { count: 1 } // 會觸發更新
// 淺層 reactive - 只有根層級屬性的變化會觸發更新
const state = shallowReactive({
nested: { count: 0 }
})
state.nested.count++ // 不會觸發更新
state.nested = { count: 1 } // 會觸發更新使用時機: 效能最佳化,大型不可變資料結構。
Vue 2 to Vue 3 Migration Guide
Critical Breaking Changes
1. Global API Changes
Vue 2:
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
Vue.use(VueRouter)
Vue.component('MyComponent', MyComponent)
new Vue({
render: h => h(App)
}).$mount('#app')Vue 3:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(router)
app.component('MyComponent', MyComponent)
app.mount('#app')2. Reactivity System
Vue 2:
export default {
data() {
return {
count: 0,
user: { name: 'EVA' }
}
},
methods: {
addProperty() {
// ❌ 需要 Vue.set
this.$set(this.user, 'age', 25)
}
}
}Vue 3:
import { reactive, ref } from 'vue'
export default {
setup() {
const count = ref(0)
const user = reactive({ name: 'EVA' })
const addProperty = () => {
// ✅ 直接新增屬性
user.age = 25
}
return { count, user, addProperty }
}
}3. v-model Changes
Vue 2:
<!-- 父元件 -->
<MyInput v-model="text" />
<!-- 子元件 -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value']
}
</script>Vue 3:
<!-- 父元件 -->
<MyInput v-model="text" />
<!-- 子元件 -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>4. Multiple v-model Support
Vue 3 新功能:
<!-- 父元件 -->
<UserForm v-model:name="userName" v-model:email="userEmail" />
<!-- 子元件 -->
<script setup>
defineProps(['name', 'email'])
const emit = defineEmits(['update:name', 'update:email'])
</script>
<template>
<input :value="name" @input="emit('update:name', $event.target.value)" />
<input :value="email" @input="emit('update:email', $event.target.value)" />
</template>5. Filters Removed
Vue 2:
<template>
<div>{{ price | currency }}</div>
</template>
<script>
export default {
filters: {
currency(value) {
return '$' + value.toFixed(2)
}
}
}
</script>Vue 3 (使用 computed 或 methods):
<template>
<div>{{ formatCurrency(price) }}</div>
</template>
<script setup>
const formatCurrency = (value) => {
return '$' + value.toFixed(2)
}
</script>6. Event Bus Removed
Vue 2:
// event-bus.js
export const EventBus = new Vue()
// ComponentA.vue
EventBus.$emit('event-name', data)
// ComponentB.vue
EventBus.$on('event-name', (data) => {})Vue 3 (使用 mitt 或 Pinia):
// Using mitt
import mitt from 'mitt'
export const emitter = mitt()
// ComponentA.vue
emitter.emit('event-name', data)
// ComponentB.vue
emitter.on('event-name', (data) => {})
// Or use Pinia for state management7. Async Component Syntax
Vue 2:
const AsyncComponent = () => import('./AsyncComponent.vue')Vue 3:
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)8. Functional Components
Vue 2:
<template functional>
<div>{{ props.msg }}</div>
</template>
<script>
export default {
functional: true,
props: ['msg']
}
</script>Vue 3:
<script setup>
defineProps(['msg'])
</script>
<template>
<div>{{ msg }}</div>
</template>9. Lifecycle Hooks in Composition API
Vue 2 Options API → Vue 3 Composition API:
// Vue 2
export default {
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {}
}
// Vue 3 Composition API
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
setup() {
// beforeCreate & created 的邏輯直接放在 setup() 中
onBeforeMount(() => {})
onMounted(() => {})
onBeforeUpdate(() => {})
onUpdated(() => {})
onBeforeUnmount(() => {})
onUnmounted(() => {})
}10. $attrs Behavior Change
Vue 2:
// class 和 style 不包含在 $attrs 中
this.$attrs // { id: 'foo' }Vue 3:
// class 和 style 也包含在 $attrs 中
this.$attrs // { class: 'bar', style: {...}, id: 'foo' }11. Key Usage Change
Vue 2:
<template v-for="item in items">
<div :key="item.id">{{ item.name }}</div>
<span :key="item.id">{{ item.desc }}</span>
</template>Vue 3:
<!-- key 應該放在 template 上 -->
<template v-for="item in items" :key="item.id">
<div>{{ item.name }}</div>
<span>{{ item.desc }}</span>
</template>Migration Checklist
- [ ] 更新 Vue 版本到 3.x
- [ ] 將
new Vue()改為createApp() - [ ] 更新全域 API 呼叫 (Vue.use → app.use)
- [ ] 移除 filters,改用 computed 或 methods
- [ ] 移除 Event Bus,改用 mitt 或 Pinia
- [ ] 更新 v-model 用法 (value → modelValue, input → update:modelValue)
- [ ] 檢查並更新生命週期 hook 名稱
- [ ] 更新 functional components
- [ ] 檢查 $attrs 的使用
- [ ] 檢查 template 上的 key 使用
- [ ] 更新 async components
- [ ] 測試響應式系統的行為變化
Common Migration Patterns
Pattern 1: Options API to Composition API
Before:
<script>
export default {
data() {
return {
count: 0,
loading: false
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
this.fetchData()
}
}
</script>After:
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const loading = ref(false)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
onMounted(() => {
fetchData()
})
</script>Pattern 2: Vuex to Pinia
Vuex (Vue 2):
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: null
},
mutations: {
SET_USER(state, user) {
state.user = user
}
},
actions: {
async login({ commit }, credentials) {
const user = await api.login(credentials)
commit('SET_USER', user)
}
},
getters: {
isLoggedIn: state => !!state.user
}
})Pinia (Vue 3):
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null
}),
getters: {
isLoggedIn: (state) => !!state.user
},
actions: {
async login(credentials) {
this.user = await api.login(credentials)
}
}
})