Vue 3 实战完全指南:从入门到进阶

技术栈:Vue 3 + Vite + TypeScript + Pinia + Vue Router 4
适合人群:前端开发者、有 Vue 2 基础的团队、想系统掌握 Vue 3 的工程师
更新日期:2026-05-16


1. Vue 3 新特性概览

1.1 为什么选择 Vue 3?

特性Vue 2Vue 3提升
核心架构Options APIComposition API代码组织更灵活
响应式系统definePropertyProxy支持深层对象、数组
TypeScript 支持勉强支持原生 TypeScript类型推断更准确
打包体积较大减小约 30%首屏加载更快
虚拟 DOMVDOM最优解(PatchFlag)渲染性能提升
插槽机制slot/scope-slot统一 v-slotAPI 更简洁
Teleport✅ 内置模态框/弹窗更易实现
Fragments组件可返回多个根节点
Suspense异步组件加载体验更好

1.2 Vue 3 架构图

┌─────────────────────────────────────────────────────────┐
│                      Vue 3 应用                          │
├─────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │
│  │ Vue Router  │  │   Pinia     │  │    VueUse       │ │
│  │  (路由管理)  │  │ (状态管理)  │  │  (Composables)  │ │
│  └──────┬──────┘  └──────┬──────┘  └────────┬────────┘ │
│         │                │                  │          │
│  ┌──────┴────────────────┴──────────────────┴──────┐   │
│  │              Composition API                     │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────────┐  │   │
│  │  │  setup()  │  │ ref()    │  │  computed()  │  │   │
│  │  │  reactive │  │ watch    │  │  lifecycle   │  │   │
│  │  └──────────┘  └──────────┘  └──────────────┘  │   │
│  └───────────────────────┬──────────────────────────┘   │
│                          │                              │
│  ┌───────────────────────┴──────────────────────────┐   │
│  │           响应式系统 (Proxy)                      │   │
│  │  ┌────────────┐  ┌────────────┐  ┌────────────┐  │   │
│  │  │   getter   │  │   setter   │  │  scheduler │  │   │
│  │  └────────────┘  └────────────┘  └────────────┘  │   │
│  └────────────────────────────────────────────────────┘   │
│                          │                              │
│  ┌───────────────────────┴──────────────────────────┐   │
│  │              渲染器 (Renderer)                   │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────────┐  │   │
│  │  │  Patch   │  │  Hooks   │  │  PatchFlag   │  │   │
│  │  └──────────┘  └──────────┘  └──────────────┘  │   │
│  └────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

2. 项目初始化:Vite + Vue 3 + TypeScript

2.1 环境要求

# Node.js 版本要求
node >= 18.12.0  (推荐 v20 LTS)

# 检查版本
node -v
npm -v

2.2 创建项目

# 方式一:交互式创建(推荐新手)
npm create vue@latest my-vue-app

# 方式二:使用 Vite 快速创建
npm create vite@latest my-vue-app -- --template vue-ts

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

# 安装依赖
npm install

# 启动开发服务器
npm run dev

2.3 项目结构详解

my-vue-app/
├── public/                 # 静态资源(不会被构建)
│   └── favicon.ico
├── src/
│   ├── assets/             # 资源文件(会被构建)
│   │   └── logo.svg
│   ├── components/         # 公共组件
│   │   ├── HelloWorld.vue
│   │   └── TheWelcome.vue
│   ├── composables/        # 组合式函数(VueUse 风格)
│   │   └── useCounter.ts
│   ├── router/             # 路由配置
│   │   └── index.ts
│   ├── stores/             # Pinia 状态管理
│   │   └── counter.ts
│   ├── types/              # TypeScript 类型定义
│   │   └── index.ts
│   ├── views/              # 页面组件
│   │   ├── HomeView.vue
│   │   └── AboutView.vue
│   ├── App.vue             # 根组件
│   ├── main.ts             # 入口文件
│   └── style.css           # 全局样式
├── index.html              # HTML 入口
├── package.json
├── tsconfig.json           # TypeScript 配置
├── tsconfig.app.json       # 应用专用 TS 配置
├── tsconfig.node.json      # Node 环境 TS 配置
├── vite.config.ts          # Vite 构建配置
└── README.md

2.4 Vite 配置详解

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],

  // 路径别名
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@views': resolve(__dirname, 'src/views'),
    },
  },

  // 开发服务器配置
  server: {
    port: 3000,
    host: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  // 构建配置
  build: {
    target: 'es2020',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
        },
      },
    },
  },

  // CSS 配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/assets/variables.scss";`,
      },
    },
  },
})

2.5 TypeScript 配置

// tsconfig.app.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* 模块解析 */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",

    /* 路径别名 */
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@views/*": ["src/views/*"]
    },

    /* 严格类型检查 */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* Vue3 类型支持 */
    "types": ["vite/client"]
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

3. 核心概念:响应式系统

3.1 Vue 3 响应式原理

Vue 3 使用 ES6 Proxy 实现响应式,相比 Vue 2 的 Object.defineProperty 有以下优势:

┌────────────────────────────────────────────────────────────────┐
│                      Vue 3 响应式原理                           │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│   const original = { name: 'Vue', version: 3 }                │
│                                                                │
│   const proxy = new Proxy(original, {                         │
│     get(target, key) {                                        │
│       console.log(`读取 ${key}`)                               │
│       return Reflect.get(target, key)                        │
│     },                                                        │
│     set(target, key, value) {                                 │
│       console.log(`设置 ${key} = ${value}`)                    │
│       return Reflect.set(target, key, value)                 │
│     },                                                        │
│     deleteProperty(target, key) {                             │
│       console.log(`删除 ${key}`)                              │
│       return Reflect.deleteProperty(target, key)              │
│     }                                                         │
│   })                                                          │
│                                                                │
│   proxy.name      // 触发 get → "读取 name"                   │
│   proxy.version = 4 // 触发 set → "设置 version = 4"         │
│                                                                │
└────────────────────────────────────────────────────────────────┘

3.2 ref() vs reactive() 对比

