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)

# 查看版本
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)

# 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 入口文件详解

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() — 基本类型的响应式

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() — 对象类型的响应式

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() — 计算属性

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() — 侦听器

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 响应式工具函数

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 文本插值与表达式

<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 指令一览

<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 事件修饰符

<!-- 事件修饰符 -->
<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 修饰符

<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> 语法糖(推荐)

<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

<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 — 定义事件

<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+)

<!-- 子组件 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 — 暴露给父组件

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

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

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

5.6 生命周期钩子

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)

<!-- 祖先组件 -->
<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。

// 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 清理
// 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 组件注册

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

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

6.2 插槽 (Slots)

<!-- 父组件 -->
<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)

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)

<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 常用类型工具

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 文件类型声明

// 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 基础路由配置

// 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 中使用路由

<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 路由过渡动画

<!-- 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 定义(组合式风格,推荐)

// 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 定义(选项式风格)

import { defineStore } from 'pinia'

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

9.3 在组件中使用

<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

<!-- 过渡动画 -->
<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 — 缓存组件

<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 — 传送门

<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 — 异步组件等待

<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 插件推荐

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 组件设计原则

<!-- ✅ 好的组件设计:单一职责、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 请求封装示例

// 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 配置建议

// 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 响应式丢失问题

// ❌ 错误:解构 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 中的区别

<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 穿透

<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 常用调试技巧

<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)

发表评论