Vue3开发指南

Vue3 开发指南(完整版)


目录

  1. 环境搭建
  2. 项目创建与结构
  3. 响应式系统
  4. 模板语法
  5. 组合式 API 深入
  6. 组件系统
  7. TypeScript 集成
  8. Vue Router 4
  9. Pinia 状态管理
  10. 内置组件与指令
  11. 生态工具推荐
  12. 项目最佳实践
  13. 常见问题与调试

1. 环境搭建

1.1 Node.js

Vue3 需要 Node.js 18+(推荐 20 LTS)

bash
# 查看版本
node -v   # 应 ≥ v18
npm -v    # 应 ≥ v9

# 推荐使用 nvm-windows 或 fnm 管理 Node 版本

1.2 包管理器选择

工具 特点 安装
pnpm(推荐) 快、省磁盘、严格依赖 npm i -g pnpm
npm 官方自带 内置
yarn Facebook 出品 npm i -g yarn

1.3 VSCode 插件

  • Vue - Official(必装):语法高亮、TypeScript 支持、模板智能提示
  • TypeScript Vue Plugin (Volar):已合并到 Vue - Official
  • ESLint + Prettier:代码规范
  • Tailwind CSS IntelliSense(如使用 Tailwind)

2. 项目创建与结构

2.1 创建项目(推荐 create-vue)

bash
# npm
npm create vue@latest

# pnpm(推荐)
pnpm create vue@latest

# 交互式选项
✔ Project name: my-vue3-app
✔ TypeScript? Yes
✔ JSX Support? No
✔ Vue Router? Yes
✔ Pinia? Yes
✔ Vitest? Yes
✔ ESLint? Yes
✔ Prettier? Yes

2.2 Vite 项目标准结构

my-vue3-app/
├── index.html              # 入口 HTML(Vite 根)
├── package.json
├── vite.config.ts          # Vite 配置
├── tsconfig.json           # TS 配置
├── env.d.ts                # 环境类型声明
├── public/                 # 静态资源(不经过编译,直接复制)
│   └── favicon.ico
└── src/
    ├── main.ts             # 应用入口
    ├── App.vue             # 根组件
    ├── router/
    │   └── index.ts        # 路由配置
    ├── stores/             # Pinia 状态管理
    │   └── counter.ts
    ├── views/              # 页面级组件
    │   ├── HomeView.vue
    │   └── AboutView.vue
    ├── components/         # 可复用组件
    │   ├── HelloWorld.vue
    │   └── common/         # 通用组件子目录
    ├── composables/        # 组合式函数(hooks)
    │   └── useCounter.ts
    ├── api/                # API 请求封装
    │   └── request.ts
    ├── types/              # TypeScript 类型定义
    │   └── index.ts
    ├── utils/              # 工具函数
    │   └── format.ts
    ├── assets/             # 需要编译的资源(图片、CSS)
    │   └── main.css
    └── styles/             # 全局样式
        └── variables.css

2.3 main.ts 入口文件详解

typescript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'

const app = createApp(App)

app.use(createPinia())   // 注册状态管理
app.use(router)           // 注册路由

app.mount('#app')         // 挂载到 index.html 的 <div id="app">

3. 响应式系统

Vue3 基于 Proxy 实现响应式,解决了 Vue2 Object.defineProperty 的诸多限制。

3.1 ref() — 基本类型的响应式

typescript
import { ref } from 'vue'

const count = ref(0)           // number
const name = ref('Andy')       // string
const isActive = ref(true)     // boolean
const list = ref([1, 2, 3])   // array
const user = ref({ name: 'Andy', age: 25 })  // object

// 读取和修改需要 .value
console.log(count.value)  // 0
count.value++              // 在 <script> 中修改
name.value = 'Bob'

// ref 包装对象(在模板中自动解包,无需 .value)

3.2 reactive() — 对象类型的响应式