特性ref()reactive()
适用类型基本类型 + 对象仅对象/数组
访问方式需要 .value直接访问
解构丢失响应式会丢失(需 toRefs会丢失(需 toRef
TypeScript 类型Ref<T>Reactive<T>
重新赋值支持(整个替换)不支持(需使用 ref)

3.3 ref() 详解

import { ref, isRef } from 'vue'

// 基本类型
const count = ref(0)
console.log(count.value)  // 0
count.value++
console.log(count.value)  // 1

// 对象类型
const user = ref({
  name: '张三',
  age: 25
})
console.log(user.value.name)  // 张三

// 模板中自动解包(不需要 .value)
// <template>
//   <div>{{ count }}</div>  <!-- 自动访问 .value -->
// </template>

// 判断是否为 ref
console.log(isRef(count))  // true
console.log(isRef(user))   // true

// 泛型类型
const str = ref<string>('Hello')
const num = ref<number>(100)
const arr = ref<string[]>(['a', 'b', 'c'])

3.4 reactive() 详解

import { reactive, readonly, isReactive } from 'vue'

// 对象响应式
const state = reactive({
  count: 0,
  user: {
    name: '李四',
    address: {
      city: '北京',
      district: '朝阳区'
    }
  },
  tags: ['Vue', 'TypeScript', 'Vite']
})

// 直接访问属性(无需 .value)
state.count++
state.user.name = '王五'

// 数组操作
state.tags.push('React')
state.tags.length = 0  // 清空数组

// 深层次响应式
state.user.address.city = '上海'  // 深层变更仍然响应

// 只读响应式
const readonlyState = readonly(state)
readonlyState.count++  // 警告!不能修改只读属性

// 判断是否为 reactive 对象
console.log(isReactive(state))  // true

3.5 toRef() 与 toRefs()

import { reactive, toRef, toRefs } from 'vue'

// ❌ 解构会丢失响应式
const state = reactive({ name: '张三', age: 25 })
const { name, age } = state  // 丢失响应式!

// ✅ 使用 toRefs 保持响应式
const state = reactive({ name: '张三', age: 25 })
const { name, age } = toRefs(state)

// 修改响应式数据
name.value = '李四'  // 会修改 state.name
age.value = 30      // 会修改 state.age

// ✅ 单个属性转换
const nameRef = toRef(state, 'name')
console.log(nameRef.value)  // 张三

3.6 computed() 计算属性

import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 只读计算属性
const fullName = computed(() => firstName.value + lastName.value)
console.log(fullName.value)  // 张三

// 可写计算属性
const user = ref({
  firstName: 'John',
  lastName: 'Doe'
})

const fullNameWritable = computed({
  get: () => `${user.value.firstName} ${user.value.lastName}`,
  set: (val) => {
    const [first, last] = val.split(' ')
    user.value.firstName = first
    user.value.lastName = last
  }
})

fullNameWritable.value = 'Jane Smith'
console.log(user.value.firstName)  // Jane

// 计算属性类型
interface UserInfo {
  name: string
  age: number
  emails: string[]
}

const users = ref<UserInfo[]>([
  { name: '张三', age: 25, emails: ['zhang@example.com'] },
  { name: '李四', age: 30, emails: ['li@example.com'] }
])

// 泛型计算属性
const userNames = computed<string[]>(() => 
  users.value.map(u => u.name)
)

// 带条件的计算
const adultUsers = computed(() =>
  users.value.filter(u => u.age >= 18)
)

3.7 watch() 监听器

import { ref, watch, watchEffect, WatchStopHandle } from 'vue'

// 基础监听
const count = ref(0)
watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
count.value++

// 监听多个数据源
const firstName = ref('张')
const lastName = ref('三')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`${oldFirst} -> ${newFirst}, ${oldLast} -> ${newLast}`)
})

// 监听深层对象
const user = ref({
  profile: {
    address: {
      city: '北京'
    }
  }
})

watch(user, (newUser) => {
  console.log('用户信息变化:', newUser)
}, { deep: true })

// 立即执行
watch(user, (newUser) => {
  console.log('立即执行:', newUser)
}, { immediate: true })

// 只监听特定路径
watch(() => user.value.profile.address.city, (newCity) => {
  console.log('城市变化:', newCity)
})

// 停止监听
const stop = watch(count, (newVal) => {
  console.log('监听中...')
})

stop()  // 停止监听

// watchEffect - 自动收集依赖
const id = ref(1)
const userData = ref(null)

watchEffect(async () => {
  // 自动追踪 id.value 的使用
  const response = await fetch(`/api/users/${id.value}`)
  userData.value = await response.json()
  console.log('用户数据更新')
})

// watchEffect 清理
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('定时器执行')
  }, 1000)

  onCleanup(() => {
    clearTimeout(timer)
    console.log('清理定时器')
  })
})

// 返回停止句柄
const stopEffect = watchEffect(() => {
  // ...
})
stopEffect()

4. Composition API 全面解析

4.1 setup() 组件选项

<script setup lang="ts">
// setup() 是 Composition API 的入口点
// 在 beforeCreate 钩子之前执行,此时组件实例还未创建

import { ref, computed, onMounted } from 'vue'

// 响应式状态
const message = ref('Hello Vue 3!')
const count = ref(0)

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

// 方法
const increment = () => {
  count.value++
}

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

// 返回的值将暴露给模板
return {
  message,
  count,
  doubled,
  increment
}
</script>

<template>
  <div>
    <h1>{{ message }}</h1>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

4.2 <script setup> 语法糖

<!-- MyComponent.vue -->
<script setup lang="ts">
// <script setup> 是 setup() 的语法糖
// 所有顶層变量自动暴露给模板

import { ref, computed, defineProps, defineEmits, defineExpose } from 'vue'

// 定义 Props
interface Props {
  title: string
  count?: number
  items?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []
})

// 定义 Emits
const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'delete', id: number): void
}>()

// 响应式数据
const localCount = ref(props.count)
const inputValue = ref('')

// 计算属性
const itemCount = computed(() => props.items.length)

// 方法
const handleUpdate = () => {
  emit('update', localCount.value)
}

const handleDelete = (id: number) => {
  emit('delete', id)
}

// 暴露给父组件(通过 ref 访问)
defineExpose({
  localCount,
  reset: () => {
    localCount.value = 0
  }
})
</script>

<template>
  <div class="my-component">
    <h2>{{ title }}</h2>
    <p>Items: {{ itemCount }}</p>
    <input v-model="inputValue" />
    <button @click="handleUpdate">更新</button>
  </div>
</template>

<style scoped>
.my-component {
  padding: 1rem;
  border: 1px solid #ddd;
}
</style>

4.3 组合式函数(Composables)

// composables/useCounter.ts
import { ref, computed, Ref } from 'vue'

interface UseCounterOptions {
  min?: number
  max?: number
  step?: number
}

interface UseCounterReturn {
  count: Ref<number>
  increment: () => void
  decrement: () => void
  reset: () => void
  isAtMin: Ref<boolean>
  isAtMax: Ref<boolean>
}

export function useCounter(initialValue = 0, options: UseCounterOptions = {}) {
  const { min = 0, max = Infinity, step = 1 } = options

  const count = ref(initialValue)

  const isAtMin = computed(() => count.value <= min)
  const isAtMax = computed(() => count.value >= max)

  const increment = () => {
    if (count.value < max) {
      count.value = Math.min(count.value + step, max)
    }
  }

  const decrement = () => {
    if (count.value > min) {
      count.value = Math.max(count.value - step, min)
    }
  }

  const reset = () => {
    count.value = initialValue
  }

  return {
    count,
    increment,
    decrement,
    reset,
    isAtMin,
    isAtMax
  }
}
// composables/useFetch.ts
import { ref, Ref, watchEffect } from 'vue'

interface UseFetchOptions {
  immediate?: boolean
  refetch?: Ref<boolean>
}

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

export function useFetch<T>(
  url: string | Ref<string>,
  options: UseFetchOptions = {}
): UseFetchReturn<T> {
  const { immediate = true, refetch } = options

  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<Error | null>(null)
  const loading = ref(false)

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

    try {
      const currentUrl = typeof url === 'string' ? url : url.value
      const response = await fetch(currentUrl)

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      data.value = await response.json()
    } catch (e) {
      error.value = e instanceof Error ? e : new Error('Unknown error')
    } finally {
      loading.value = false
    }
  }

  if (immediate) {
    watchEffect(() => {
      execute()
    })
  }

  if (refetch) {
    watchEffect(() => {
      if (refetch.value) {
        execute()
      }
    })
  }

  return { data, error, loading, execute }
}
// composables/useLocalStorage.ts
import { ref, watch, Ref } from 'vue'

export function useLocalStorage<T>(
  key: string,
  defaultValue: T
): Ref<T> {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)

  watch(
    data,
    (newValue) => {
      if (newValue === null || newValue === undefined) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newValue))
      }
    },
    { deep: true }
  )

  return data
}

// 使用示例
// const theme = useLocalStorage('theme', 'light')
// theme.value = 'dark'  // 自动保存到 localStorage
<!-- 组件中使用 Composable -->
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
import { useFetch } from '@/composables/useFetch'

// 计数器
const { count, increment, decrement, reset } = useCounter(0, {
  min: 0,
  max: 100,
  step: 5
})

// 数据获取
const userId = ref(1)
const { data: user, loading, error, execute } = useFetch<User>(
  () => `/api/users/${userId.value}`,
  { immediate: false }
)

// 手动触发
const fetchNextUser = () => {
  userId.value++
  execute()
}
</script>

