assets/component-templates/BasicComponent.vue
<script setup>
import { ref, computed, onMounted } from 'vue'
// Props
const props = defineProps({
// 定義你的 props
title: {
type: String,
default: ''
}
})
// Emits
const emit = defineEmits(['update', 'change'])
// State
const isActive = ref(false)
// Computed
const displayText = computed(() => {
return props.title.toUpperCase()
})
// Methods
const handleClick = () => {
isActive.value = !isActive.value
emit('change', isActive.value)
}
// Lifecycle
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div class="component">
<h2>{{ displayText }}</h2>
<button @click="handleClick">
{{ isActive ? '啟用' : '停用' }}
</button>
</div>
</template>
<style scoped>
.component {
/* 你的樣式 */
}
</style>assets/component-templates/DataTable.vue
<script setup>
import { ref, computed } from 'vue'
// Props
const props = defineProps({
data: {
type: Array,
required: true
},
columns: {
type: Array,
required: true
// 格式: [{ key: 'name', label: '姓名', sortable: true }, ...]
},
itemsPerPage: {
type: Number,
default: 10
}
})
// Emits
const emit = defineEmits(['row-click', 'edit', 'delete'])
// State
const currentPage = ref(1)
const sortKey = ref('')
const sortOrder = ref('asc') // 'asc' or 'desc'
const searchQuery = ref('')
// Computed
const filteredData = computed(() => {
if (!searchQuery.value) return props.data
return props.data.filter(item => {
return Object.values(item).some(value =>
String(value).toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
})
const sortedData = computed(() => {
if (!sortKey.value) return filteredData.value
return [...filteredData.value].sort((a, b) => {
const aVal = a[sortKey.value]
const bVal = b[sortKey.value]
let result = 0
if (aVal < bVal) result = -1
if (aVal > bVal) result = 1
return sortOrder.value === 'asc' ? result : -result
})
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * props.itemsPerPage
const end = start + props.itemsPerPage
return sortedData.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(sortedData.value.length / props.itemsPerPage)
})
const pageNumbers = computed(() => {
const pages = []
const maxVisible = 5
let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
let end = Math.min(totalPages.value, start + maxVisible - 1)
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1)
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
})
// Methods
const handleSort = (key) => {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortKey.value = key
sortOrder.value = 'asc'
}
}
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const handleRowClick = (item) => {
emit('row-click', item)
}
const handleEdit = (item) => {
emit('edit', item)
}
const handleDelete = (item) => {
emit('delete', item)
}
</script>
<template>
<div class="data-table">
<!-- 搜尋列 -->
<div class="table-controls">
<input
v-model="searchQuery"
type="text"
placeholder="搜尋..."
class="search-input"
/>
</div>
<!-- 表格 -->
<div class="table-wrapper">
<table>
<thead>
<tr>
<th
v-for="column in columns"
:key="column.key"
:class="{ 'sortable': column.sortable }"
@click="column.sortable && handleSort(column.key)"
>
{{ column.label }}
<span v-if="column.sortable && sortKey === column.key" class="sort-icon">
{{ sortOrder === 'asc' ? '▲' : '▼' }}
</span>
</th>
<th v-if="$slots.actions">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in paginatedData"
:key="item.id"
@click="handleRowClick(item)"
class="table-row"
>
<td v-for="column in columns" :key="column.key">
<slot :name="`cell-${column.key}`" :item="item">
{{ item[column.key] }}
</slot>
</td>
<td v-if="$slots.actions" class="actions-cell">
<slot name="actions" :item="item" :edit="handleEdit" :delete="handleDelete">
<button @click.stop="handleEdit(item)" class="btn-edit">編輯</button>
<button @click.stop="handleDelete(item)" class="btn-delete">刪除</button>
</slot>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分頁 -->
<div v-if="totalPages > 1" class="pagination">
<button
@click="goToPage(1)"
:disabled="currentPage === 1"
class="page-btn"
>
«
</button>
<button
@click="goToPage(currentPage - 1)"
:disabled="currentPage === 1"
class="page-btn"
>
‹
</button>
<button
v-for="page in pageNumbers"
:key="page"
@click="goToPage(page)"
:class="{ 'active': currentPage === page }"
class="page-btn"
>
{{ page }}
</button>
<button
@click="goToPage(currentPage + 1)"
:disabled="currentPage === totalPages"
class="page-btn"
>
›
</button>
<button
@click="goToPage(totalPages)"
:disabled="currentPage === totalPages"
class="page-btn"
>
»
</button>
<span class="page-info">
第 {{ currentPage }} 頁,共 {{ totalPages }} 頁
</span>
</div>
</div>
</template>
<style scoped>
.data-table {
width: 100%;
}
.table-controls {
margin-bottom: 1rem;
}
.search-input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
width: 300px;
}
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f8f9fa;
font-weight: 600;
user-select: none;
}
th.sortable {
cursor: pointer;
transition: background-color 0.2s;
}
th.sortable:hover {
background-color: #e9ecef;
}
.sort-icon {
margin-left: 0.5rem;
font-size: 0.75rem;
}
.table-row {
cursor: pointer;
transition: background-color 0.2s;
}
.table-row:hover {
background-color: #f8f9fa;
}
.actions-cell {
white-space: nowrap;
}
.btn-edit,
.btn-delete {
padding: 0.25rem 0.5rem;
margin-right: 0.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.btn-edit {
background-color: #3498db;
color: white;
}
.btn-delete {
background-color: #e74c3c;
color: white;
}
.pagination {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
}
.page-btn {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.page-btn:hover:not(:disabled) {
background-color: #f8f9fa;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}
.page-info {
margin-left: auto;
font-size: 0.875rem;
color: #666;
}
</style>assets/component-templates/Modal.vue
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
width: {
type: String,
default: '500px'
},
closable: {
type: Boolean,
default: true
},
closeOnClickOutside: {
type: Boolean,
default: true
},
closeOnEsc: {
type: Boolean,
default: true
},
showFooter: {
type: Boolean,
default: true
},
confirmText: {
type: String,
default: '確認'
},
cancelText: {
type: String,
default: '取消'
},
loading: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits([
'update:modelValue',
'confirm',
'cancel',
'opened',
'closed'
])
// Methods
const close = () => {
if (props.closable && !props.loading) {
emit('update:modelValue', false)
emit('closed')
}
}
const handleConfirm = () => {
if (!props.loading) {
emit('confirm')
}
}
const handleCancel = () => {
if (!props.loading) {
emit('cancel')
close()
}
}
const handleOverlayClick = () => {
if (props.closeOnClickOutside) {
close()
}
}
const handleKeydown = (e) => {
if (e.key === 'Escape' && props.closeOnEsc) {
close()
}
}
// Watch for modal open/close
watch(() => props.modelValue, (isOpen) => {
if (isOpen) {
document.body.style.overflow = 'hidden'
document.addEventListener('keydown', handleKeydown)
emit('opened')
} else {
document.body.style.overflow = ''
document.removeEventListener('keydown', handleKeydown)
}
})
// Cleanup on unmount
onBeforeUnmount(() => {
document.body.style.overflow = ''
document.removeEventListener('keydown', handleKeydown)
})
</script>
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="modelValue" class="modal-overlay" @click="handleOverlayClick">
<div
class="modal-container"
:style="{ width }"
@click.stop
>
<!-- Header -->
<div v-if="title || closable || $slots.header" class="modal-header">
<slot name="header">
<h3 class="modal-title">{{ title }}</h3>
</slot>
<button
v-if="closable"
@click="close"
:disabled="loading"
class="modal-close"
aria-label="關閉"
>
×
</button>
</div>
<!-- Body -->
<div class="modal-body">
<slot />
</div>
<!-- Footer -->
<div v-if="showFooter || $slots.footer" class="modal-footer">
<slot name="footer">
<button
@click="handleCancel"
:disabled="loading"
class="btn btn-cancel"
>
{{ cancelText }}
</button>
<button
@click="handleConfirm"
:disabled="loading"
class="btn btn-confirm"
>
<span v-if="loading" class="loading-spinner"></span>
{{ loading ? '處理中...' : confirmText }}
</button>
</slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
}
.modal-close {
background: none;
border: none;
font-size: 2rem;
line-height: 1;
color: #6b7280;
cursor: pointer;
padding: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.modal-close:hover:not(:disabled) {
background-color: #f3f4f6;
color: #1f2937;
}
.modal-close:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
flex: 1;
}
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
padding: 1.5rem;
border-top: 1px solid #e5e7eb;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-cancel {
background-color: white;
color: #374151;
border-color: #d1d5db;
}
.btn-cancel:hover:not(:disabled) {
background-color: #f9fafb;
}
.btn-confirm {
background-color: #3b82f6;
color: white;
}
.btn-confirm:hover:not(:disabled) {
background-color: #2563eb;
}
.loading-spinner {
width: 1rem;
height: 1rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Transitions */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-active .modal-container,
.modal-leave-active .modal-container {
transition: transform 0.3s ease;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
transform: scale(0.9);
}
</style>assets/composable-templates/useFatch.js
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
})
}assets/composable-templates/useLocalStorage.js
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
}references/ best-practices.md
# Vue 3 Best Practices
## Component Design
### 1. 使用 `<script setup>` 語法
更簡潔、效能更好、更好的 TypeScript 支援。
```vue
<!-- ✅ 推薦 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<!-- ❌ 避免 (除非必要) -->
<script>
export default {
setup() {
const count = ref(0)
return { count }
}
}
</script>
```
### 2. Props 定義要明確且嚴格
```vue
<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 封裝可重用邏輯
```javascript
// ✅ 推薦 - 建立可重用的 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. 適當的元件拆分
```vue
<!-- ❌ 避免 - 單一元件過於龐大 -->
<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
```javascript
// ✅ 基本型別使用 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 } // OK
```
### 2. 避免直接解構響應式物件
```javascript
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
```javascript
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
```vue
<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
```vue
<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
```vue
<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. 延遲載入大型元件
```javascript
// ✅ 使用 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 快取元件
```vue
<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. 虛擬滾動處理大列表
```vue
<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. 元件命名規範
```vue
<!-- ✅ 使用 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.vue
```
### 3. Composables 命名規範
```javascript
// ✅ 使用 use 前綴
export function useAuth() { }
export function useFetch() { }
export function useLocalStorage() { }
// ❌ 避免
export function auth() { }
export function fetch() { }
```
## Error Handling
### 1. 全域錯誤處理
```javascript
// 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. 元件內錯誤處理
```vue
<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 錯誤處理
```javascript
<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 定義型別
```vue
<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 定義型別
```vue
<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 定義型別
```typescript
// 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. 可測試的元件設計
```vue
<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. 使用依賴注入方便測試
```javascript
// ✅ 使用 provide/inject
// app.js
app.provide('api', apiService)
// component.vue
const api = inject('api')
// 測試時可以輕鬆 mock
```references/ common-patterns.md
# Vue 3 Common Patterns
## Form Handling Patterns
### 1. 基本表單處理
```vue
<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 (推薦)
```vue
<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 封裝
```javascript
// 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 (推薦)
```vue
<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)
```vue
<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. 帶分頁的列表
```vue
<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. 無限滾動
```vue
<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. 搜尋和過濾
```vue
<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
```vue
<!-- 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)
```javascript
// 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
```javascript
// 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 組合
```javascript
// 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. 路由守衛
```javascript
// 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 router
```
### 2. 巢狀路由和版面配置
```javascript
// 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 設定
```javascript
// 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
```vue
<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>
```references/composition-api.md
# Vue 3 Composition API Reference
## Reactivity APIs
### ref()
為基本型別建立響應式引用。
```javascript
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()
為物件建立深層響應式代理。
```javascript
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()
建立計算屬性。
```javascript
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()
觀察響應式資料變化。
```javascript
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()
自動追蹤依賴並執行副作用。
```javascript
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
```javascript
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>` 中可用)。
```javascript
<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>` 中可用)。
```javascript
<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>` 中可用)。
```javascript
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 只暴露這些給父元件
defineExpose({
count,
increment
})
</script>
```
### useSlots() & useAttrs()
訪問 slots 和 attrs。
```javascript
<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()
跨層級元件通訊。
```javascript
// 祖先元件
<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
```javascript
<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 (可重用邏輯)
```javascript
// 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()
```javascript
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()
```javascript
import { ref, unref } from 'vue'
const count = ref(10)
unref(count) // 10
unref(5) // 5 (如果不是 ref,返回原值)
```
### isRef() / isReactive() / isReadonly()
```javascript
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) // true
```
### shallowRef() / shallowReactive()
淺層響應式,只有根層級是響應式的。
```javascript
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 } // 會觸發更新
```
**使用時機:** 效能最佳化,大型不可變資料結構。references/migration-guide.md
# Vue 2 to Vue 3 Migration Guide
## Critical Breaking Changes
### 1. Global API Changes
**Vue 2:**
```javascript
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:**
```javascript
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:**
```javascript
export default {
data() {
return {
count: 0,
user: { name: 'EVA' }
}
},
methods: {
addProperty() {
// ❌ 需要 Vue.set
this.$set(this.user, 'age', 25)
}
}
}
```
**Vue 3:**
```javascript
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:**
```vue
<!-- 父元件 -->
<MyInput v-model="text" />
<!-- 子元件 -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value']
}
</script>
```
**Vue 3:**
```vue
<!-- 父元件 -->
<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 新功能:**
```vue
<!-- 父元件 -->
<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:**
```vue
<template>
<div>{{ price | currency }}</div>
</template>
<script>
export default {
filters: {
currency(value) {
return '$' + value.toFixed(2)
}
}
}
</script>
```
**Vue 3 (使用 computed 或 methods):**
```vue
<template>
<div>{{ formatCurrency(price) }}</div>
</template>
<script setup>
const formatCurrency = (value) => {
return '$' + value.toFixed(2)
}
</script>
```
### 6. Event Bus Removed
**Vue 2:**
```javascript
// 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):**
```javascript
// 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 management
```
### 7. Async Component Syntax
**Vue 2:**
```javascript
const AsyncComponent = () => import('./AsyncComponent.vue')
```
**Vue 3:**
```javascript
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
```
### 8. Functional Components
**Vue 2:**
```vue
<template functional>
<div>{{ props.msg }}</div>
</template>
<script>
export default {
functional: true,
props: ['msg']
}
</script>
```
**Vue 3:**
```vue
<script setup>
defineProps(['msg'])
</script>
<template>
<div>{{ msg }}</div>
</template>
```
### 9. Lifecycle Hooks in Composition API
**Vue 2 Options API → Vue 3 Composition API:**
```javascript
// 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:**
```javascript
// class 和 style 不包含在 $attrs 中
this.$attrs // { id: 'foo' }
```
**Vue 3:**
```javascript
// class 和 style 也包含在 $attrs 中
this.$attrs // { class: 'bar', style: {...}, id: 'foo' }
```
### 11. Key Usage Change
**Vue 2:**
```vue
<template v-for="item in items">
<div :key="item.id">{{ item.name }}</div>
<span :key="item.id">{{ item.desc }}</span>
</template>
```
**Vue 3:**
```vue
<!-- 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:**
```vue
<script>
export default {
data() {
return {
count: 0,
loading: false
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
this.fetchData()
}
}
</script>
```
**After:**
```vue
<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):**
```javascript
// 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):**
```javascript
// 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)
}
}
})
```skill.md
---
name: vue3-frontend
description: "Comprehensive Vue 3 frontend development skill. Use when developing Vue 3 applications, components, or composables. Covers: (1) Creating new Vue 3 projects and components, (2) Vue 2 to Vue 3 migration, (3) Code review and optimization, (4) Composition API implementation, (5) Best practices and common patterns, (6) TypeScript integration, (7) State management with Pinia, (8) Performance optimization. Includes component templates, composable examples, migration guides, and best practices documentation."
---
# 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:**
```bash
# 複製範本到專案
cp assets/component-templates/BasicComponent.vue src/components/YourComponent.vue
```
### 2. Composables Development
建立可重用的邏輯抽象。
**Available templates:**
- `assets/composable-templates/useFetch.js` - API 請求封裝
- `assets/composable-templates/useLocalStorage.js` - localStorage 同步狀態
**Usage:**
```bash
# 複製到專案
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.md` Form Handling section
- Data fetching → `common-patterns.md` Data Fetching section
- State management → `common-patterns.md` State Management section
- i18n → `common-patterns.md` i18n section
## Common Development Workflows
### Workflow 1: Create a New Component
```bash
# 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 small
```
### Workflow 2: Migrate Vue 2 Code to Vue 3
```bash
# 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 dependencies
```
### Workflow 3: Implement Data Fetching
```bash
# 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 states
```
### Workflow 4: Optimize Performance
```bash
# 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 lists
```
### Workflow 5: Form Development
```bash
# 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 feedback
```
## Quick Reference Commands
### Component Templates
```bash
# List all templates
ls -la assets/component-templates/
# Copy specific template
cp assets/component-templates/[TemplateName].vue src/components/
```
### Composable Templates
```bash
# List all composables
ls -la assets/composable-templates/
# Copy specific composable
cp assets/composable-templates/[composableName].js src/composables/
```
### Read Documentation
```bash
# 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.md
```
## Best 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-show` for frequent toggles
- Use `v-if` for initial render conditions
- Always use unique `key` in `v-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 services
```
## TypeScript Support
When using TypeScript:
- Define prop types with interfaces
- Use generics in composables
- Type emits properly
- Use `defineComponent` when 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 `.value` in `<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 use `watchEffect()` 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