typescript
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: 'Andy', age: 25 },
  items: ['a', 'b', 'c']
})

// 直接访问,无需 .value
state.count++
state.user.name = 'Bob'

// ⚠️ reactive() 的限制
// 1. 只对对象类型有效
// 2. 不能整体替换(失去响应式)
// state = { count: 1 }  // ❌ 错误!会断开响应式连接
// 3. 解构会丢失响应式
// const { count } = state  // ❌ count 是普通数字

3.3 ref vs reactive 选择

场景 推荐 原因
基本类型(number, string, boolean) ref() reactive 不支持基本类型
需要整体替换的对象 ref() reactive 不支持整体替换
表单数据 reactive() 语义更自然
复杂嵌套对象 reactive() 深层自动响应式
组合式函数返回值 ref() 避免解构丢失响应式
通用建议 ref() 优先 官方风格指南推荐,TypeScript 更友好

3.4 computed() — 计算属性

typescript
import { ref, computed } from 'vue'

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

// 只读计算属性
const fullName = computed(() => lastName.value + firstName.value)
// 结果: "张三"

// 可写计算属性
const fullNameEdit = computed({
  get: () => lastName.value + firstName.value,
  set: (val) => {
    lastName.value = val[0]
    firstName.value = val.slice(1)
  }
})

fullNameEdit.value = '李四'
// lastName.value = '李', firstName.value = '四'

// 计算属性 vs 方法的区别:
// - computed 有缓存,依赖不变不会重新计算
// - 方法每次渲染都执行

3.5 watch() — 侦听器

typescript
import { ref, watch } from 'vue'

const count = ref(0)
const name = ref('Andy')

// 侦听单个 ref
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

// 侦听多个源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('count 或 name 变了')
})

// 侦听 reactive 对象属性(需用 getter 函数)
const state = reactive({ user: { name: 'Andy' } })
watch(
  () => state.user.name,
  (newName) => console.log('name 变了:', newName)
)

// 深度侦听
watch(
  () => state.user,
  (newVal) => console.log('user 深层属性变了'),
  { deep: true }
)

// 立即执行
watch(count, (val) => console.log(val), { immediate: true })

// 侦听 reactive 整个对象(自动 deep)
watch(state, (newVal) => console.log('state 变了'))

// ⚡ watchEffect() — 自动追踪依赖
import { watchEffect } from 'vue'

watchEffect(() => {
  // 自动追踪 count.value 和 name.value
  console.log(`count=${count.value}, name=${name.value}`)
})
// 立即执行一次,之后依赖变化自动重新执行

// 停止侦听
const stop = watch(count, callback)
stop()  // 手动停止

3.6 响应式工具函数

typescript
import { isRef, unref, toRef, toRefs, toRaw, markRaw } from 'vue'

const count = ref(0)

isRef(count)           // true — 判断是否为 ref
unref(count)           // 0 — ref 返回 .value,否则返回本身
toRef(state, 'name')    // 将 reactive 对象的属性转为 ref
toRefs(state)           // 将 reactive 对象所有属性转为 ref(用于解构保持响应式)
toRaw(state)            // 返回 reactive 代理的原始对象
markRaw(obj)            // 标记对象永不转为响应式(用于大数据或第三方实例)

4. 模板语法

4.1 文本插值与表达式

vue
<template>
  <!-- 文本插值 -->
  <span>{{ message }}</span>

  <!-- JS 表达式(单条语句,不能是语句块或声明) -->
  <span>{{ count + 1 }}</span>
  <span>{{ isActive ? '开' : '关' }}</span>
  <span>{{ list.length > 0 ? `${list.length}条` : '无数据' }}</span>

  <!-- ❌ 以下写法不行 -->
  <!-- {{ if (true) { return 'yes' } }} -->
  <!-- {{ let a = 1 }} -->
</template>

4.2 指令一览

