引言

Vue.js 作为一款渐进式 JavaScript 框架,自推出以来就以其简洁、灵活和高效的特性受到开发者的广泛喜爱。Vue 3 在 Vue 2 的基础上进行了全面的重构,引入了 Composition API、更好的 TypeScript 支持、性能优化等众多新特性,使其在大型项目和复杂场景中更具优势。本文将从 Vue 3 的核心概念入手,逐步深入到实战应用,并针对项目中常见的问题与挑战提供解决方案,帮助读者系统性地掌握 Vue 3 的核心技能。

一、Vue 3 基础入门

1.1 Vue 3 的新特性概览

Vue 3 相较于 Vue 2 主要带来了以下几大改进:

  • 性能提升:通过静态标记和优化 diff 算法,渲染性能提升约 55%。
  • Composition API:提供了更灵活的逻辑组织方式,特别适合大型项目。
  • 更好的 TypeScript 支持:类型推断更加完善。
  • 响应式系统重构:使用 Proxy 替代 Object.defineProperty,解决了 Vue 2 中响应式系统的诸多限制。
  • Teleport 组件:允许将子组件渲染到 DOM 的其他位置。
  • Fragment 片段:组件可以拥有多个根节点。

1.2 搭建 Vue 3 开发环境

推荐使用 Vite 作为构建工具,它提供了极速的开发体验。

# 使用 npm
npm create vite@latest my-vue-app -- --template vue

# 使用 yarn
yarn create vite my-vue-app --template vue

# 进入项目目录
cd my-vue-app

# 安装依赖
npm install

# 启动开发服务器
npm run dev

1.3 Vue 3 的基本语法

1.3.1 组件的创建

Vue 3 支持两种组件定义方式:Options API 和 Composition API。

Options API 示例

<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="increment">计数: {{ count }}</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: 'Vue 3 Options API 示例',
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

Composition API 示例

<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="increment">计数: {{ count }}</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const title = ref('Vue 3 Composition API 示例')
const count = ref(0)

function increment() {
  count.value++
}
</script>

1.3.2 响应式系统

Vue 3 使用 refreactive 来创建响应式数据。

<script setup>
import { ref, reactive, computed } from 'vue'

// ref 用于基本类型和对象
const count = ref(0)
const user = ref({ name: '张三', age: 25 })

// reactive 用于对象
const state = reactive({
  count: 0,
  user: { name: '李四', age: 30 }
})

// 计算属性
const doubleCount = computed(() => count.value * 2)
const fullName = computed(() => `${user.value.name} (${user.value.age})`)

// 方法
function updateCount() {
  count.value++
  state.count++
}
</script>

二、Vue 3 核心技能深入

2.1 Composition API 详解

Composition API 是 Vue 3 最重要的特性之一,它允许我们按照逻辑功能来组织代码,而不是按照选项类型。

2.1.1 基本使用

<script setup>
import { ref, onMounted, watch } from 'vue'

// 响应式数据
const count = ref(0)
const message = ref('Hello Vue 3')

// 生命周期钩子
onMounted(() => {
  console.log('组件已挂载')
})

// 监听器
watch(count, (newValue, oldValue) => {
  console.log(`计数从 ${oldValue} 变为 ${newValue}`)
})

// 方法
function increment() {
  count.value++
}

function updateMessage() {
  message.value = 'Hello Composition API'
}
</script>

2.1.2 自定义组合函数

Composition API 的强大之处在于可以轻松创建可复用的逻辑组合。

示例:创建一个可复用的计数器组合函数

// composables/useCounter.js
import { ref, readonly } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = initialValue
  }
  
  // 返回只读的 count,防止外部直接修改
  return {
    count: readonly(count),
    increment,
    decrement,
    reset
  }
}

在组件中使用

