0%

Vue 3 高级技巧与最佳实践:从入门到精通的完整指南

Vue 3 高级技巧与最佳实践:从入门到精通的完整指南

摘要:Vue 3 带来了 Composition API、Proxy 响应式、Teleport、Suspense 等强大特性。本文深入解析 Vue 3 高级技巧:从 Composition API 最佳实践、响应式原理深度理解、性能优化策略,到大型项目架构设计。包含 10+ 个实战案例、完整代码示例、常见陷阱规避,以及 CrystalForge 项目的真实应用经验。

关键词:Vue 3、Composition API、性能优化、最佳实践、前端架构、实战案例


一、Vue 3 核心特性回顾

1.1 Composition API vs Options API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
graph TB
subgraph "Options API (Vue 2)"
O1[data 分散]
O2[methods 分散]
O3[computed 分散]
O4[watch 分散]
O5[生命周期分散]
end

subgraph "Composition API (Vue 3)"
C1[setup() 集中管理]
C2[逻辑复用 Composables]
C3[更好的 TypeScript]
C4[更小的打包体积]
end

O1 -.->|重构 | C1
O2 -.->|重构 | C1
O3 -.->|重构 | C1
O4 -.->|重构 | C1
O5 -.->|重构 | C1

C1 --> C2
C1 --> C3
C1 --> C4

1.2 响应式系统升级

特性 Vue 2 Vue 3 提升
响应式原理 Object.defineProperty Proxy 全面
数组监听 ❌ 不支持 ✅ 支持 100%
对象新增属性 ❌ 需 Vue.set ✅ 直接支持 100%
性能提升 基准 2 倍 100%

二、Composition API 最佳实践

2.1 setup() 语法糖

传统写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
import { ref, computed, onMounted } from 'vue'

export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)

function increment() {
count.value++
}

onMounted(() => {
console.log('Mounted')
})

return {
count,
double,
increment
}
}
}
</script>

语法糖写法(推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)

function increment() {
count.value++
}

onMounted(() => {
console.log('Mounted')
})
</script>

优势

  • ✅ 代码减少 40%
  • ✅ 无需 return 暴露
  • ✅ 更好的 IDE 支持
  • ✅ TypeScript 类型推断更准确

2.2 逻辑复用 - Composables

2.2.1 使用 Mouse 跟踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
const x = ref(0)
const y = ref(0)

function update(event) {
x.value = event.pageX
y.value = event.pageY
}

onMounted(() => {
window.addEventListener('mousemove', update)
})

onUnmounted(() => {
window.removeEventListener('mousemove', update)
})

return { x, y }
}

使用

1
2
3
4
5
6
7
8
9
<script setup>
import { useMouse } from '@/composables/useMouse'

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

<template>
<div>Mouse position: {{ x }}, {{ y }}</div>
</template>

2.2.2 使用 Fetch 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// composables/useFetch.js
import { ref, watch } from 'vue'

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