vue
<template>
  <!-- v-bind — 动态绑定属性 -->
  <img v-bind:src="imageUrl" :alt="title" />
  <div :class="{ active: isActive, 'text-red': hasError }"></div>
  <div :style="{ color: textColor, fontSize: size + 'px' }"></div>
  <!-- 简写 :attr -->

  <!-- v-on — 事件监听 -->
  <button v-on:click="handleClick">点击</button>
  <button @click="count++">直接表达式</button>
  <form @submit.prevent="onSubmit">  <!-- 事件修饰符 -->
  <!-- 简写 @event -->

  <!-- v-model — 双向绑定 -->
  <input v-model="text" />
  <!-- 等价于 :value="text" @input="text = $event.target.value" -->

  <!-- v-if / v-else-if / v-else — 条件渲染 -->
  <div v-if="type === 'A'">A类型</div>
  <div v-else-if="type === 'B'">B类型</div>
  <div v-else>其他类型</div>

  <!-- v-show — 基于 CSS display 控制显隐 -->
  <div v-show="isVisible">显示/隐藏(DOM 始终存在)</div>

  <!-- v-for — 列表渲染 -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }} - {{ item.name }}
  </li>
  <!-- 遍历对象 -->
  <li v-for="(value, key, index) in obj" :key="key">
    {{ key }}: {{ value }}
  </li>

  <!-- v-once — 只渲染一次 -->
  <span v-once>{{ initValue }}</span>

  <!-- v-html — 渲染 HTML(⚠️ XSS 风险) -->
  <div v-html="rawHtml"></div>

  <!-- v-pre — 跳过编译,显示原始 {{}} -->
  <span v-pre>{{ 这不会被编译 }}</span>

  <!-- v-cloak — 编译完成前隐藏(需要配套 CSS) -->
  <div v-cloak>{{ message }}</div>
</template>

4.3 事件修饰符

vue
<!-- 事件修饰符 -->
<a @click.stop="doThis">      阻止冒泡</a>
<form @submit.prevent="onSubmit"> 阻止默认行为</form>
<a @click.stop.prevent="doThat"></a>   <!-- 链式 -->
<form @submit.prevent></form>          <!-- 只有修饰符,无处理函数 -->
<div @click.self="doThat">     仅 event.target 是自身时触发</div>
<div @click.once="doThis">     只触发一次</div>
<div @scroll.passive="onScroll"> 滚动事件的 passive(提升性能)</div>

<!-- 按键修饰符 -->
<input @keyup.enter="submit" />       <!-- Enter -->
<input @keyup.esc="cancel" />         <!-- Esc -->
<input @keyup.delete="remove" />      <!-- Delete / Backspace -->
<input @keyup.ctrl.s="save" />        <!-- 系统修饰键组合 -->
<input @keyup.exact="onExact" />      <!-- 仅按指定键,无组合键 -->

<!-- 鼠标按键修饰符 -->
<button @click.left="leftClick">左键</button>
<button @click.right="rightClick">右键</button>
<button @click.middle="middleClick">中键</button>

4.4 v-model 修饰符

vue
<input v-model.lazy="msg" />    <!-- change 事件后同步(而非 input) -->
<input v-model.number="age" />  <!-- 自动转为数字 -->
<input v-model.trim="msg" />    <!-- 自动去除首尾空格 -->

4.5 v-if vs v-show

特性 v-if v-show
渲染方式 条件为假时不渲染 DOM 始终渲染,用 display:none 隐藏
切换开销 高(创建/销毁) 低(只改 CSS)
初始开销 低(条件为假时不渲染) 高(始终渲染)
适用场景 运行时条件很少改变 频繁切换显示/隐藏
配合 v-for ❌ v-if 和 v-for 不可同级使用

5. 组合式 API 深入

5.1 <script setup> 语法糖(推荐)

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

// 变量和函数直接暴露给模板
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() { count.value++ }

onMounted(() => console.log('组件已挂载'))
</script>