<template>
  <div>
    <p>计数: {{ count }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="reset">重置</button>
  </div>
</template>

<script setup>
import { useCounter } from './composables/useCounter'

const { count, increment, decrement, reset } = useCounter(10)
</script>

2.2 响应式系统原理

Vue 3 的响应式系统基于 ES6 的 Proxy 实现,相比 Vue 2 的 Object.defineProperty 有以下优势:

  • 可以监听对象属性的添加和删除
  • 可以监听数组索引的变化
  • 性能更好

2.2.1 响应式创建方式对比

import { ref, reactive, toRefs, toRef, isRef, isReactive } from 'vue'

// 1. ref - 用于基本类型和对象
const count = ref(0)
const user = ref({ name: '张三' })

// 2. reactive - 用于对象
const state = reactive({
  count: 0,
  user: { name: '李四' }
})

// 3. toRefs - 将 reactive 对象转换为 ref 对象集合
const stateRefs = toRefs(state)
// stateRefs.count 是一个 ref,stateRefs.user 也是一个 ref

// 4. toRef - 为 reactive 对象的某个属性创建 ref
const countRef = toRef(state, 'count')

// 5. 类型检查
console.log(isRef(count)) // true
console.log(isReactive(state)) // true

2.2.2 响应式数据的解构

import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: '张三',
    age: 25
  }
})

// 错误做法:直接解构会失去响应式
const { count, user } = state
// count 和 user 不再是响应式的

// 正确做法:使用 toRefs
const { count, user } = toRefs(state)
// 现在 count 和 user 都是 ref,保持响应式

// 在模板中可以直接使用
// <div>{{ count }}</div>
// <div>{{ user.name }}</div>

2.3 组件通信与状态管理

2.3.1 Props 和 Emit

父组件向子组件传递数据

<!-- Parent.vue -->
<template>
  <ChildComponent 
    :title="parentTitle" 
    :count="parentCount"
    @update-count="handleUpdateCount"
  />
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentTitle = ref('来自父组件的标题')
const parentCount = ref(0)

function handleUpdateCount(newCount) {
  parentCount.value = newCount
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>计数: {{ count }}</p>
    <button @click="updateCount">更新计数</button>
  </div>
</template>

<script setup>
// 定义 props
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 定义 emit
const emit = defineEmits(['update-count'])

function updateCount() {
  emit('update-count', props.count + 1)
}
</script>

2.3.2 Provide/Inject

适用于跨层级组件通信,特别适合插件和库的开发。

<!-- App.vue -->
<template>
  <div>
    <h1>Provide/Inject 示例</h1>
    <ChildComponent />
  </div>
</template>

<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const theme = ref('dark')
const user = ref({ name: '管理员', role: 'admin' })

// 提供数据
provide('theme', theme)
provide('user', user)
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>子组件</h2>
    <p>主题: {{ theme }}</p>
    <p>用户: {{ user.name }} ({{ user.role }})</p>
    <button @click="changeTheme">切换主题</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 注入数据
const theme = inject('theme')
const user = inject('user')

function changeTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
</script>

2.3.3 状态管理 - Pinia

Pinia 是 Vue 3 官方推荐的状态管理库,相比 Vuex 更轻量且支持 Composition API。

安装 Pinia

npm install pinia

创建 Store

// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // 状态
  const users = ref([])
  const currentUser = ref(null)
  
  // 计算属性
  const userCount = computed(() => users.value.length)
  const isAdmin = computed(() => currentUser.value?.role === 'admin')
  
  // Actions
  function addUser(user) {
    users.value.push(user)
  }
  
  function setCurrentUser(user) {
    currentUser.value = user
  }
  
  function removeUser(userId) {
    users.value = users.value.filter(u => u.id !== userId)
  }
  
  // 异步操作
  async function fetchUsers() {
    try {
      const response = await fetch('/api/users')
      const data = await response.json()
      users.value = data
    } catch (error) {
      console.error('获取用户失败:', error)
    }
  }
  
  return {
    users,
    currentUser,
    userCount,
    isAdmin,
    addUser,
    setCurrentUser,
    removeUser,
    fetchUsers
  }
})

在组件中使用

