Vue3 开发指南(完整版)
目录
- 环境搭建
- 项目创建与结构
- 响应式系统
- 模板语法
- 组合式 API 深入
- 组件系统
- TypeScript 集成
- Vue Router 4
- Pinia 状态管理
- 内置组件与指令
- 生态工具推荐
- 项目最佳实践
- 常见问题与调试
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 设计原则:
- 单一职责:一个 composable 只做一件事
- 无副作用入参:接收参数而非依赖全局状态
- 返回值清晰:return { ... } 明确的 API
- 生命周期自管理:内部 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 编码规范建议
<script setup lang="ts">统一使用,禁止混用选项式 API- ref() 优先,reactive() 仅用于表单等局部场景
- defineProps 用 TS 纯类型声明 + withDefaults
- Composable 文件统一
useXxx.ts命名 - 组件文件至少两个单词(如
UserCard.vue),避免与 HTML 标签冲突 - props 用 camelCase,模板中 kebab-case:
:userNamevs<UserCard user-name="..."/> - 尽可能使用
shallowRef()和shallowReactive()处理大数据以减少性能开销 - v-for 必须有唯一 key,且不与 v-if 同级使用
- 避免直接修改 props,使用 emit 或 v-model
- 用
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)
发表评论