<template>
  <p>{{ count }} × 2 = {{ double }}</p>
  <button @click="increment">+1</button>
</template>

5.2 defineProps — 定义 Props

vue
<script setup lang="ts">
// 运行时声明
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 },
  tags: { type: Array, default: () => [] },
})

// TypeScript 纯类型声明(推荐)
interface Props {
  title: string
  count?: number
  tags?: string[]
  onUpdate?: (val: number) => void
}
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  tags: () => [],
})
</script>

5.3 defineEmits — 定义事件

vue
<script setup lang="ts">
// 运行时声明
const emit = defineEmits(['update', 'delete'])

// TypeScript 声明(推荐)
const emit = defineEmits<{
  update: [id: number, value: string]      // (e: 'update', id: number, value: string)
  delete: [id: number]                      // (e: 'delete', id: number)
}>()

// 触发事件
emit('update', 1, 'newValue')
</script>

5.4 defineModel — 组件 v-model(Vue 3.4+)

vue
<!-- 子组件 ChildInput.vue -->
<script setup lang="ts">
// 单个 v-model
const model = defineModel<string>({ default: '' })
// 使用: model.value 用于读,model.value = xxx 用于更新
</script>

<template>
  <input v-model="model" />
</template>

<!-- 父组件 -->
<ChildInput v-model="text" />

<!-- 多个 v-model -->
<script setup>
const title = defineModel<string>('title', { default: '' })
const content = defineModel<string>('content', { default: '' })
</script>

5.5 defineExpose — 暴露给父组件

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

const count = ref(0)
function reset() { count.value = 0 }

// 默认不暴露,需显式声明
defineExpose({ count, reset })
</script>

5.6 生命周期钩子

typescript
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,      // <KeepAlive> 激活
  onDeactivated,    // <KeepAlive> 停用
  onErrorCaptured   // 捕获子组件错误
} from 'vue'

// 组合式 API 生命周期对应关系
// beforeCreate  → setup() 本身
// created        → setup() 本身
// beforeMount    → onBeforeMount()
// mounted        → onMounted()
// beforeUpdate   → onBeforeUpdate()
// updated        → onUpdated()
// beforeUnmount  → onBeforeUnmount()
// unmounted      → onUnmounted()

5.7 依赖注入 (provide / inject)

vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'

const theme = ref('dark')
provide('theme', readonly(theme))    // 只读下发,防止子组件修改
provide('setTheme', (val: string) => { theme.value = val })  // 下发修改方法

// 使用 Symbol 作为 key(避免命名冲突)
import { InjectionKey } from 'vue'
const themeKey: InjectionKey<string> = Symbol('theme')
provide(themeKey, theme)
</script>

<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme')             // 可能为 undefined
const theme2 = inject('theme', 'light')   // 提供默认值
const theme3 = inject('theme', undefined, true)  // 第三个参数 true 表示不传默认值时允许 undefined

const setTheme = inject<(val: string) => void>('setTheme')
setTheme('light')  // 修改祖先的 theme
</script>

5.8 组合式函数 (Composables)

组合式函数是 Vue3 的逻辑复用核心机制,替代 Vue2 的 Mixins。

typescript
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

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

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

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

// 使用
const { x, y } = useMouse()

composables 命名规范:

  • 文件名:useXxx.ts(驼峰,use 前缀)
  • 函数名:useXxx()
  • 放在 src/composables/ 目录

composables 设计原则:

  1. 单一职责:一个 composable 只做一件事
  2. 无副作用入参:接收参数而非依赖全局状态
  3. 返回值清晰:return { ... } 明确的 API
  4. 生命周期自管理:内部 addEventListener 要在 onUnmounted 清理
typescript
// composables/useFetch.ts — 通用数据请求封装
import { ref, watchEffect, toValue } from 'vue'