<template>
  <div>
    <h2>用户管理</h2>
    <p>用户总数: {{ userStore.userCount }}</p>
    <p>当前用户: {{ userStore.currentUser?.name || '未登录' }}</p>
    <p v-if="userStore.isAdmin">您是管理员</p>
    
    <button @click="addRandomUser">添加随机用户</button>
    <button @click="fetchUsers">获取用户列表</button>
    
    <ul>
      <li v-for="user in userStore.users" :key="user.id">
        {{ user.name }} - {{ user.role }}
        <button @click="userStore.removeUser(user.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

function addRandomUser() {
  const randomId = Math.floor(Math.random() * 1000)
  userStore.addUser({
    id: randomId,
    name: `用户${randomId}`,
    role: randomId % 2 === 0 ? 'admin' : 'user'
  })
}

function fetchUsers() {
  userStore.fetchUsers()
}
</script>

三、Vue 3 实战应用

3.1 路由管理 - Vue Router 4

Vue Router 4 是 Vue 3 官方路由库,支持 Composition API。

3.1.1 基本配置

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
import AboutView from '@/views/AboutView.vue'
import UserView from '@/views/UserView.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: AboutView
  },
  {
    path: '/user/:id',
    name: 'user',
    component: UserView,
    props: true // 将路由参数作为 props 传递
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('@/views/DashboardView.vue'), // 懒加载
    meta: { requiresAuth: true } // 路由元信息
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 导航守卫
router.beforeEach((to, from, next) => {
  const isAuthenticated = localStorage.getItem('token')
  
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

export default router

3.1.2 在组件中使用路由

<template>
  <div>
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
      <router-link :to="{ name: 'user', params: { id: 123 } }">用户</router-link>
    </nav>
    
    <router-view />
  </div>
</template>

<script setup>
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 获取路由参数
console.log(route.params.id)

// 编程式导航
function goToAbout() {
  router.push('/about')
}

function goBack() {
  router.back()
}
</script>

3.2 HTTP 请求 - Axios

3.2.1 封装 Axios 实例

// utils/request.js
import axios from 'axios'
import { useUserStore } from '@/stores/user'

// 创建 Axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    const userStore = useUserStore()
    
    // 添加认证 token
    if (userStore.token) {
      config.headers.Authorization = `Bearer ${userStore.token}`
    }
    
    // 添加请求时间戳
    config.headers['X-Request-Time'] = Date.now()
    
    return config
  },
  error => {
    console.error('请求拦截器错误:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const { data, status } = response
    
    // 处理业务状态码
    if (status === 200) {
      return data
    } else {
      // 处理错误
      return Promise.reject(new Error(data.message || '请求失败'))
    }
  },
  error => {
    // 处理 HTTP 错误
    if (error.response) {
      const { status, data } = error.response
      
      switch (status) {
        case 401:
          console.error('未授权,请重新登录')
          // 可以在这里处理跳转到登录页
          break
        case 403:
          console.error('拒绝访问')
          break
        case 404:
          console.error('请求资源不存在')
          break
        case 500:
          console.error('服务器内部错误')
          break
        default:
          console.error('其他错误:', error.message)
      }
    } else {
      console.error('网络错误:', error.message)
    }
    
    return Promise.reject(error)
  }
)

export default service

3.2.2 API 封装

// api/user.js
import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/api/auth/login',
    method: 'post',
    data
  })
}

export function getUserInfo() {
  return request({
    url: '/api/user/info',
    method: 'get'
  })
}

export function updateUser(data) {
  return request({
    url: '/api/user/update',
    method: 'put',
    data
  })
}

3.3 表单处理与验证

3.3.1 使用 Vuelidate 进行表单验证