const fetchData = async () => {
loading.value = true
error.value = null

try {
const response = await fetch(url, options)
if (!response.ok) throw new Error(response.statusText)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}

// 自动监听 URL 变化
watch(() => url, fetchData, { immediate: true })

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

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { useFetch } from '@/composables/useFetch'

const { data: crystals, loading, error, refetch } = useFetch(
'/api/crystals',
{ headers: { 'Authorization': `Bearer ${token}` } }
)
</script>

<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<CrystalList :crystals="crystals" />
<button @click="refetch">Refresh</button>
</div>
</template>

2.2.3 使用 Pagination

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// composables/usePagination.js
import { ref, computed } from 'vue'

export function usePagination(total, pageSize = 10) {
const currentPage = ref(1)

const totalPages = computed(() => Math.ceil(total / pageSize))

const startIndex = computed(() => (currentPage.value - 1) * pageSize)
const endIndex = computed(() => Math.min(startIndex.value + pageSize, total))

function goToPage(page) {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}

function nextPage() {
goToPage(currentPage.value + 1)
}

function prevPage() {
goToPage(currentPage.value - 1)
}

return {
currentPage,
totalPages,
startIndex,
endIndex,
goToPage,
nextPage,
prevPage
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script setup>
import { computed } from 'vue'
import { usePagination } from '@/composables/usePagination'

const props = defineProps({
items: { type: Array, required: true },
pageSize: { type: Number, default: 10 }
})

const {
currentPage,
totalPages,
startIndex,
endIndex,
goToPage,
nextPage,
prevPage
} = usePagination(props.items.length, props.pageSize)

const paginatedItems = computed(() => {
return props.items.slice(startIndex.value, endIndex.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">Previous</button>
<span>Page {{ currentPage }} of {{ totalPages }}</span>
<button @click="nextPage" :disabled="currentPage === totalPages">Next</button>
</div>
</div>
</template>

2.3 响应式深度理解

2.3.1 ref vs reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { ref, reactive, toRef, toRefs } from 'vue'

// ref - 用于基本类型
const count = ref(0)
const name = ref('John')

// reactive - 用于对象
const user = reactive({
name: 'John',
age: 30
})

// ❌ 错误:解构失去响应性
// const { name, age } = user

// ✅ 正确:使用 toRefs
const { name, age } = toRefs(user)

// ✅ 正确:使用 toRef(单个属性)
const nameRef = toRef(user, 'name')
</script>

选择指南

场景 推荐 原因
基本类型 ref 简单直接
对象/数组 reactive 自动解包
Props 解构 toRefs 保持响应性
可选属性 ref 更灵活

2.3.2 响应式陷阱

陷阱 #1:直接替换 reactive 对象

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { reactive } from 'vue'

const user = reactive({ name: 'John', age: 30 })

// ❌ 错误:失去响应性
user = reactive({ name: 'Jane', age: 25 })

// ✅ 正确:使用 Object.assign
Object.assign(user, { name: 'Jane', age: 25 })
</script>

陷阱 #2:数组索引修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { reactive } from 'vue'

const items = reactive([1, 2, 3])

// ✅ Vue 3 支持索引修改
items[0] = 100

// ✅ Vue 3 支持 length 修改
items.length = 2

// ✅ Vue 3 支持所有数组方法
items.push(4)
items.splice(1, 1)
</script>

陷阱 #3:watch 深度监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, watch } from 'vue'

const user = ref({ name: 'John', profile: { age: 30 } })

// ❌ 错误:只监听第一层
watch(user, (newVal) => {
console.log('Changed') // 不会触发
})

// ✅ 正确:深度监听
watch(user, (newVal) => {
console.log('Changed')
}, { deep: true })

// ✅ 更好:监听具体路径
watch(() => user.value.profile.age, (newAge) => {
console.log('Age changed:', newAge)
})
</script>

三、性能优化策略

3.1 组件懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { defineAsyncComponent } from 'vue'

// 基础懒加载
const ChartComponent = defineAsyncComponent(() =>
import('@/components/ChartComponent.vue')
)

// 带加载状态
const HeavyComponent = defineAsyncComponent({
loader: () => import('@/components/HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
</script>

3.2 v-show vs v-if

1
2
3
4
5
6
7
8
9
10
<template>
<!-- ✅ 频繁切换用 v-show -->
<div v-show="isVisible">Content</div>

<!-- ✅ 条件渲染用 v-if -->
<div v-if="hasPermission">Admin Panel</div>

<!-- ✅ 初始化不渲染用 v-if -->
<Modal v-if="showModal" />
</template>

性能对比

场景 v-show v-if 推荐
频繁切换 ⭐⭐⭐⭐⭐ ⭐⭐ v-show
初始化隐藏 ⭐⭐ ⭐⭐⭐⭐⭐ v-if
条件渲染 ⭐⭐⭐⭐⭐ v-if
性能开销 低(CSS) 高(DOM) -

3.3 列表优化

3.3.1 key 的正确使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- ❌ 错误:使用索引作为 key -->
<div v-for="(item, index) in items" :key="index">
{{ item.name }}
</div>

<!-- ✅ 正确:使用唯一 ID -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>

<!-- ✅ 无 key 场景(静态列表) -->
<div v-for="i in 10">
Item {{ i }}
</div>
</template>

3.3.2 虚拟滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
items: { type: Array, required: true },
itemHeight: { type: Number, default: 50 },
containerHeight: { type: Number, default: 600 }
})

const scrollTop = ref(0)

const visibleCount = computed(() => {
return Math.ceil(props.containerHeight / props.itemHeight)
})

const startIndex = computed(() => {
return Math.floor(scrollTop.value / props.itemHeight)
})

const endIndex = computed(() => {
return Math.min(startIndex.value + visibleCount.value, props.items.length)
})

const visibleItems = computed(() => {
return props.items.slice(startIndex.value, endIndex.value)
})

const totalHeight = computed(() => {
return props.items.length * props.itemHeight
})
</script>

<template>
<div
class="virtual-list"
:style="{ height: containerHeight + 'px', overflow: 'auto' }"
@scroll="e => scrollTop = e.target.scrollTop"
>
<div :style="{ height: totalHeight + 'px', position: 'relative' }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{
position: 'absolute',
top: (startIndex + items.indexOf(item)) * itemHeight + 'px',
height: itemHeight + 'px'
}"
>
{{ item.name }}
</div>
</div>
</div>
</template>

3.4 计算属性优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script setup>
import { ref, computed } from 'vue'

const items = ref([...]) // 10000 条数据
const filter = ref('')

// ❌ 错误:每次渲染都重新计算
const filteredItems = items.value.filter(item =>
item.name.includes(filter.value)
)

// ✅ 正确:使用 computed 缓存
const filteredItems = computed(() => {
console.log('Computing filtered items...') // 仅在依赖变化时执行
return items.value.filter(item =>
item.name.includes(filter.value)
)
})

// ✅ 更好:添加缓存提示
const expensiveComputed = computed({
get() {
// 耗时计算
return heavyComputation()
},
set(value) {
// 可选的 setter
}
})
</script>

3.5 事件优化

3.5.1 事件修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<!-- 阻止默认行为 -->
<a @click.prevent="handleClick">Link</a>

<!-- 阻止事件冒泡 -->
<button @click.stop="handleClick">Button</button>

<!-- 只触发一次 -->
<button @click.once="handleClick">Button</button>

<!-- 仅在元素本身触发 -->
<div @click.self="handleClick">
<span>Child</span>
</div>

<!-- 被动监听(滚动优化) -->
<div @scroll.passive="handleScroll">Content</div>

<!-- 捕获阶段监听 -->
<div @click.capture="handleClick">Content</div>
</template>

3.5.2 防抖与节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timer = null

watch(value, (newValue) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})

return debouncedValue
}

// composables/useThrottle.js
export function useThrottle(value, interval = 300) {
const throttledValue = ref(value.value)
let lastUpdate = 0

watch(value, (newValue) => {
const now = Date.now()
if (now - lastUpdate >= interval) {
throttledValue.value = newValue
lastUpdate = now
}
})

return throttledValue
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { ref } from 'vue'
import { useDebounce } from '@/composables/useDebounce'

const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)

// 仅在停止输入 500ms 后才搜索
watch(debouncedQuery, (query) => {
performSearch(query)
})
</script>

<template>
<input v-model="searchQuery" placeholder="Search..." />
</template>

四、实战案例

4.1 案例 #1:CrystalForge 晶体列表

需求

  • 支持搜索、筛选、排序
  • 分页显示(每页 20 条)
  • 懒加载图片
  • 性能要求:首屏 < 1s

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<script setup>
import { ref, computed, watch } from 'vue'
import { useFetch } from '@/composables/useFetch'
import { useDebounce } from '@/composables/useDebounce'
import { usePagination } from '@/composables/usePagination'

// 搜索
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 300)

// 筛选
const filters = ref({
category: '',
priceRange: [0, 10000],
status: ''
})

// 排序
const sortBy = ref('createdAt')
const sortOrder = ref('desc')

// 获取数据
const { data: crystals, loading, error } = useFetch(
computed(() => `/api/crystals?search=${debouncedQuery.value}&category=${filters.value.category}`)
)

// 筛选和排序
const filteredCrystals = computed(() => {
let result = crystals.value || []

// 价格筛选
result = result.filter(c =>
c.price >= filters.value.priceRange[0] &&
c.price <= filters.value.priceRange[1]
)

// 状态筛选
if (filters.value.status) {
result = result.filter(c => c.status === filters.value.status)
}

// 排序
result.sort((a, b) => {
const aVal = a[sortBy.value]
const bVal = b[sortBy.value]
return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
})

return result
})

// 分页
const {
currentPage,
totalPages,
paginatedItems
} = usePagination(filteredCrystals, 20)
</script>

<template>
<div>
<!-- 搜索框 -->
<input v-model="searchQuery" placeholder="搜索晶体..." />

<!-- 筛选器 -->
<FilterPanel v-model="filters" />

<!-- 排序 -->
<SortSelector v-model:sort-by="sortBy" v-model:order="sortOrder" />

<!-- 列表 -->
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<CrystalCard
v-for="crystal in paginatedItems"
:key="crystal.id"
:crystal="crystal"
/>

<!-- 分页 -->
<Pagination
v-model:page="currentPage"
:total="filteredCrystals.length"
:page-size="20"
/>
</div>
</div>
</template>

4.2 案例 #2:实时数据更新

需求

  • WebSocket 实时推送晶体价格
  • 自动更新 UI
  • 价格变化动画提示

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// composables/useWebSocket.js
import { ref, onMounted, onUnmounted } from 'vue'

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

let ws = null
let reconnectTimer = null

function connect() {
ws = new WebSocket(url)

ws.onopen = () => {
connected.value = true
error.value = null
}

ws.onmessage = (event) => {
data.value = JSON.parse(event.data)
options.onMessage?.(data.value)
}

ws.onclose = () => {
connected.value = false
// 自动重连
reconnectTimer = setTimeout(connect, 3000)
}

ws.onerror = (e) => {
error.value = e
}
}

onMounted(() => {
connect()
})

onUnmounted(() => {
if (reconnectTimer) clearTimeout(reconnectTimer)
if (ws) ws.close()
})

function send(message) {
if (ws && connected.value) {
ws.send(JSON.stringify(message))
}
}

return { data, connected, error, send }
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<script setup>
import { ref, watch } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'

const crystalPrices = ref({})
const priceChanges = ref({})

const { data: priceUpdate, connected } = useWebSocket(
'wss://api.crystalforge.com/prices',
{
onMessage: (data) => {
const oldPrice = crystalPrices.value[data.id]
crystalPrices.value[data.id] = data.price

// 记录价格变化
if (oldPrice !== undefined) {
priceChanges.value[data.id] = data.price - oldPrice

// 3 秒后清除变化标记
setTimeout(() => {
delete priceChanges.value[data.id]
}, 3000)
}
}
}
)
</script>

<template>
<div>
<div v-for="crystal in crystals" :key="crystal.id" class="crystal-item">
<span>{{ crystal.name }}</span>
<span
class="price"
:class="{
'price-up': priceChanges[crystal.id] > 0,
'price-down': priceChanges[crystal.id] < 0
}"
>
¥{{ crystalPrices[crystal.id] }}
</span>
</div>

<div v-if="!connected" class="connection-lost">
连接断开,正在重连...
</div>
</div>
</template>

<style scoped>
.price-up {
color: #f5222d;
animation: flash-up 0.5s;
}

.price-down {
color: #52c41a;
animation: flash-down 0.5s;
}

@keyframes flash-up {
0%, 100% { background: transparent; }
50% { background: rgba(245, 34, 45, 0.2); }
}

@keyframes flash-down {
0%, 100% { background: transparent; }
50% { background: rgba(82, 196, 26, 0.2); }
}
</style>

4.3 案例 #3:表单验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// composables/useForm.js
import { ref, reactive } from 'vue'

export function useForm(initialValues, validationRules) {
const values = reactive({ ...initialValues })
const errors = reactive({})
const touched = reactive({})
const submitting = ref(false)

function validateField(field) {
const rules = validationRules[field]
const value = values[field]

if (!rules) return true

for (const rule of rules) {
const result = rule(value, values)
if (result !== true) {
errors[field] = result
return false
}
}

errors[field] = ''
return true
}

function validateAll() {
let valid = true
for (const field of Object.keys(validationRules)) {
if (!validateField(field)) {
valid = false
}
}
return valid
}

function handleChange(field, value) {
values[field] = value
if (touched[field]) {
validateField(field)
}
}

function handleBlur(field) {
touched[field] = true
validateField(field)
}

async function handleSubmit(submitFn) {
if (!validateAll()) return

submitting.value = true
try {
await submitFn(values)
} finally {
submitting.value = false
}
}

function reset() {
Object.assign(values, initialValues)
Object.keys(errors).forEach(key => delete errors[key])
Object.keys(touched).forEach(key => delete touched[key])
}

return {
values,
errors,
touched,
submitting,
validateField,
validateAll,
handleChange,
handleBlur,
handleSubmit,
reset
}
}

// 验证规则
export const validators = {
required: (value) => !value ? '此项为必填项' : true,
email: (value) => {
if (!value) return true
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(value) ? true : '邮箱格式不正确'
},
minLength: (min) => (value) => {
if (!value) return true
return value.length >= min ? true : `长度至少为 ${min}`
},
maxLength: (max) => (value) => {
if (!value) return true
return value.length <= max ? true : `长度最多为 ${max}`
},
pattern: (regex, message) => (value) => {
if (!value) return true
return regex.test(value) ? true : message
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<script setup>
import { useForm, validators } from '@/composables/useForm'

const { values, errors, handleChange, handleBlur, handleSubmit, submitting } = useForm(
{
username: '',
email: '',
password: '',
confirmPassword: ''
},
{
username: [
validators.required,
validators.minLength(3),
validators.maxLength(20)
],
email: [
validators.required,
validators.email
],
password: [
validators.required,
validators.minLength(8)
],
confirmPassword: [
validators.required,
(value) => value === values.password ? true : '两次密码不一致'
]
}
)

async function onSubmit(values) {
await api.register(values)
}
</script>

<template>
<form @submit.prevent="handleSubmit(onSubmit)">
<div>
<label>用户名</label>
<input
:value="values.username"
@input="e => handleChange('username', e.target.value)"
@blur="() => handleBlur('username')"
/>
<span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>

<div>
<label>邮箱</label>
<input
type="email"
:value="values.email"
@input="e => handleChange('email', e.target.value)"
@blur="() => handleBlur('email')"
/>
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>

<div>
<label>密码</label>
<input
type="password"
:value="values.password"
@input="e => handleChange('password', e.target.value)"
@blur="() => handleBlur('password')"
/>
<span v-if="errors.password" class="error">{{ errors.password }}</span>
</div>

<div>
<label>确认密码</label>
<input
type="password"
:value="values.confirmPassword"
@input="e => handleChange('confirmPassword', e.target.value)"
@blur="() => handleBlur('confirmPassword')"
/>
<span v-if="errors.confirmPassword" class="error">{{ errors.confirmPassword }}</span>
</div>

<button type="submit" :disabled="submitting">
{{ submitting ? '提交中...' : '注册' }}
</button>
</form>
</template>

五、TypeScript 集成

5.1 Props 类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script setup lang="ts">
import { ref, computed } from 'vue'

// 方式 1:泛型定义
const props = defineProps<{
title: string
count?: number
items: Array<{
id: number
name: string
}>
}>()

// 方式 2:类型别名
interface CrystalProps {
crystalId: number
showDetails?: boolean
theme?: 'light' | 'dark'
}

const props = withDefaults(defineProps<CrystalProps>(), {
showDetails: false,
theme: 'light'
})

// 响应式数据
const count = ref<number>(0)
const items = ref<Array<string>>([])

// 计算属性
const doubled = computed<number>(() => count.value * 2)

// 事件
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'delete', id: number): void
}>()
</script>

5.2 Composables 类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// composables/useFetch.ts
import { ref, watch } from 'vue'

export interface UseFetchOptions {
method?: string
headers?: Record<string, string>
body?: any
immediate?: boolean
}

export interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
loading: Ref<boolean>
refetch: () => Promise<void>
}

export function useFetch<T>(
url: string | Ref<string>,
options: UseFetchOptions = {}
): UseFetchReturn<T> {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(true)

const fetchData = async () => {
loading.value = true
error.value = null

try {
const response = await fetch(
typeof url === 'string' ? url : url.value,
{
method: options.method || 'GET',
headers: options.headers,
body: options.body ? JSON.stringify(options.body) : undefined
}
)

if (!response.ok) throw new Error(response.statusText)
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}

if (options.immediate !== false) {
watch(() => typeof url === 'string' ? url : url.value, fetchData, { immediate: true })
}

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

六、常见陷阱与规避

6.1 陷阱 #1:this 指向问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref } from 'vue'

const count = ref(0)

// ❌ 错误:setup 中没有 this
// function increment() {
// this.count++
// }

// ✅ 正确:直接使用
function increment() {
count.value++
}
</script>

6.2 陷阱 #2:生命周期钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
onErrorCaptured
} from 'vue'

// ✅ 正确:使用 Composition API 生命周期
onMounted(() => {
console.log('Mounted')
})

// ❌ 错误:不能使用 Options API 生命周期
// export default {
// mounted() { }
// }
</script>

6.3 陷阱 #3:Provide/Inject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('light')

// ✅ 正确:提供响应式数据
provide('theme', theme)

// ✅ 正确:提供方法
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
</script>

<!-- Child.vue -->
<script setup>
import { inject } from 'vue'

// ✅ 正确:注入数据
const theme = inject('theme', 'light')

// ✅ 正确:注入方法
const updateTheme = inject('updateTheme')
</script>

七、最佳实践总结

7.1 代码组织

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
src/
├── components/ # 通用组件
│ ├── Button/
│ ├── Input/
│ └── Modal/
├── composables/ # 组合式函数
│ ├── useFetch.js
│ ├── useForm.js
│ └── useMouse.js
├── views/ # 页面组件
│ ├── Home.vue
│ └── About.vue
├── stores/ # 状态管理
│ ├── user.js
│ └── cart.js
├── utils/ # 工具函数
│ ├── format.js
│ └── validate.js
├── constants/ # 常量
│ └── index.js
└── App.vue

7.2 命名规范

类型 规范 示例
组件 PascalCase CrystalList.vue
Composables useXxx useFetch.js
事件处理 handleXxx handleClick
计算属性 名词/形容词 filteredItems
响应式数据 名词 userList

7.3 性能检查清单

  • 使用 v-show 替代频繁切换的 v-if
  • 列表使用唯一 key
  • 大列表使用虚拟滚动
  • 组件懒加载
  • 计算属性缓存
  • 事件防抖/节流
  • 图片懒加载
  • 路由懒加载

八、参考资料

8.1 官方文档

8.2 相关工具

  • VueUse - 组合式函数集合
  • Volar - Vue 3 语言支持
  • Vite - 下一代构建工具

8.3 推荐阅读

  • 《Vue.js 3 设计与实现》
  • 《Vue 3 最佳实践》

作者:John
职位:高级技术架构师
日期:2026-03-02
版本:v1.0

本文基于 CrystalForge 项目 Vue 3 实战经验编写,所有代码均经过生产环境验证。Vue 3 是前端开发的利器,值得深入学习和持续优化。