export function useFetch<T>(url: string | (() => string)) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    data.value = null
    error.value = null
    try {
      const res = await fetch(toValue(url))
      data.value = await res.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

// 使用
const { data, error, loading } = useFetch<User[]>('/api/users')

6. 组件系统

6.1 组件注册

vue
<script setup lang="ts">
// 局部注册(推荐)— import 即注册,无需额外步骤
import MyButton from './MyButton.vue'
import MyInput from './MyInput.vue'
</script>

<template>
  <MyButton />
  <MyInput />
</template>
typescript
// main.ts — 全局注册
import MyButton from './components/MyButton.vue'
app.component('MyButton', MyButton)

6.2 插槽 (Slots)

vue
<!-- 父组件 -->
<Layout>
  <template #header>
    <h1>页面标题</h1>
  </template>

  <template #default>
    <p>默认内容</p>
  </template>

  <template #footer="{ year }">
    <p>© {{ year }}</p>
  </template>
</Layout>

<!-- 子组件 Layout.vue -->
<template>
  <header><slot name="header" /></header>
  <main><slot>无内容时的默认文字</slot></main>
  <footer><slot name="footer" :year="2026" /></footer>
</template>

6.3 异步组件 (defineAsyncComponent)

typescript
import { defineAsyncComponent } from 'vue'

// 基础用法
const AsyncComp = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 高级配置
const AsyncComp = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,   // 加载中显示
  errorComponent: ErrorDisplay,       // 加载失败显示
  delay: 300,                         // 延迟显示 loading(ms)
  timeout: 10000,                     // 超时时间(ms)
})

6.4 组件通信方式总结

方式 适用场景 方向
Props + Emits 父子组件 父→子(props),子→父(emits)
v-model (defineModel) 表单类双向绑定 父子双向
provide / inject 跨层级传递(祖先→后代) 上→下
Pinia Store 全局状态、跨组件共享 任意方向
模板 ref + defineExpose 父组件直接调用子组件方法 父→子
EventBus (mitt) 任意组件通信(Vue3 不再内置,需第三方库) 任意方向

6.5 模板引用 (Template Refs)

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

// 单个 DOM 元素
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => inputRef.value?.focus())

// 组件实例引用
const childComp = ref<InstanceType<typeof ChildComp> | null>(null)
childComp.value?.reset()  // 需子组件 defineExpose 暴露

// v-for 中的 ref
const itemRefs = ref<HTMLElement[]>([])
function setItemRef(el: HTMLElement | null, index: number) {
  if (el) itemRefs.value[index] = el
}
</script>

<template>
  <input ref="inputRef" />
  <ChildComp ref="childComp" />
  <li v-for="(item, i) in list" :key="item.id" :ref="el => setItemRef(el, i)">
    {{ item.name }}
  </li>
</template>

7. TypeScript 集成

7.1 常用类型工具

typescript
import type { Ref, ComputedRef, ComponentPublicInstance } from 'vue'

// Ref 类型
const count: Ref<number> = ref(0)
const name: Ref<string | null> = ref(null)

// PropType 用于运行时 props
import type { PropType } from 'vue'
defineProps({
  status: { type: String as PropType<'active' | 'inactive'>, required: true },
})

// 组件实例类型
import MyComp from './MyComp.vue'
const compRef = ref<InstanceType<typeof MyComp> | null>(null)

// defineEmits 事件类型
const emit = defineEmits<{
  change: [id: number, value: string]
  close: []
}>()

// defineProps withDefaults
interface Props {
  title: string
  count?: number
  items?: string[]
}
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => [],
})

7.2 .vue 文件类型声明

typescript
// env.d.ts(项目根目录,Vite 自动识别)
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

8. Vue Router 4