npm install @vuelidate/core @vuelidate/validators
<template>
  <div>
    <h2>用户注册表单</h2>
    <form @submit.prevent="handleSubmit">
      <div>
        <label>用户名:</label>
        <input 
          v-model="form.username" 
          @blur="v$.username.$touch()"
          :class="{ 'error': v$.username.$error }"
        />
        <div v-if="v$.username.$error" class="error-message">
          <span v-for="error in v$.username.$errors" :key="error.$uid">
            {{ error.$message }}
          </span>
        </div>
      </div>
      
      <div>
        <label>邮箱:</label>
        <input 
          v-model="form.email" 
          @blur="v$.email.$touch()"
          :class="{ 'error': v$.email.$error }"
        />
        <div v-if="v$.email.$error" class="error-message">
          <span v-for="error in v$.email.$errors" :key="error.$uid">
            {{ error.$message }}
          </span>
        </div>
      </div>
      
      <div>
        <label>密码:</label>
        <input 
          type="password"
          v-model="form.password" 
          @blur="v$.password.$touch()"
          :class="{ 'error': v$.password.$error }"
        />
        <div v-if="v$.password.$error" class="error-message">
          <span v-for="error in v$.password.$errors" :key="error.$uid">
            {{ error.$message }}
          </span>
        </div>
      </div>
      
      <div>
        <label>确认密码:</label>
        <input 
          type="password"
          v-model="form.confirmPassword" 
          @blur="v$.confirmPassword.$touch()"
          :class="{ 'error': v$.confirmPassword.$error }"
        />
        <div v-if="v$.confirmPassword.$error" class="error-message">
          <span v-for="error in v$.confirmPassword.$errors" :key="error.$uid">
            {{ error.$message }}
          </span>
        </div>
      </div>
      
      <button type="submit" :disabled="v$.$invalid">注册</button>
    </form>
  </div>
</template>

<script setup>
import { reactive, computed } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength, sameAs } from '@vuelidate/validators'

const form = reactive({
  username: '',
  email: '',
  password: '',
  confirmPassword: ''
})

const rules = computed(() => ({
  username: {
    required,
    minLength: minLength(3)
  },
  email: {
    required,
    email
  },
  password: {
    required,
    minLength: minLength(6)
  },
  confirmPassword: {
    required,
    sameAs: sameAs(form.password)
  }
}))

const v$ = useVuelidate(rules, form)

function handleSubmit() {
  v$.value.$touch()
  
  if (!v$.value.$invalid) {
    console.log('表单验证通过:', form)
    // 提交表单数据
  }
}
</script>

<style scoped>
.error {
  border-color: red;
}

.error-message {
  color: red;
  font-size: 12px;
  margin-top: 4px;
}
</style>

四、项目中常见问题与挑战的解决方案

4.1 性能优化问题

4.1.1 组件渲染优化

问题:大型列表渲染导致页面卡顿。

解决方案:使用虚拟滚动或分页。

<template>
  <div>
    <h2>虚拟滚动列表</h2>
    <div class="list-container" ref="listContainer">
      <div class="list-viewport" :style="viewportStyle">
        <div 
          v-for="item in visibleItems" 
          :key="item.id"
          class="list-item"
          :style="getItemStyle(item)"
        >
          {{ item.name }} - {{ item.value }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

// 模拟大量数据
const allItems = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `项目 ${i}`,
  value: Math.random() * 1000
}))

const itemHeight = 40 // 每项高度
const containerHeight = 400 // 容器高度
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2 // 可见项数量

const scrollTop = ref(0)
const listContainer = ref(null)

// 计算可见项
const visibleItems = computed(() => {
  const startIndex = Math.floor(scrollTop.value / itemHeight)
  const endIndex = Math.min(startIndex + visibleCount, allItems.length)
  
  return allItems.slice(startIndex, endIndex)
})

// 计算视口样式
const viewportStyle = computed(() => {
  const totalHeight = allItems.length * itemHeight
  const startIndex = Math.floor(scrollTop.value / itemHeight)
  const offsetY = startIndex * itemHeight
  
  return {
    height: `${totalHeight}px`,
    transform: `translateY(${offsetY}px)`
  }
})

// 获取每项的样式
const getItemStyle = (item) => {
  const index = allItems.indexOf(item)
  return {
    height: `${itemHeight}px`,
    position: 'absolute',
    top: `${index * itemHeight}px`,
    width: '100%'
  }
}