<template>
  <div>
    <h2>计数器: {{ count }}</h2>
    <button @click="decrement">-</button>
    <button @click="reset">重置</button>
    <button @click="increment">+</button>

    <hr />

    <button @click="fetchNextUser">加载用户</button>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error.message }}</div>
    <div v-else-if="user">{{ user.name }}</div>
  </div>
</template>

4.4 Provide / Inject 跨组件通信

// ParentComponent.vue
<script setup lang="ts">
import { provide, ref } from 'vue'

// 提供简单值
provide('appName', 'My Vue App')

// 提供响应式数据
const theme = ref('light')
provide('theme', theme)

// 提供对象(包含多个值)
const userContext = {
  name: '张三',
  role: 'admin'
}
provide('user', userContext)

// 提供函数
const updateTheme = (newTheme: string) => {
  theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>

<template>
  <div class="parent">
    <h1>父组件 - 主题: {{ theme }}</h1>
    <ChildComponent />
  </div>
</template>
<!-- GrandChildComponent.vue (深层嵌套) -->
<script setup lang="ts">
import { inject, computed } from 'vue'

// 注入简单值
const appName = inject<string>('appName')

// 注入响应式数据
const theme = inject<Ref<string>>('theme')

// 注入带默认值的响应式数据
const config = inject('config', { apiUrl: 'http://localhost:3000' })

// 注入函数
const updateTheme = inject<(theme: string) => void>('updateTheme')

// 注入并处理不存在的情况
const analytics = inject('analytics', () => console.log('no analytics'))
</script>

<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <p>应用名称: {{ appName }}</p>
    <p>当前主题: {{ theme }}</p>
    <button @click="updateTheme?.('dark')">切换到深色</button>
  </div>
</template>

4.5 nextTick 与异步更新

import { ref, nextTick } from 'vue'

const count = ref(0)
const listRef = ref<HTMLElement>()

// 更新后获取 DOM
const updateAndGetDOM = async () => {
  count.value++

  // DOM 更新是异步的,需要等待
  await nextTick()

  console.log(listRef.value?.innerHTML)  // 此时 DOM 已更新
}

// 批量更新
const batchUpdate = async () => {
  // 多次修改只触发一次更新
  count.value++
  count.value++
  count.value++
  // 此时还未更新 DOM

  await nextTick()
  // DOM 现在已更新为最终值
}

5. TypeScript 与 Vue 3 深度集成

5.1 组件 Props 类型定义

// types/components.ts

// 基础类型定义
interface BaseProps {
  title: string
  visible: boolean
}

// 可选属性
interface OptionalProps {
  width?: number
  height?: number
  class?: string
}

// 带验证的 Props
interface ValidatedProps {
  count: {
    type: NumberConstructor
    required: true
    default: 0
    validator: (val: number) => val >= 0
  }
}

// 联合类型 Props
type ButtonVariant = 'primary' | 'secondary' | 'danger'
type IconPosition = 'left' | 'right'

interface ButtonProps {
  variant: ButtonVariant
  iconPosition?: IconPosition
  disabled?: boolean
}

// 泛型 Props
interface ListProps<T> {
  items: T[]
  selected?: T
  onSelect?: (item: T) => void
}

// 事件类型
interface CustomEvents {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'click', event: MouseEvent): void
}

// 暴露类型
interface ExposedMethods {
  reset(): void
  refresh(): Promise<void>
  scrollToTop(): void
}
<!-- TypedProps.vue -->
<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  title: string
  count: number
  variant?: 'primary' | 'secondary' | 'danger'
  items?: string[]
  onUpdate?: (value: number) => void
}

// 定义 Props
const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  items: () => []
})

// Props 只读
// props.title = '新标题'  // 错误!Props 是只读的

// 使用 Props
const titleClass = computed(() => `title-${props.variant}`)
</script>

<template>
  <div :class="titleClass">
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
  </div>
</template>

5.2 事件类型定义

<!-- EventEmitting.vue -->
<script setup lang="ts">
import { defineEmits } from 'vue'

// 方式一:使用类型化 emits
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'custom', payload: { action: string; data: unknown }): void
}>()

// 触发事件
const handleSubmit = () => {
  emit('update', 'new value')
}

const handleDelete = (id: number) => {
  emit('delete', id)
}
</script>

<template>
  <button @click="handleSubmit">更新</button>
  <button @click="handleDelete(1)">删除</button>
</template>
<!-- ParentComponent.vue -->
<script setup lang="ts">
const handleUpdate = (value: string) => {
  console.log('更新:', value)
}

const handleDelete = (id: number) => {
  console.log('删除 ID:', id)
}
</script>

<template>
  <!-- 类型安全的事件处理 -->
  <EventEmitting 
    @update="handleUpdate" 
    @delete="handleDelete"
  />
</template>

5.3 模板引用类型

<!-- TemplateRef.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'

// DOM 元素引用
const containerRef = ref<HTMLElement>()
const inputRef = ref<HTMLInputElement>()
const canvasRef = ref<HTMLCanvasElement>()

// 组件引用
import ChildComponent from './ChildComponent.vue'
const childRef = ref<InstanceType<typeof ChildComponent>>()

onMounted(() => {
  // DOM 操作
  console.log(containerRef.value?.className)
  inputRef.value?.focus()

  // 组件方法
  childRef.value?.reset()
  childRef.value?.refresh()
})

// 数组引用
const itemRefs = ref<HTMLElement[]>([])
const setItemRef = (el: HTMLElement | null, index: number) => {
  if (el) {
    itemRefs.value[index] = el
  }
}
</script>

<template>
  <div ref="containerRef">
    <input ref="inputRef" type="text" />
    <canvas ref="canvasRef" width="800" height="600"></canvas>
    <ChildComponent ref="childRef" />

    <!-- 动态引用 -->
    <div 
      v-for="(item, index) in items" 
      :ref="(el) => setItemRef(el as HTMLElement, index)"
    >
      {{ item }}
    </div>
  </div>
</template>

5.4 泛型组件

<!-- GenericTable.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
import { ref, computed } from 'vue'

interface Props<T> {
  data: T[]
  columns: {
    key: keyof T
    label: string
  }[]
  selectable?: boolean
}

const props = withDefaults(defineProps<Props<T>>(), {
  selectable: false
})

const emit = defineEmits<{
  (e: 'select', item: T): void
  (e: 'row-click', item: T): void
}>()

const selectedItem = ref<T | null>(null)

const handleSelect = (item: T) => {
  selectedItem.value = item
  emit('select', item)
}
</script>