8.1 基础路由配置

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('@/views/HomeView.vue'),  // 懒加载
    },
    {
      path: '/users/:id',
      name: 'user-detail',
      component: () => import('@/views/UserDetail.vue'),
      props: true,  // 将路由参数作为 props 传给组件
    },
    {
      path: '/admin',
      component: () => import('@/views/AdminLayout.vue'),
      meta: { requiresAuth: true },
      children: [
        { path: '', component: () => import('@/views/AdminDashboard.vue') },
        { path: 'users', component: () => import('@/views/AdminUsers.vue') },
      ],
    },
    {
      path: '/:pathMatch(.*)*',  // Vue Router 4 404 语法
      name: 'not-found',
      component: () => import('@/views/NotFound.vue'),
    },
  ],
})

// 全局导航守卫
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

export default router

8.2 组合式 API 中使用路由

vue
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'

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

// 读取路由参数(响应式)
const userId = computed(() => route.params.id)

// 读取 query
const search = computed(() => route.query.q as string)

// 编程式导航
function goToUser(id: number) {
  router.push({ name: 'user-detail', params: { id } })
}
function goBack() {
  router.back()
}

// 导航守卫(组件内)
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    return confirm('有未保存的修改,确定离开?')
  }
})
</script>

8.3 路由过渡动画

vue
<!-- App.vue -->
<template>
  <RouterView v-slot="{ Component, route }">
    <Transition name="fade" mode="out-in">
      <component :is="Component" :key="route.path" />
    </Transition>
  </RouterView>
</template>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}
</style>

9. Pinia 状态管理

9.1 Store 定义(组合式风格,推荐)

typescript
// stores/useUserStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // state
  const user = ref<User | null>(null)
  const token = ref(localStorage.getItem('token') || '')

  // getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? '游客')

  // actions
  async function login(credentials: { username: string; password: string }) {
    const res = await api.login(credentials)
    token.value = res.token
    user.value = res.user
    localStorage.setItem('token', res.token)
  }

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

  return { user, token, isLoggedIn, userName, login, logout }
})

9.2 Store 定义(选项式风格)

typescript
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() { this.count++ },
  },
})

9.3 在组件中使用

vue
<script setup lang="ts">
import { useUserStore } from '@/stores/useUserStore'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构保持响应式(state 和 getters 需要用 storeToRefs)
const { user, isLoggedIn, userName } = storeToRefs(userStore)

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

// 调用 action
await login({ username: 'admin', password: '123456' })
</script>

10. 内置组件与指令

10.1 Transition / TransitionGroup

vue
<!-- 过渡动画 -->
<Transition name="fade">
  <p v-if="show">显示/隐藏带过渡</p>
</Transition>

<!-- Transition 自定义 class(配合 Animate.css 等库) -->
<Transition
  enter-active-class="animate__animated animate__fadeIn"
  leave-active-class="animate__animated animate__fadeOut"
>
  <p v-if="show">动画文字</p>
</Transition>

<!-- TransitionGroup — 列表动画 -->
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>

10.2 KeepAlive — 缓存组件

vue
<template>
  <!-- 基本用法:缓存路由组件 -->
  <RouterView v-slot="{ Component }">
    <KeepAlive>
      <component :is="Component" />
    </KeepAlive>
  </RouterView>

  <!-- 高级配置 -->
  <KeepAlive :include="['HomeView', 'UserList']" :exclude="['LoginView']" :max="10">
    <component :is="Component" />
  </KeepAlive>
</template>

10.3 Teleport — 传送门

vue
<template>
  <!-- 将内容传送到 body 下(弹窗、下拉菜单等) -->
  <Teleport to="body">
    <Modal v-if="showModal" @close="showModal = false" />
  </Teleport>

  <!-- 禁用 Teleport -->
  <Teleport to="body" :disabled="isMobile">
    <Modal />
  </Teleport>
</template>

10.4 Suspense — 异步组件等待

vue
<template>
  <Suspense>
    <!-- 异步内容 -->
    <template #default>
      <AsyncDashboard />
    </template>
    <!-- 加载中 -->
    <template #fallback>
      <LoadingSkeleton />
    </template>
  </Suspense>
</template>