// 监听滚动事件
const handleScroll = () => {
  if (listContainer.value) {
    scrollTop.value = listContainer.value.scrollTop
  }
}

onMounted(() => {
  if (listContainer.value) {
    listContainer.value.addEventListener('scroll', handleScroll)
  }
})

onUnmounted(() => {
  if (listContainer.value) {
    listContainer.value.removeEventListener('scroll', handleScroll)
  }
})
</script>

<style scoped>
.list-container {
  height: 400px;
  overflow-y: auto;
  border: 1px solid #ccc;
  position: relative;
}

.list-viewport {
  position: relative;
}

.list-item {
  border-bottom: 1px solid #eee;
  padding: 0 10px;
  display: flex;
  align-items: center;
  box-sizing: border-box;
}
</style>

4.1.2 代码分割与懒加载

问题:应用初始加载时间过长。

解决方案:使用动态导入进行代码分割。

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/HomeView.vue')
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('@/views/DashboardView.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    name: 'admin',
    component: () => import('@/views/AdminView.vue'),
    meta: { requiresAuth: true, requiresAdmin: true }
  }
]

// 组件级别的懒加载
const router = createRouter({
  history: createWebHistory(),
  routes
})

// 路由级别的懒加载
router.beforeEach((to, from, next) => {
  // 可以在这里处理路由级别的懒加载逻辑
  next()
})

export default router

4.2 响应式数据管理问题

4.2.1 响应式数据丢失

问题:在异步操作或第三方库中,响应式数据可能丢失。

解决方案:使用 toRawmarkRaw 控制响应式。

import { reactive, toRaw, markRaw, watch } from 'vue'

const state = reactive({
  user: null,
  config: null
})

// 1. 使用 toRaw 获取原始对象(非响应式)
function updateUser(user) {
  // 在某些情况下,我们可能需要原始对象
  const rawUser = toRaw(user)
  console.log('原始用户数据:', rawUser)
  
  // 但更新时仍需使用响应式对象
  state.user = user
}

// 2. 使用 markRaw 标记对象为非响应式
function loadConfig() {
  // 如果配置数据不需要响应式,可以标记为非响应式
  const config = markRaw({
    apiVersion: '1.0',
    features: ['feature1', 'feature2']
  })
  
  state.config = config
}

// 3. 监听响应式数据的变化
watch(
  () => state.user,
  (newUser, oldUser) => {
    console.log('用户数据变化:', newUser)
    // 这里可以执行一些副作用操作
  },
  { deep: true }
)

4.2.2 大型对象响应式性能问题

问题:对大型对象进行深度响应式转换可能导致性能问题。

解决方案:使用 shallowRefshallowReactive

import { shallowRef, shallowReactive, watch } from 'vue'

// 1. 使用 shallowRef - 只有 .value 变化时才触发更新
const largeData = shallowRef({
  // 假设这是一个包含大量数据的对象
  items: Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    data: `Item ${i}`
  })),
  metadata: {
    total: 10000,
    page: 1
  }
})

// 2. 使用 shallowReactive - 只监听顶层属性变化
const state = shallowReactive({
  user: { name: '张三', age: 25 },
  settings: { theme: 'dark', language: 'zh' }
})

// 3. 性能优化示例
function updateLargeData() {
  // 这种方式不会触发响应式更新
  largeData.value.items.push({ id: 9999, data: 'New Item' })
  
  // 需要手动触发更新
  largeData.value = { ...largeData.value }
}

4.3 组件通信问题

4.3.1 跨层级组件通信

问题:深层嵌套组件之间的数据传递困难。

解决方案:使用 Provide/Inject 或事件总线。

方案一:Provide/Inject(推荐)

// composables/useEventBus.js
import { provide, inject, ref } from 'vue'

const EventBusSymbol = Symbol('event-bus')