<template>
  <table class="generic-table">
    <thead>
      <tr>
        <th v-if="selectable">选择</th>
        <th v-for="col in columns" :key="String(col.key)">
          {{ col.label }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr 
        v-for="item in data" 
        :key="item.id"
        @click="emit('row-click', item)"
      >
        <td v-if="selectable">
          <input 
            type="radio" 
            :checked="selectedItem?.id === item.id"
            @change="handleSelect(item)"
          />
        </td>
        <td v-for="col in columns" :key="String(col.key)">
          {{ item[col.key] }}
        </td>
      </tr>
    </tbody>
  </table>
</template>
<!-- 使用泛型组件 -->
<script setup lang="ts">
import GenericTable from './GenericTable.vue'

interface User {
  id: number
  name: string
  email: string
  role: string
}

const users = ref<User[]>([
  { id: 1, name: '张三', email: 'zhang@example.com', role: 'admin' },
  { id: 2, name: '李四', email: 'li@example.com', role: 'user' }
])

const columns = [
  { key: 'name', label: '姓名' },
  { key: 'email', label: '邮箱' },
  { key: 'role', label: '角色' }
]
</script>

<template>
  <GenericTable 
    :data="users" 
    :columns="columns"
    selectable
    @row-click="(user) => console.log('点击:', user)"
  />
</template>

5.5 装饰器与 Vue 3

⚠️ 注意:Vue 3 推荐使用 <script setup> 语法糖,不推荐 Vue 2 的 Class 装饰器写法。以下仅供参考。

// 如果必须使用 Class 风格,可以使用 vue-property-decorator
import { Component, Vue, Prop, Watch, Emit } from 'vue-property-decorator'

@Component
export default class MyClassComponent extends Vue {
  @Prop({ type: String, required: true })
  title!: string

  @Prop({ default: 0 })
  count!: number

  private message = 'Hello'

  @Watch('count')
  onCountChange(newVal: number, oldVal: number) {
    console.log(`count 从 ${oldVal} 变为 ${newVal}`)
  }

  @Emit('update')
  handleUpdate() {
    return this.message
  }
}

6. Vue Router 4 路由管理

6.1 安装与基础配置

npm install vue-router@4
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

// 路由配置类型
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/HomeView.vue'),
    // 路由元信息
    meta: { title: '首页', requiresAuth: false }
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('@/views/AboutView.vue'),
    meta: { title: '关于我们' }
  },
  {
    path: '/user/:id',
    name: 'user-profile',
    component: () => import('@/views/UserProfile.vue'),
    props: true  // 将路由参数作为 props 传递
  },
  {
    path: '/products/:category/:id?',
    name: 'products',
    component: () => import('@/views/Products.vue'),
    props: (route) => ({
      category: route.params.category,
      id: route.params.id
    })
  }
]

// 创建路由实例
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    }
    return { top: 0 }
  }
})

// 路由守卫
router.beforeEach((to, from, next) => {
  // 设置页面标题
  document.title = to.meta.title as string || 'Vue App'

  // 权限检查
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next({ name: 'login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

export default router
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)

app.mount('#app')

6.2 嵌套路由

// router/index.ts
const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    component: () => import('@/layouts/DashboardLayout.vue'),
    meta: { requiresAuth: true },
    children: [
      {
        path: '',
        redirect: 'overview'
      },
      {
        path: 'overview',
        name: 'dashboard-overview',
        component: () => import('@/views/dashboard/Overview.vue'),
        meta: { title: '总览' }
      },
      {
        path: 'analytics',
        name: 'dashboard-analytics',
        component: () => import('@/views/dashboard/Analytics.vue'),
        meta: { title: '数据分析' }
      },
      {
        path: 'reports',
        name: 'dashboard-reports',
        component: () => import('@/views/dashboard/Reports.vue'),
        meta: { title: '报告' }
      }
    ]
  }
]
<!-- DashboardLayout.vue -->
<script setup lang="ts">
import { RouterView } from 'vue-router'
import DashboardSidebar from '@/components/DashboardSidebar.vue'
import DashboardHeader from '@/components/DashboardHeader.vue'
</script>

<template>
  <div class="dashboard-layout">
    <DashboardSidebar />
    <div class="dashboard-content">
      <DashboardHeader />
      <!-- 嵌套路由出口 -->
      <RouterView />
    </div>
  </div>
</template>

6.3 编程式导航

import { useRouter, useRoute } from 'vue-router'
import { onMounted } from 'vue'

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

// 导航到指定路径
router.push('/user/123')

// 导航到命名路由
router.push({ name: 'user-profile', params: { id: 123 } })

// 带查询参数
router.push({ path: '/search', query: { q: 'vue' } })

// 替换当前历史记录
router.replace('/home')

// 前进/后退
router.forward()  // 前进
router.back()     // 后退
router.go(-2)     // 后退两步

// 获取路由信息
onMounted(() => {
  const id = route.params.id        // 路由参数
  const query = route.query         // 查询参数
  const hash = route.hash           // 锚点
  const path = route.path           // 路径
  const name = route.name           // 路由名称

  console.log('当前路由:', { id, query, hash, path, name })
})

6.4 路由元信息与权限控制

// types/router.ts
import type { RouteRecordRaw } from 'vue-router'

interface Meta {
  title: string
  requiresAuth: boolean
  roles?: string[]
  keepAlive?: boolean
}

declare module 'vue-router' {
  interface RouteMeta extends Meta {}
}

const routes: RouteRecordRaw[] = [
  {
    path: '/admin',
    component: () => import('@/layouts/AdminLayout.vue'),
    meta: { requiresAuth: true, roles: ['admin'] },
    children: [
      {
        path: 'users',
        component: () => import('@/views/admin/Users.vue'),
        meta: { title: '用户管理', requiresAuth: true, roles: ['admin'] }
      }
    ]
  }
]
// router/guards.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

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

// 权限检查函数
const checkPermission = (to: RouteLocationNormalized) => {
  const userStore = useUserStore()

  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }

  if (to.meta.roles && !hasRole(to.meta.roles as string[])) {
    return { name: 'forbidden' }
  }

  return true
}

const hasRole = (roles: string[]): boolean => {
  const userStore = useUserStore()
  return roles.some(role => userStore.roles.includes(role))
}

router.beforeEach(async (to, from, next) => {
  // 设置标题
  document.title = to.meta.title || 'Vue App'

  // 权限检查
  const result = checkPermission(to)
  if (result === true) {
    next()
  } else {
    next(result)
  }
})

export default router

6.5 路由懒加载

// 方式一:动态导入(推荐)
const Home = () => import('./views/Home.vue')

// 方式二:带命名 chunk 的动态导入
const About = () => import(/* webpackChunkName: "about" */ './views/About.vue')

// 方式三:路由懒加载 + 预加载
const User = () => import(/* webpackChunkName: "user", webpackPrefetch: true */ './views/User.vue')

// 方式四:组合式函数封装
import { defineAsyncComponent } from 'vue'
import Loading from '@/components/Loading.vue'
import ErrorComponent from '@/components/ErrorComponent.vue'

export function useAsyncComponent(loader: () => Promise<any>) {
  return defineAsyncComponent({
    loader,
    loadingComponent: Loading,
    errorComponent: ErrorComponent,
    delay: 200,
    timeout: 3000
  })
}

7. Pinia 状态管理

7.1 安装与配置

npm install pinia
// stores/index.ts
import { createPinia } from 'pinia'

export const pinia = createPinia()

export * from './user'
export * from './cart'
export * from './settings'
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { pinia } from './stores'

const app = createApp(App)

app.use(pinia)

app.mount('#app')

7.2 定义 Store

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, UserCredentials } from '@/types'