11. 生态工具推荐

11.1 必备库

用途 安装
VueUse 组合式工具函数(useMouse、useStorage 等 200+) pnpm add @vueuse/core
Pinia 状态管理(官方) pnpm add pinia
Vue Router 路由管理(官方) pnpm add vue-router

11.2 UI 组件库

特点 适用场景
Element Plus Vue3 最流行、组件最全、中文友好 后台管理系统
Naive UI 组件丰富、TypeScript 优先、暗色模式好 中后台、工具类产品
Ant Design Vue 企业级 UI、组件质量高 企业应用
Vant 4 移动端 UI、轻量高性能 移动端 H5
PrimeVue 主题丰富、国际化好 多场景
TDesign Vue Next 腾讯出品、设计规范成熟 内部/企业项目

11.3 Vite 插件推荐

bash
pnpm add -D \
  @vitejs/plugin-vue \           # Vue SFC 编译(内置)
  unplugin-auto-import \         # 自动导入 Vue API(不用手动 import)
  unplugin-vue-components \      # 按需自动导入组件
  vite-plugin-compression \      # Gzip / Brotli 压缩
  vite-plugin-imagemin \         # 图片压缩
  rollup-plugin-visualizer       # 打包体积分析

11.4 工具库

用途
axios HTTP 请求(配合拦截器封装使用)
dayjs 日期处理(轻量替代 moment.js,2KB)
lodash-es 工具函数(需配合 Tree-shaking)
mitt 极简事件总线(替代 Vue2 的 $bus)
vue-i18n 国际化
echarts 数据可视化图表

12. 项目最佳实践

12.1 组件设计原则

vue
<!-- ✅ 好的组件设计:单一职责、props down / events up -->
<script setup lang="ts">
// UserCard.vue — 只负责展示用户信息
interface Props {
  user: User
  loading?: boolean
}
const props = withDefaults(defineProps<Props>(), { loading: false })
const emit = defineEmits<{ edit: [id: number]; delete: [id: number] }>()
</script>

12.2 项目目录组织(中型项目)

src/
├── assets/          # 需要编译的静态资源
│   ├── images/
│   └── styles/
│       ├── index.scss
│       ├── variables.scss   # CSS 变量
│       └── mixins.scss      # SCSS 混入
├── components/      # 全局可复用组件
│   ├── common/      #   通用组件(Button、Modal、Table)
│   └── business/    #   业务组件
├── composables/     # 组合式函数
├── layouts/         # 布局组件
├── router/          # 路由
│   ├── index.ts
│   └── guards.ts    # 路由守卫
├── stores/          # Pinia
├── views/           # 页面
├── api/             # API 封装
│   ├── request.ts   #   axios 实例 + 拦截器
│   └── modules/     #   按模块拆分 API
├── utils/           # 工具函数
├── types/           # 全局类型
├── constants/       # 常量
└── hooks/           # 可选,同 composables

12.3 API 请求封装示例

typescript
// api/request.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/stores/useUserStore'
import router from '@/router'

const instance: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 15000,
})