export function createEventBus() {
  const events = ref(new Map())
  
  function on(event, callback) {
    if (!events.value.has(event)) {
      events.value.set(event, [])
    }
    events.value.get(event).push(callback)
  }
  
  function emit(event, ...args) {
    const callbacks = events.value.get(event)
    if (callbacks) {
      callbacks.forEach(callback => callback(...args))
    }
  }
  
  function off(event, callback) {
    const callbacks = events.value.get(event)
    if (callbacks) {
      const index = callbacks.indexOf(callback)
      if (index > -1) {
        callbacks.splice(index, 1)
      }
    }
  }
  
  return { on, emit, off }
}

export function useEventBus() {
  const eventBus = inject(EventBusSymbol)
  if (!eventBus) {
    throw new Error('EventBus not provided')
  }
  return eventBus
}

export function provideEventBus(app) {
  const eventBus = createEventBus()
  app.provide(EventBusSymbol, eventBus)
}

方案二:事件总线(简单场景)

// utils/eventBus.js
import { ref } from 'vue'

const listeners = ref(new Map())

export function on(event, callback) {
  if (!listeners.value.has(event)) {
    listeners.value.set(event, [])
  }
  listeners.value.get(event).push(callback)
}

export function emit(event, ...args) {
  const callbacks = listeners.value.get(event)
  if (callbacks) {
    callbacks.forEach(callback => callback(...args))
  }
}

export function off(event, callback) {
  const callbacks = listeners.value.get(event)
  if (callbacks) {
    const index = callbacks.indexOf(callback)
    if (index > -1) {
      callbacks.splice(index, 1)
    }
  }
}

4.4 TypeScript 集成问题

4.4.1 类型定义不完整

问题:在 Vue 3 中使用 TypeScript 时,类型定义不完整。

解决方案:使用 definePropsdefineEmits 的泛型支持。

<script setup lang="ts">
import { ref, computed } from 'vue'

// 1. 使用泛型定义 Props
interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

const props = defineProps<{
  user: User
  title: string
  count?: number
}>()

// 2. 使用泛型定义 Emits
const emit = defineEmits<{
  (e: 'update:user', user: User): void
  (e: 'delete:user', userId: number): void
  (e: 'click'): void
}>()

// 3. 响应式数据类型
const localUser = ref<User>(props.user)
const localCount = ref<number>(props.count || 0)

// 4. 计算属性类型
const displayName = computed<string>(() => {
  return `${localUser.value.name} (${localUser.value.role})`
})

// 5. 方法类型
function updateUser(updatedUser: User): void {
  emit('update:user', updatedUser)
}

function deleteUser(): void {
  emit('delete:user', localUser.value.id)
}

function handleClick(): void {
  emit('click')
}
</script>

4.4.2 组件实例类型

问题:在 Options API 中,this 的类型推断不准确。

解决方案:使用 defineComponent 和类型注解。

<script lang="ts">
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
  name: 'UserCard',
  
  props: {
    user: {
      type: Object as () => { id: number; name: string; email: string },
      required: true
    }
  },
  
  emits: {
    'update:user': (user: { id: number; name: string; email: string }) => true
  },
  
  setup(props, { emit }) {
    const localUser = ref(props.user)
    
    const displayName = computed(() => {
      return localUser.value.name
    })
    
    function updateUser() {
      emit('update:user', localUser.value)
    }
    
    return {
      localUser,
      displayName,
      updateUser
    }
  }
})
</script>

4.5 错误处理与调试

4.5.1 全局错误处理

问题:未捕获的错误可能导致应用崩溃。

解决方案:使用 Vue 3 的错误处理 API。

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const app = createApp(App)
const pinia = createPinia()

// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
  console.error('Vue 错误:', err)
  console.error('组件实例:', instance)
  console.error('错误信息:', info)
  
  // 可以发送到错误监控服务
  // sendToErrorMonitoringService(err, instance, info)
  
  // 可以显示错误提示
  // showErrorNotification(err)
}

// 全局警告处理
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue 警告:', msg)
  console.warn('组件实例:', instance)
  console.warn('调用栈:', trace)
}

// 全局性能标记
app.config.performance = true

app.use(pinia)
app.mount('#app')

4.5.2 组件内错误处理