export const useUserStore = defineStore('user', () => {
  // ===== State =====
  const user = ref<User | null>(null)
  const token = ref<string | null>(localStorage.getItem('token'))
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  // ===== Getters =====
  const isLoggedIn = computed(() => !!token.value && !!user.value)

  const userName = computed(() => user.value?.name || 'Guest')

  const userInitials = computed(() => {
    if (!user.value?.name) return 'G'
    return user.value.name
      .split(' ')
      .map(n => n[0])
      .join('')
      .toUpperCase()
  })

  const isAdmin = computed(() => user.value?.role === 'admin')

  // ===== Actions =====
  const login = async (credentials: UserCredentials) => {
    isLoading.value = true
    error.value = null

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      })

      if (!response.ok) {
        throw new Error('登录失败')
      }

      const data = await response.json()
      token.value = data.token
      user.value = data.user

      localStorage.setItem('token', data.token)

      return data.user
    } catch (e) {
      error.value = e instanceof Error ? e.message : '登录失败'
      throw e
    } finally {
      isLoading.value = false
    }
  }

  const logout = () => {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  const updateProfile = async (updates: Partial<User>) => {
    if (!user.value) return

    const response = await fetch(`/api/users/${user.value.id}`, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token.value}`
      },
      body: JSON.stringify(updates)
    })

    if (response.ok) {
      const updated = await response.json()
      user.value = { ...user.value, ...updated }
    }
  }

  return {
    // State
    user,
    token,
    isLoading,
    error,
    // Getters
    isLoggedIn,
    userName,
    userInitials,
    isAdmin,
    // Actions
    login,
    logout,
    updateProfile
  }
})
// stores/cart.ts - 使用 Options API 风格
import { defineStore } from 'pinia'

interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

interface CartState {
  items: CartItem[]
  isOpen: boolean
}

export const useCartStore = defineStore('cart', {
  state: (): CartState => ({
    items: [],
    isOpen: false
  }),

  getters: {
    totalItems: (state) => 
      state.items.reduce((sum, item) => sum + item.quantity, 0),

    totalPrice: (state) =>
      state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),

    isEmpty: (state) => state.items.length === 0
  },

  actions: {
    addItem(item: Omit<CartItem, 'quantity'>) {
      const existing = this.items.find(i => i.id === item.id)

      if (existing) {
        existing.quantity++
      } else {
        this.items.push({ ...item, quantity: 1 })
      }
    },

    removeItem(id: string) {
      const index = this.items.findIndex(i => i.id === id)
      if (index > -1) {
        this.items.splice(index, 1)
      }
    },

    updateQuantity(id: string, quantity: number) {
      const item = this.items.find(i => i.id === id)
      if (item) {
        if (quantity <= 0) {
          this.removeItem(id)
        } else {
          item.quantity = quantity
        }
      }
    },

    clearCart() {
      this.items = []
    },

    toggleCart() {
      this.isOpen = !this.isOpen
    }
  }
})

7.3 在组件中使用 Store

<!-- UserProfile.vue -->
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

const userStore = useUserStore()
const cartStore = useCartStore()

// 使用 storeToRefs 保持响应式
const { user, isAdmin, userName } = storeToRefs(userStore)

// 直接解构 actions(不需要 storeToRefs)
const { logout } = userStore

// 购物车
const { totalItems, totalPrice, isEmpty } = storeToRefs(cartStore)
const { addItem, removeItem, clearCart } = cartStore
</script>

<template>
  <div class="user-profile">
    <h1>欢迎, {{ userName }}</h1>
    <p v-if="isAdmin">管理员用户</p>

    <div class="user-info" v-if="user">
      <p>邮箱: {{ user.email }}</p>
      <p>角色: {{ user.role }}</p>
    </div>

    <button @click="logout">退出登录</button>

    <hr />

    <div class="cart-summary">
      <p>购物车共 {{ totalItems }} 件商品</p>
      <p>总价: ¥{{ totalPrice.toFixed(2) }}</p>
      <button v-if="!isEmpty" @click="clearCart">清空购物车</button>
    </div>
  </div>
</template>

7.4 Store 持久化

// stores/plugins/persist.ts
import { watch } from 'pinia'
import type { PiniaPluginContext } from 'pinia'

interface PersistOptions {
  key?: string
  storage?: Storage
  paths?: string[]
}

declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    persist?: boolean | PersistOptions
  }
}

export function persistPlugin({ store, options }: PiniaPluginContext) {
  const persistOptions = options.persist

  if (!persistOptions) return

  const config = typeof persistOptions === 'boolean' 
    ? { key: store.$id }
    : { key: store.$id, ...persistOptions }

  const { key, storage = localStorage, paths } = config

  // 恢复状态
  const savedState = storage.getItem(key)
  if (savedState) {
    store.$patch(JSON.parse(savedState))
  }

  // 监听变化并保存
  watch(
    () => store.$state,
    (state) => {
      const toSave = paths 
        ? paths.reduce((acc, path) => ({ ...acc, [path]: state[path] }), {})
        : state
      storage.setItem(key, JSON.stringify(toSave))
    },
    { deep: true }
  )
}
// stores/index.ts
import { createPinia } from 'pinia'
import { persistPlugin } from './plugins/persist'

export const pinia = createPinia()
pinia.use(persistPlugin)

export { useUserStore } from './user'
export { useCartStore } from './cart'
// 使用持久化
export const useUserStore = defineStore('user', () => {
  const token = ref<string | null>(null)
  // ...
  return { token }
}, {
  persist: {
    key: 'user-store',
    paths: ['token', 'user']  // 只持久化这些字段
  }
})

7.5 Store 间交互

// stores/order.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'

export const useOrderStore = defineStore('order', () => {
  const userStore = useUserStore()
  const cartStore = useCartStore()

  const orders = ref<Order[]>([])

  const createOrder = async () => {
    if (!userStore.isLoggedIn) {
      throw new Error('请先登录')
    }

    const order: Order = {
      id: generateId(),
      userId: userStore.user!.id,
      items: [...cartStore.items],
      total: cartStore.totalPrice,
      status: 'pending',
      createdAt: new Date().toISOString()
    }

    orders.push(order)
    cartStore.clearCart()

    return order
  }

  return { orders, createOrder }
})

8. 组件化开发进阶

8.1 插槽(Slots)

<!-- BaseCard.vue -->
<script setup lang="ts">
defineProps<{
  title?: string
  shadow?: 'none' | 'sm' | 'md' | 'lg'
}>()
</script>

<template>
  <div class="card" :class="`shadow-${shadow || 'md'}`">
    <div v-if="$slots.header || title" class="card-header">
      <slot name="header">
        <h3>{{ title }}</h3>
      </slot>
    </div>

    <div class="card-body">
      <slot />
    </div>

    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>
<!-- 使用插槽 -->
<BaseCard title="用户信息">
  <!-- 默认插槽 -->
  <p>用户名:张三</p>
  <p>邮箱:zhang@example.com</p>

  <!-- 具名插槽 -->
  <template #footer>
    <button>编辑</button>
    <button>删除</button>
  </template>
</BaseCard>

<!-- 作用域插槽 -->
<!-- BaseList.vue -->
<script setup lang="ts">
defineProps<{
  items: string[]
}>()

defineEmits<{
  (e: 'select', item: string): void
}>()
</script>

<template>
  <ul>
    <li 
      v-for="(item, index) in items" 
      :key="index"
      @click="$emit('select', item)"
    >
      <!-- 作用域插槽:传递数据给父组件 -->
      <slot :item="item" :index="index" />
    </li>
  </ul>
</template>
<!-- 使用作用域插槽 -->
<BaseList :items="['苹果', '香蕉', '橙子']" @select="handleSelect">
  <template #default="{ item, index }">
    <span class="item">{{ index + 1 }}. {{ item }}</span>
  </template>
</BaseList>

8.2 Teleport 传送门

<!-- Modal.vue -->
<script setup lang="ts">
import { Teleport } from 'vue'

defineProps<{
  show: boolean
}>()

defineEmits<{
  (e: 'close'): void
}>()
</script>

<template>
  <Teleport to="body">
    <Transition name="modal">
      <div v-if="show" class="modal-overlay" @click.self="$emit('close')">
        <div class="modal-content">
          <slot />
          <button @click="$emit('close')">关闭</button>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<style scoped>
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 2rem;
  border-radius: 8px;
}

/* 过渡动画 */
.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s ease;
}

.modal-enter-from,
.modal-leave-to {
  opacity: 0;
}
</style>

8.3 Suspense 异步组件

<!-- AsyncComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'

// 异步加载的数据
const data = await fetchData()
const result = ref(data)
</script>

<template>
  <div>{{ result }}</div>
</template>
<!-- 使用 Suspense -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from '@/components/LoadingSpinner.vue'
import ErrorBoundary from '@/components/ErrorBoundary.vue'

const AsyncUserList = defineAsyncComponent(() => 
  import('./UserList.vue')
)
</script>

<template>
  <Suspense>
    <template #default>
      <AsyncUserList />
    </template>

    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>

  <!-- 带错误处理 -->
  <ErrorBoundary>
    <Suspense>
      <template #default>
        <AsyncUserList />
      </template>
      <template #fallback>
        <LoadingSpinner />
      </template>
    </Suspense>
  </ErrorBoundary>
</template>

8.4 自定义指令

// directives/focus.ts
import type { Directive, DirectiveBinding } from 'vue'

export const vFocus: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    // 聚焦
    el.focus()

    // 支持 v-focus:method.arg
    if (binding.arg === 'method') {
      binding.value(el)
    }
  }
}

// v-permission 指令示例
export const vPermission: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const userStore = useUserStore()
    const requiredPermission = binding.value

    if (!userStore.hasPermission(requiredPermission)) {
      el.style.display = 'none'
      // 或者移除元素
      // el.remove()
    }
  }
}
// main.ts 注册全局指令
import { createApp } from 'vue'
import { vFocus, vPermission } from '@/directives'

const app = createApp(App)

app.directive('focus', vFocus)
app.directive('permission', vPermission)

app.mount('#app')
<!-- 使用自定义指令 -->
<script setup lang="ts">
// 局部注册
import { vFocus } from '@/directives/focus'
</script>

<template>
  <input v-focus />
  <button v-permission="'admin'">管理面板</button>
</template>

9. 生命周期与Hooks

9.1 生命周期图解

┌─────────────────────────────────────────────────────────────────┐
│                     Vue 3 生命周期钩子                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   创建阶段 (Creation)                                           │
│   ──────────────────                                            │
│   beforeCreate  ──→  setup()  ──→  created                    │
│         ↑                                                         │
│   setup() 是 Composition API 的入口                              │
│                                                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   挂载阶段 (Mounting)                                            │
│   ──────────────────                                            │
│   onBeforeMount  ──→  编译模板 → 生成虚拟DOM  ──→  onMounted    │
│                                                                  │
│   onBeforeMount  :  DOM 即将挂载,可访问 this                     │
│   onMounted      :  DOM 已挂载,可进行 DOM 操作                   │
│                                                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   更新阶段 (Updating)                                            │
│   ──────────────────                                            │
│   onBeforeUpdate  ──→  虚拟DOM对比(Patch)  ──→  onUpdated     │
│                                                                  │
│   onBeforeUpdate  :  数据变化后,DOM 更新前                       │
│   onUpdated       :  DOM 更新完成                                │
│                                                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   卸载阶段 (Unmounting)                                          │
│   ────────────────────                                           │
│   onBeforeUnmount  ──→  卸载组件  ──→  onUnmounted            │
│                                                                  │
│   onBeforeUnmount  :  实例仍可用,可清理定时器/事件监听           │
│   onUnmounted      :  完全卸载,清理完成                         │
│                                                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   错误处理                                                       │
│   ──────────                                                    │
│   onErrorCaptured  :  子组件错误时触发                           │
│                                                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   调试钩子 (Dev Only)                                            │
│   ─────────────────                                             │
│   onRenderTracked  :  响应式依赖被追踪时                         │
│   onRenderTriggered :  响应式依赖触发更新时                      │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

9.2 组合式 API 生命周期钩子

import {
  onMounted,
  onBeforeMount,
  onUpdated,
  onBeforeUpdate,
  onUnmounted,
  onBeforeUnmount,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered
} from 'vue'

export default {
  setup() {
    // ===== 挂载阶段 =====
    onBeforeMount(() => {
      console.log('组件即将挂载')
    })

    onMounted(() => {
      console.log('组件已挂载')
      // 获取 DOM
      const el = document.querySelector('.my-element')
      // 初始化第三方库
      // initChart(el)
    })

    // ===== 更新阶段 =====
    onBeforeUpdate(() => {
      console.log('组件即将更新')
    })

    onUpdated(() => {
      console.log('组件已更新')
    })

    // ===== 卸载阶段 =====
    onBeforeUnmount(() => {
      console.log('组件即将卸载')
      // 清理定时器
      clearInterval(timerId)
      // 移除事件监听
      window.removeEventListener('resize', handleResize)
    })

    onUnmounted(() => {
      console.log('组件已卸载')
    })

    // ===== 错误处理 =====
    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err)
      console.error('错误信息:', info)
      return false  // 阻止错误传播
    })

    // ===== 调试钩子 (开发模式) =====
    onRenderTracked((event) => {
      console.log('依赖被追踪:', event)
      // event: { target, type, key, newValue, oldValue }
    })

    onRenderTriggered((event) => {
      console.log('依赖触发更新:', event)
    })

    return {}
  }
}

9.3 常用 Hooks 封装

// composables/useEventListener.ts
import { onMounted, onUnmounted, Ref } from 'vue'

export function useEventListener(
  target: Window | HTMLElement | Ref<Window | HTMLElement | null>,
  event: string,
  handler: EventListener
) {
  const addEvent = () => {
    const el = unref(target)
    el?.addEventListener(event, handler)
  }

  const removeEvent = () => {
    const el = unref(target)
    el?.removeEventListener(event, handler)
  }

  onMounted(addEvent)
  onUnmounted(removeEvent)
}
// composables/useInterval.ts
import { ref, onUnmounted } from 'vue'

export function useInterval(callback: () => void, delay: number) {
  const intervalId = ref<number | null>(null)

  const start = () => {
    if (intervalId.value === null) {
      intervalId.value = window.setInterval(callback, delay)
    }
  }

  const stop = () => {
    if (intervalId.value !== null) {
      window.clearInterval(intervalId.value)
      intervalId.value = null
    }
  }

  onUnmounted(stop)

  return { start, stop }
}
// composables/useResizeObserver.ts
import { ref, onMounted, onUnmounted, watch, Ref } from 'vue'
import type { ResizeObserverEntry } from '@vueuse/core'

export function useResizeObserver(
  target: Ref<HTMLElement | null>,
  callback: (entries: ResizeObserverEntry[]) => void
) {
  let observer: ResizeObserver | null = null

  const observe = () => {
    if (target.value && observer) {
      observer.observe(target.value)
    }
  }

  const disconnect = () => {
    observer?.disconnect()
  }

  onMounted(() => {
    observer = new ResizeObserver(callback)
    observe()
  })

  onUnmounted(disconnect)

  watch(target, (newTarget, oldTarget) => {
    if (oldTarget) {
      observer?.unobserve(oldTarget)
    }
    if (newTarget) {
      observer?.observe(newTarget)
    }
  })
}
// composables/useIntersectionObserver.ts
import { ref, onMounted, onUnmounted, Ref } from 'vue'

export function useIntersectionObserver(
  target: Ref<HTMLElement | null>,
  options: IntersectionObserverInit = {}
) {
  const isIntersecting = ref(false)
  const intersectionRatio = ref(0)

  let observer: IntersectionObserver | null = null

  const handleIntersect = (entries: IntersectionObserverEntry[]) => {
    const entry = entries[0]
    isIntersecting.value = entry.isIntersecting
    intersectionRatio.value = entry.intersectionRatio
  }

  onMounted(() => {
    observer = new IntersectionObserver(handleIntersect, options)
    if (target.value) {
      observer.observe(target.value)
    }
  })

  onUnmounted(() => {
    observer?.disconnect()
  })

  return { isIntersecting, intersectionRatio }
}

10. Composition API vs Options API 对比

10.1 代码组织对比

<!-- Options API 写法 -->
<script>
export default {
  data() {
    return {
      count: 0,
      message: 'Hello',
      users: [],
      loading: false
    }
  },

  computed: {
    doubledCount() {
      return this.count * 2
    },
    filteredUsers() {
      return this.users.filter(u => u.active)
    }
  },

  methods: {
    increment() {
      this.count++
    },
    async fetchUsers() {
      this.loading = true
      try {
        const res = await fetch('/api/users')
        this.users = await res.json()
      } finally {
        this.loading = false
      }
    }
  },

  watch: {
    count(newVal) {
      console.log('count 变化:', newVal)
    }
  },

  mounted() {
    this.fetchUsers()
  }
}
</script>
<!-- Composition API 写法 (<script setup>) -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

// 相关逻辑放在一起
const count = ref(0)
const doubledCount = computed(() => count.value * 2)
const increment = () => count.value++

// 用户相关逻辑独立成块
const users = ref<User[]>([])
const loading = ref(false)
const filteredUsers = computed(() => users.value.filter(u => u.active))

const fetchUsers = async () => {
  loading.value = true
  try {
    const res = await fetch('/api/users')
    users.value = await res.json()
  } finally {
    loading.value = false
  }
}

// 生命周期钩子
watch(count, (newVal) => {
  console.log('count 变化:', newVal)
})

onMounted(() => {
  fetchUsers()
})
</script>

10.2 逻辑复用对比

// Options API - Mixins
// mixins/counterMixin.js
export const counterMixin = {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

// 使用
export default {
  mixins: [counterMixin]
  // 可能存在命名冲突、来源不清晰的问题
}
// Composition API - Composables
// composables/useCounter.ts
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  return { count, increment }
}

// 使用
<script setup>
import { useCounter } from '@/composables/useCounter'

// 每个组件都是独立的实例
const { count, increment } = useCounter(10)
const { count: cartCount } = useCounter(0)

// 来源清晰,可读性强
</script>

10.3 性能对比

场景Options APIComposition API说明
首屏加载正常略优Composition API 减少约 10% 包体积
响应式性能相同相同底层都是 Proxy
大列表渲染正常更优可精确控制更新范围
代码分割需整体引入可按需导入 hooksTree-shaking 更友好
TypeScript 支持一般优秀更好的类型推断

11. 实战技巧与最佳实践

11.1 响应式数据最佳实践

// ❌ 避免:将普通对象直接赋值给 ref
const obj = { name: '张三', age: 25 }
const state = ref(obj)
obj.age = 30  // 不会触发更新!

// ✅ 正确:使用 reactive 或 .value
const state = ref({ name: '张三', age: 25 })
state.value.age = 30

// 或使用 reactive
const state = reactive(obj)
state.age = 30  // 响应式更新

// ❌ 避免:解构响应式对象丢失响应式
const { name, age } = state

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

// ❌ 避免:数组索引直接赋值
items.value[0] = newItem  // 不响应!

// ✅ 正确:使用 splice 或整个替换
items.value.splice(0, 1, newItem)
// 或
items.value = [...items.value.slice(0, 0), newItem, ...items.value.slice(1)]

// ❌ 避免:给响应式对象的属性重新赋值整个对象
state.user = { name: '李四' }  // 不响应

// ✅ 正确:使用 Object.assign
Object.assign(state.user, { name: '李四' })

11.2 性能优化技巧

// 1. 使用 shallowRef 优化大对象
import { shallowRef, triggerRef } from 'vue'

// 适用于数据量大的场景
const bigData = shallowRef<DataType>([])
const updateBigData = async () => {
  bigData.value = await fetchLargeData()
  triggerRef(bigData)  // 手动触发更新
}

// 2. 使用 markRaw 标记不需要响应式的数据
import { reactive, markRaw } from 'vue'

const chart = markRaw(new ChartInstance())
const state = reactive({
  data: [1, 2, 3],
  chart: chart  // 不会被代理
})

// 3. 使用 readonly 保护状态
import { readonly, reactive } from 'vue'

const state = reactive({
  count: 0
})
const readonlyState = readonly(state)

// 4. 合理使用 watchEffect 的 flush 选项
watchEffect(() => {
  // 默认 'pre' - 组件更新前
}, { flush: 'post' })  // 组件更新后执行

// 5. 使用 v-memo 优化列表渲染
<div v-for="item in items" :key="item.id" v-memo="[item.id, item.name]">
  <!-- 只有 item.id 或 item.name 变化时才更新 -->
</div>

11.3 TypeScript 最佳实践

// 1. 善用泛型约束
function fetchData<T extends { id: string | number }>(id: T['id']): Promise<T> {
  return fetch(`/api/${id}`).then(res => res.json())
}

// 2. 使用 satisfies 运算符(TypeScript 4.9+)
const config = {
  db: { host: 'localhost', port: 5432 },
  cache: { maxAge: 3600 }
} satisfies Config

// 3. 组件 Props 类型
interface Props {
  // 使用泛型
  items: string[]
  // 使用联合类型
  variant: 'primary' | 'secondary' | 'danger'
  // 使用函数类型
  onClick: (e: MouseEvent) => void
  // 使用可选链
  user?: { name: string }
}

// 4. 暴露类型给父组件
import type { ExtractProps, ExtractEmits } from 'vue'

type ButtonProps = ExtractProps<typeof import('./Button.vue')>
type ButtonEmits = ExtractEmits<typeof import('./Button.vue')>

11.4 组件设计模式

// 1. 展示组件 vs 容器组件
// PresentationalComponent.vue - 只负责展示
<script setup lang="ts">
defineProps<{
  title: string
  content: string
}>()
</script>

// ContainerComponent.vue - 负责数据和逻辑
<script setup lang="ts">
import { ref } from 'vue'
import PresentationalComponent from './PresentationalComponent.vue'

const data = ref({ title: '标题', content: '内容' })
</script>

<template>
  <PresentationalComponent 
    :title="data.title"
    :content="data.content"
  />
</template>

// 2. 高阶组件模式 (Render Props)
interface Props {
  render: (scope: { items: string[] }) => any
}

defineProps<Props>()

// 3. 依赖注入模式
// provide/inject + symbol key
const STORAGE_KEY = Symbol('storage')

// 在父组件
import { provide } from 'vue'
provide(STORAGE_KEY, {
  get: () => localStorage.getItem('key'),
  set: (val: string) => localStorage.setItem('key', val)
})

// 在子组件
import { inject } from 'vue'
const storage = inject(STORAGE_KEY)

12. 性能优化策略

12.1 首屏加载优化

// vite.config.ts
export default defineConfig({
  build: {
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['vue', 'vue-router', 'pinia'],
          'element-plus': ['element-plus']
        }
      }
    },
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  // 预加载
  preload: {
    include: ['src/**/*.ts', 'src/**/*.vue']
  }
})
<!-- index.html - 预连接关键资源 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://api.example.com">

12.2 路由懒加载

// router/index.ts
const routes = [
  // 使用动态导入
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
  },
  {
    path: '/dashboard',
    component: () => import(/* webpackChunkName: "dashboard", webpackPrefetch: true */ '@/views/Dashboard.vue')
  }
]

12.3 虚拟列表

<!-- VirtualList.vue -->
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'

const props = defineProps<{
  items: any[]
  itemHeight: number
  visibleCount?: number
}>()

const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)
const containerHeight = ref(0)

const visibleCount = computed(() => 
  props.visibleCount || Math.ceil(containerHeight.value / props.itemHeight) + 2
)

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

const visibleItems = computed(() => 
  props.items.slice(startIndex.value, startIndex.value + visibleCount.value)
)

const offsetY = computed(() => 
  startIndex.value * props.itemHeight
)

const totalHeight = computed(() => 
  props.items.length * props.itemHeight
)

const handleScroll = (e: Event) => {
  scrollTop.value = (e.target as HTMLElement).scrollTop
}

onMounted(() => {
  if (containerRef.value) {
    containerHeight.value = containerRef.value.clientHeight
  }
})
</script>

<template>
  <div 
    ref="containerRef" 
    class="virtual-list" 
    @scroll="handleScroll"
  >
    <div class="virtual-list-spacer" :style="{ height: totalHeight + 'px' }">
      <div 
        class="virtual-list-content"
        :style="{ transform: `translateY(${offsetY}px)` }"
      >
        <div 
          v-for="(item, index) in visibleItems" 
          :key="item.id"
          class="virtual-list-item"
          :style="{ height: itemHeight + 'px' }"
        >
          <slot :item="item" :index="startIndex + index" />
        </div>
      </div>
    </div>
  </div>
</template>

12.4 依赖按需加载

// 完整导入
import { ref, computed, watch, onMounted } from 'vue'

// 按需导入 - 更好的 Tree-shaking
import ref from 'vue'
import computed from 'vue'
// 或使用自动导入插件

12.5 图片优化

<!-- ImageWithLazyLoad.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const props = defineProps<{
  src: string
  alt: string
  placeholder?: string
}>()

const imageRef = ref<HTMLImageElement>()
const isLoaded = ref(false)
const error = ref(false)

const observer = ref<IntersectionObserver | null>(null)

onMounted(() => {
  observer.value = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        loadImage()
        observer.value?.unobserve(entry.target)
      }
    })
  })

  if (imageRef.value) {
    observer.value.observe(imageRef.value)
  }
})

const loadImage = () => {
  const img = new Image()
  img.onload = () => {
    isLoaded.value = true
  }
  img.onerror = () => {
    error.value = true
  }
  img.src = props.src
}
</script>

<template>
  <div ref="imageRef" class="image-wrapper">
    <img 
      v-if="isLoaded && !error" 
      :src="src" 
      :alt="alt"
      class="loaded"
    />
    <img 
      v-else-if="placeholder" 
      :src="placeholder" 
      class="placeholder"
    />
    <div v-else class="loading"></div>
  </div>
</template>

13. 常见问题与排错

13.1 响应式问题

问题现象原因解决方案
修改数据视图不更新数组/对象直接用索引赋值使用 splice() 或完整替换
解构后数据不响应丢失响应式代理使用 toRefs 包装
深层对象不响应对象属性重新赋值使用 reactiveObject.assign
props 修改报错props 本身只读在组件内部创建本地副本
// 排错命令
import { isRef, isReactive, isProxy } from 'vue'

console.log(isRef(count))      // 检查是否为 ref
console.log(isReactive(state)) // 检查是否为 reactive
console.log(isProxy(state))   // 检查是否为代理对象

13.2 TypeScript 问题

问题解决方案
defineProps 类型错误确保使用 TypeScript 3.8+,使用 withDefaults 提供默认值
defineEmits 类型推断失败显式声明 emits 类型
组件实例类型使用 InstanceType<typeof Component>
template ref 类型使用泛型 <HTMLElement>
// 常见 TS 错误修复

// 错误:Cannot find name 'User'
// 解决:确保导入类型
import type { User } from '@/types'

// 错误:Property 'xxx' does not exist
// 解决:添加类型断言或使用 satisfies
const data = res.json() as User[]

// 错误:Type 'Ref<string>' is not assignable to type 'string'
// 解决:在模板外使用 .value

13.3 性能问题排查

// 使用 Vue DevTools Profiler
// 1. 安装 Vue DevTools 浏览器扩展
// 2. 打开开发者工具 → Vue 面板
// 3. 点击 Performance 录制

// 添加调试标记
import { devtools } from 'vue'

devtools.emit('custom-inspector-action', { name: 'myAction' })

// 检查不必要的重新渲染
import { renderTracked, renderTriggered } from 'vue'

watch(someRef, () => {
  console.log('重新渲染')
}, { onTrigger: (e) => console.log('触发:', e) })

13.4 常见报错与解决

错误信息原因解决方案
Cannot read property of undefined访问 undefined 属性使用可选链 ?.
Promise is not defined异步在同步上下文中确保 await/async 正确使用
Module not found路径别名未配置检查 vite/tsconfig 配置
Hydration completed but contains mismatchesSSR 与客户端不一致检查初始数据生成
Too many re-renders渲染中修改状态使用防抖/节流,避免循环更新
// 调试工具
import { dumpRef, dumpReactive } from 'vue'

// 打印 ref 值
console.log('count:', dumpRef(countRef))

// 打印 reactive 对象
console.log('state:', dumpReactive(stateReactive))

14. 从 Vue 2 迁移指南

14.1 主要变化速查

Vue 2Vue 3说明
Vue.setProxy直接赋值即可
Vue.deletedelete使用原生 delete
$childrenuseTemplateRef获取子组件引用
filters函数调用移除,推荐方法或计算属性
mixinsComposables更清晰的复用方式
$listeners$attrs合并到 $attrs
scopedSlotsv-slot统一插槽语法

14.2 Vue 2 迁移示例

// Vue 2 写法
export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  computed: {
    upperMessage() {
      return this.message.toUpperCase()
    }
  },
  methods: {
    greet() {
      this.message = 'Hi'
    }
  },
  mounted() {
    console.log('mounted')
  }
}
// Vue 3 写法
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

const message = ref('Hello')
const upperMessage = computed(() => message.value.toUpperCase())

const greet = () => {
  message.value = 'Hi'
}

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

14.3 迁移工具

# 使用 Vue 2to3 迁移工具
npm install -g @vue-migration/vue-codemod

# 运行迁移
npx vue-codemod src/**/*.{vue,js,ts}

# 或者使用 Volar 的 Vue Language Features
# 在 VS Code 中安装 Volar 扩展获得更好的 Vue 3 支持

14.4 兼容性配置

// vite.config.ts
export default defineConfig({
  plugins: [
    vue({
      // 启用 Vue 3 兼容性模式
      compatibility: {
        COMPAT: true
      }
    })
  ]
})

// 全局配置兼容 Vue 2 API
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'

Vue.use(VueCompositionAPI)

15. 附录:快速命令速查表

15.1 项目创建命令

# 创建项目
npm create vue@latest my-app          # 完整项目(推荐)
npm create vite@latest my-app -- --template vue-ts  # 基础模板

# 安装依赖
npm install
npm install vue-router@4 pinia
npm install -D @vue/tsconfig

15.2 常用脚本

# 开发
npm run dev        # 启动开发服务器
npm run dev -- --port 3000  # 指定端口

# 构建
npm run build      # 生产构建
npm run build -- --mode staging  # 指定环境

# 代码检查
npm run lint       # ESLint 检查
npm run lint:fix   # 自动修复
npm run type-check # TypeScript 类型检查

# 预览
npm run preview    # 预览生产构建

15.3 Vue 3 Composition API 速查

// 响应式
ref()              // 基本类型响应式
reactive()         // 对象响应式
toRef()            // 提取响应式属性
toRefs()           // 解构保持响应式
readonly()         // 只读代理
shallowRef()       // 浅层响应式

// 计算
computed()         // 计算属性
watchEffect()      // 立即执行并收集依赖
watch()            // 精确监听

// 生命周期
onMounted()        // 挂载完成
onUpdated()        // 更新完成
onUnmounted()      // 卸载完成
onBeforeMount()    // 挂载前
onBeforeUpdate()   // 更新前
onBeforeUnmount()  // 卸载前
onErrorCaptured()  // 错误捕获

// 工具
nextTick()         // 等待更新完成
isRef()            // 检查 ref
isReactive()       // 检查 reactive
isProxy()          // 检查代理
isReadonly()       // 检查只读

// 组件
defineProps()      // 定义 props
defineEmits()      // 定义 emits
defineExpose()     // 暴露方法
useSlots()         // 使用插槽
useAttrs()         // 使用 attrs

15.4 VS Code 推荐配置

// .vscode/extensions.json
{
  "recommendations": [
    "vue.volar",
    "vue.vscode-typescript-vue-plugin",
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode"
  ]
}
// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "[vue]": {
    "editor.defaultFormatter": "Vue.volar"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "typescript.preferences.importModuleSpecifier": "non-relative",
  "vue.features.templatesLanguageServer": true
}
© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容