// 请求拦截器
instance.interceptors.request.use((config) => {
  const { token } = useUserStore()
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// 响应拦截器
instance.interceptors.response.use(
  (res: AxiosResponse) => res.data,
  (error) => {
    if (error.response?.status === 401) {
      useUserStore().logout()
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

export default instance

12.4 Vite 配置建议

typescript
// 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') },  // 路径别名
  },
  server: {
    port: 3000,
    proxy: {
      '/api': { target: 'http://localhost:8080', changeOrigin: true },
    },
  },
  build: {
    chunkSizeWarningLimit: 1000,          // 调大 chunk 警告阈值
    rollupOptions: {
      output: {
        manualChunks: {                    // 手动拆包
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'element-plus': ['element-plus'],
        },
      },
    },
  },
})

12.5 编码规范建议

  1. <script setup lang="ts"> 统一使用,禁止混用选项式 API
  2. ref() 优先,reactive() 仅用于表单等局部场景
  3. defineProps 用 TS 纯类型声明 + withDefaults
  4. Composable 文件统一 useXxx.ts 命名
  5. 组件文件至少两个单词(如 UserCard.vue),避免与 HTML 标签冲突
  6. props 用 camelCase,模板中 kebab-case:userName vs <UserCard user-name="..."/>
  7. 尽可能使用 shallowRef()shallowReactive() 处理大数据以减少性能开销
  8. v-for 必须有唯一 key,且不与 v-if 同级使用
  9. 避免直接修改 props,使用 emit 或 v-model
  10. readonly() 包裹 provide 的数据 防止子组件修改

13. 常见问题与调试

13.1 响应式丢失问题

typescript
// ❌ 错误:解构 reactive 丢失响应式
const state = reactive({ count: 0 })
const { count } = state  // count 是普通数字,不会响应

// ✅ 正确:用 toRefs
const { count } = toRefs(state)

// ✅ 或用 ref 代替 reactive
const state = ref({ count: 0 })
const { count } = state.value  // 仍需 .value

13.2 ref 在模板和 script 中的区别

vue
<script setup>
const count = ref(0)
console.log(count.value)  // script 中必须 .value
</script>

<template>
  <!-- 模板中自动解包顶层 ref,不需要 .value -->
  <p>{{ count }}</p>
  <!-- 嵌套在对象中的 ref 不会自动解包 -->
  <p>{{ obj.count.value }}</p>
</template>

13.3 watch vs watchEffect

特性 watch watchEffect
需要指定侦听源 否(自动追踪)
可访问旧值
首次是否执行 默认不执行(需 immediate: true 立即执行
适用场景 精确控制触发时机、需要旧值对比 简单同步副作用

13.4 CSS scoped 穿透

vue
<style scoped>
/* 修改子组件内部样式 */
:deep(.el-input__inner) { color: red; }

/* 修改插槽内容样式 */
:slotted(.slot-content) { font-size: 14px; }

/* 全局样式(仅当前组件 */  
:global(.global-class) { margin: 0; }
</style>

13.5 浏览器 DevTools

  • 安装 Vue.js devtools 浏览器扩展(v6+ 支持 Vue3)
  • 支持:组件树查看、状态检查、事件追踪、性能分析

13.6 常用调试技巧

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

// 1. 使用 JSON.stringify 深拷贝对象
const clone = JSON.parse(JSON.stringify(obj))

// 2. 监听所有 props 变化
watch(() => ({ ...props }), (newVal) => console.log('props变化', newVal))

// 3. 捕获子组件异常
onErrorCaptured((err, instance, info) => {
  console.error('子组件错误:', err, info)
  return false  // 阻止向上传播
})
</script>

快速参考卡片

创建项目:        pnpm create vue@latest
开发运行:        pnpm dev
构建生产:        pnpm build
本地预览:        pnpm preview

核心概念:
  ref()            基本类型响应式,需 .value
  reactive()       对象响应式,不能整体替换
  computed()       缓存计算属性
  watch()          精确侦听,可获取旧值
  watchEffect()    自动收集依赖,立即执行

组件通信:
  defineProps      父传子
  defineEmits      子传父
  defineModel      组件 v-model
  provide/inject   跨层级
  Pinia store      全局状态

生命周期:
  onMounted        DOM 已挂载
  onUnmounted      组件卸载前
  onBeforeUpdate   更新前

路由:
  useRouter()      编程式导航 (push/replace/back)
  useRoute()       获取当前路由信息(params/query)
  router.beforeEach 全局守卫

Store:
  defineStore      创建 store
  storeToRefs      解构保持响应式

版本: 2026-06 | Vue 版本: 3.4+ | Vite 版本: 5+
后续持续更新,建议配合 Vue3 官方文档 学习

评论 (0)

发表评论