<template>
  <div>
    <h2>错误处理示例</h2>
    
    <button @click="triggerError">触发错误</button>
    
    <div v-if="error" class="error-boundary">
      <h3>发生错误</h3>
      <p>{{ error.message }}</p>
      <button @click="resetError">重试</button>
    </div>
    
    <div v-else>
      <slot />
    </div>
  </div>
</template>

<script setup>
import { ref, onErrorCaptured } from 'vue'

const error = ref(null)

// 捕获子组件错误
onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err)
  error.value = err
  return false // 阻止错误继续传播
})

function triggerError() {
  throw new Error('这是一个测试错误')
}

function resetError() {
  error.value = null
}
</script>

<style scoped>
.error-boundary {
  border: 2px solid red;
  padding: 20px;
  background-color: #ffe6e6;
  border-radius: 8px;
  margin: 20px 0;
}
</style>

五、最佳实践与进阶技巧

5.1 项目结构组织

推荐的项目结构:

src/
├── assets/          # 静态资源
├── components/      # 通用组件
│   ├── common/      # 公共组件
│   └── layout/      # 布局组件
├── composables/     # 组合函数
├── stores/          # Pinia 状态管理
├── router/          # 路由配置
├── views/           # 页面组件
├── utils/           # 工具函数
├── services/        # API 服务
├── types/           # TypeScript 类型定义
├── App.vue          # 根组件
└── main.ts          # 入口文件

5.2 自定义指令

// directives/vPermission.js
export const vPermission = {
  mounted(el, binding) {
    const { value } = binding
    const permissions = getPermissions() // 从 store 获取权限
    
    if (!permissions.includes(value)) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  }
}

// directives/vLazyLoad.js
export const vLazyLoad = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const src = binding.value
          if (el.tagName === 'IMG') {
            el.src = src
          } else {
            el.style.backgroundImage = `url(${src})`
          }
          observer.unobserve(el)
        }
      })
    })
    
    observer.observe(el)
  }
}

5.3 插件开发

// plugins/myPlugin.js
export default {
  install(app, options) {
    // 1. 添加全局组件
    app.component('MyGlobalComponent', MyGlobalComponent)
    
    // 2. 添加全局指令
    app.directive('my-directive', {
      mounted(el, binding) {
        el.style.color = binding.value || 'blue'
      }
    })
    
    // 3. 添加全局方法
    app.config.globalProperties.$myMethod = function() {
      console.log('全局方法被调用')
    }
    
    // 4. 添加全局混入
    app.mixin({
      created() {
        console.log('全局混入在组件创建时执行')
      }
    })
    
    // 5. 提供/注入
    app.provide('myPlugin', {
      version: '1.0.0',
      options
    })
  }
}

六、总结

掌握 Vue 3 需要系统性地学习其核心概念,并通过实战项目不断积累经验。本文从基础入门到实战应用,详细介绍了 Vue 3 的核心技能,并针对项目中常见的问题与挑战提供了具体的解决方案。

关键要点回顾:

  1. Composition API 是 Vue 3 的核心特性,提供了更灵活的代码组织方式
  2. 响应式系统 基于 Proxy 实现,性能更好且功能更强大
  3. Pinia 是 Vue 3 推荐的状态管理方案
  4. 性能优化 需要从多个角度考虑,包括代码分割、虚拟滚动等
  5. TypeScript 集成需要良好的类型定义和类型推断
  6. 错误处理 是保证应用稳定性的关键

进阶学习建议:

  1. 深入研究 Vue 3 源码,理解其响应式系统和编译原理
  2. 学习 Vue 3 的测试工具(Vitest、Vue Test Utils)
  3. 掌握 Vue 3 与后端框架(如 NestJS)的集成
  4. 学习 Vue 3 在移动端开发中的应用(如使用 Capacitor)
  5. 探索 Vue 3 在微前端架构中的应用

通过不断实践和总结,你将能够熟练运用 Vue 3 解决各种复杂的前端问题,构建高性能、可维护的 Web 应用。