Vue3 完全指南:从原理到实战
文档版本:v1.0 更新时间:2026-09-21 适用范围:前端工程师、全栈工程师、技术负责人,以及所有需要从 Vue2 迁移或从零学习 Vue3 的人 说明:版本与特性基于 2026 年 9 月 Vue 官方资料;Vue 3.5 “Tengen Toppa Gurren Lagann” 为当前最新小版本
目录
1. 概述:Vue3 是什么、为什么选它
1.1 Vue3 的定位
Vue 3 是 Vue.js 的第三个大版本,于 2020 年 9 月正式发布——它不是"Vue2 的升级版",而是从头重写的新一代前端框架。2026 年的今天,Vue 3.5 “Tengen Toppa Gurren Lagann” 已是当前最新小版本,Vue2 已 EOL(2023 年 12 月停止维护)。
Vue3 的核心定位是:
- 渐进式框架:从小项目到大型应用都能覆盖;
- 性能优先:从头重写的响应式系统与虚拟 DOM;
- Composition API:解决大型应用的代码组织问题;
- TypeScript 原生支持:从设计之初就拥抱类型系统。
1.2 为什么选 Vue3
| 性能 | Proxy 响应式、编译时优化、更小的运行时 |
| 代码组织 | Composition API 解决大型应用的逻辑复用 |
| TypeScript | 原生 TS 支持,类型推导更自然 |
| 生态 | Vite、Pinia、Nuxt3 组成新一代生态 |
| 维护 | Vue2 已 EOL,Vue3 是唯一现役版本 |
2026 年的现状:Vue2 已停止维护两年多,Vue3 是事实上的标准——新项目直接选 Vue3,老项目按节奏迁移。
1.3 Vue3 的设计目标
Vue3 的三个核心设计目标:
1.4 2026 年 Vue3 生态现状
2026 年 9 月的今天,Vue3 生态已经完全成熟:
| Vue 核心 | 3.5 “Tengen Toppa Gurren Lagann” | 当前最新小版本 |
| Vite | 6.x | 构建工具标准 |
| Pinia | 2.x | 官方推荐状态管理 |
| Vue Router | 4.x | 官方路由 |
| Nuxt | 3.x | SSR/SSG 元框架 |
| TypeScript | 5.x | 原生支持 |
关键事实:Vue2 已于 2023 年 12 月 31 日正式 EOL——不再有安全补丁、不再有新功能、社区不再维护。2026 年的今天,Vue2 是"已淘汰"的技术,Vue3 是唯一现役选择。
1.5 Vue3 的适用场景
| 中大型单页应用(SPA) | ⭐⭐⭐⭐⭐ | Composition API + Pinia 完美适配 |
| 内容型网站(博客/文档/营销页) | ⭐⭐⭐⭐⭐ | Nuxt3 SSG 静态生成 |
| 电商/门户 | ⭐⭐⭐⭐⭐ | Nuxt3 SSR 首屏快 |
| 组件库开发 | ⭐⭐⭐⭐⭐ | TypeScript 原生支持 |
| 移动端 H5 | ⭐⭐⭐⭐ | Vite 构建快,体验好 |
| 小程序 | ⭐⭐⭐ | uni-app/Taro 支持 Vue3 |
不适合的场景:
- 极端高性能游戏/实时可视化:选 Canvas/WebGL 原生方案;
- 需要与 React 生态深度集成:如果团队主力是 React,不要硬切 Vue。
1.6 Vue3 的设计哲学
Vue3 的设计哲学可以概括为三句话:
与 React 的对比:
| 学习曲线 | 平缓(模板语法友好) | 陡峭(JSX + Hooks) |
| 状态管理 | Pinia(官方推荐) | Redux/Zustand/Jotai(多种选择) |
| 构建工具 | Vite(官方推荐) | Vite/Webpack/Next.js |
| SSR 元框架 | Nuxt 3 | Next.js |
| 类型支持 | 原生 TS 支持 | TSX 原生 |
Vue3 的核心优势:学习曲线平缓、官方生态统一、开发效率高——这是 Vue3 在国内市场占有率持续领先的根本原因。
1.7 Vue3 与其他框架对比
| 学习曲线 | 平缓 | 陡峭 | 平缓 |
| 包体积 | ~30KB | ~42KB | 更小(编译时) |
| 性能 | 好 | 好 | 更好(编译时优化) |
| 状态管理 | Pinia | Redux/Zustand | 内置 store |
| SSR | Nuxt 3 | Next.js | SvelteKit |
| TypeScript | 原生 | 原生 | 原生 |
| 国内生态 | 最成熟 | 成熟 | 较新 |
| 就业市场 | 大 | 大 | 小 |
选型建议:
- 国内企业:Vue3 生态最成熟,就业市场最大;
- 国际化团队:React 生态更成熟,国际社区更大;
- 小团队/快速开发:Svelte 更轻量,但生态较小;
- 长期学习:Vue3 和 React 都值得学,思路相通。
1.8 Vue3 的学习资源
| Vue 官方文档 | 文档 | https://cn.vuejs.org — 最权威的文档 |
| Vue 官方博客 | 博客 | https://blog.vuejs.org — 最新版本动态 |
| Vue Mastery | 视频课程 | 付费,质量高,实战导向 |
| Vue School | 视频课程 | 付费,实战项目多 |
| VueUse | 工具库 | https://vueuse.org — 实用 composable 集合 |
| awesome-vue | 资源大全 | GitHub 上的 Vue 生态资源列表 |
学习建议:
- 入门:先看官方文档,掌握基础语法;
- 进阶:做实战项目,深入 Composition API;
- 深入:读源码,理解响应式原理;
- 生态:学习 Vite、Pinia、Nuxt 3 等生态工具。
Vue3 适合谁学:
| 前端新手 | ⭐⭐⭐⭐⭐ | 模板语法友好,学习曲线平缓 |
| Vue2 老司机 | ⭐⭐⭐⭐⭐ | 升级路径清晰,生态统一 |
| React 开发者 | ⭐⭐⭐⭐ | 思路相通,迁移成本低 |
| 后端转前端 | ⭐⭐⭐⭐⭐ | 模板语法更接近后端思维 |
| 全栈开发者 | ⭐⭐⭐⭐⭐ | 配合 Nuxt 3 做全栈开发 |
为什么 2026 年还要学 Vue3:
2. Vue2 vs Vue3 核心区别总览
2.1 一张表看懂核心差异
| 响应式原理 | Object.defineProperty | Proxy + Reflect |
| API 风格 | Options API(data/methods/computed) | Composition API(ref/reactive/computed) |
| 构建工具 | webpack | Vite |
| 状态管理 | Vuex | Pinia |
| 路由 | Vue Router 3 | Vue Router 4 |
| TypeScript | 支持有限 | 原生支持 |
| 多根节点 | 不支持(单根) | 支持 Fragment(多根) |
| Teleport | 无 | 内置 |
| Suspense | 无 | 内置 |
| 生命周期 | beforeDestroy/destroyed | onBeforeUnmount/onUnmounted |
2.2 迁移的本质
从 Vue2 到 Vue3 的迁移,本质上是三件事:
这不是"改几个语法",而是"整个开发范式的升级"——但升级方向是对的,且路径清晰。
2.3 迁移的收益分析
从 Vue2 迁移到 Vue3,你能得到什么:
| 性能提升 | Proxy 响应式 + 编译时优化,渲染性能提升 1~2 倍 |
| 代码组织 | Composition API 解决大型应用的逻辑复用 |
| TypeScript | 原生 TS 支持,类型推导更自然 |
| 开发体验 | Vite 极快启动 + 热更新 |
| 维护性 | Vue2 已 EOL,Vue3 持续迭代 |
| 生态 | 新组件库、新工具都优先支持 Vue3 |
迁移成本:
| 小型项目(< 10 个页面) | 语法迁移 + Pinia | 1~3 天 |
| 中型项目(10~50 个页面) | Composition API 迁移 + Vuex→Pinia | 1~2 周 |
| 大型项目(> 50 个页面) | 全量迁移 + 组件库适配 | 2~4 周 |
核心结论:Vue2 已 EOL,迁移不是"要不要",而是"什么时候"——越早迁移,技术债越少。
2.4 Vue2 到 Vue3 的渐进迁移策略
渐进迁移三步走:
#mermaid-svg-ynznx95KtWlpV66U{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ynznx95KtWlpV66U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ynznx95KtWlpV66U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ynznx95KtWlpV66U .error-icon{fill:#552222;}#mermaid-svg-ynznx95KtWlpV66U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ynznx95KtWlpV66U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ynznx95KtWlpV66U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ynznx95KtWlpV66U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ynznx95KtWlpV66U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ynznx95KtWlpV66U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ynznx95KtWlpV66U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ynznx95KtWlpV66U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ynznx95KtWlpV66U .marker.cross{stroke:#333333;}#mermaid-svg-ynznx95KtWlpV66U svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ynznx95KtWlpV66U p{margin:0;}#mermaid-svg-ynznx95KtWlpV66U .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ynznx95KtWlpV66U .cluster-label text{fill:#333;}#mermaid-svg-ynznx95KtWlpV66U .cluster-label span{color:#333;}#mermaid-svg-ynznx95KtWlpV66U .cluster-label span p{background-color:transparent;}#mermaid-svg-ynznx95KtWlpV66U .label text,#mermaid-svg-ynznx95KtWlpV66U span{fill:#333;color:#333;}#mermaid-svg-ynznx95KtWlpV66U .node rect,#mermaid-svg-ynznx95KtWlpV66U .node circle,#mermaid-svg-ynznx95KtWlpV66U .node ellipse,#mermaid-svg-ynznx95KtWlpV66U .node polygon,#mermaid-svg-ynznx95KtWlpV66U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ynznx95KtWlpV66U .rough-node .label text,#mermaid-svg-ynznx95KtWlpV66U .node .label text,#mermaid-svg-ynznx95KtWlpV66U .image-shape .label,#mermaid-svg-ynznx95KtWlpV66U .icon-shape .label{text-anchor:middle;}#mermaid-svg-ynznx95KtWlpV66U .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ynznx95KtWlpV66U .rough-node .label,#mermaid-svg-ynznx95KtWlpV66U .node .label,#mermaid-svg-ynznx95KtWlpV66U .image-shape .label,#mermaid-svg-ynznx95KtWlpV66U .icon-shape .label{text-align:center;}#mermaid-svg-ynznx95KtWlpV66U .node.clickable{cursor:pointer;}#mermaid-svg-ynznx95KtWlpV66U .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ynznx95KtWlpV66U .arrowheadPath{fill:#333333;}#mermaid-svg-ynznx95KtWlpV66U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ynznx95KtWlpV66U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ynznx95KtWlpV66U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ynznx95KtWlpV66U .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ynznx95KtWlpV66U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ynznx95KtWlpV66U .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ynznx95KtWlpV66U .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ynznx95KtWlpV66U .cluster text{fill:#333;}#mermaid-svg-ynznx95KtWlpV66U .cluster span{color:#333;}#mermaid-svg-ynznx95KtWlpV66U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ynznx95KtWlpV66U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ynznx95KtWlpV66U rect.text{fill:none;stroke-width:0;}#mermaid-svg-ynznx95KtWlpV66U .icon-shape,#mermaid-svg-ynznx95KtWlpV66U .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ynznx95KtWlpV66U .icon-shape p,#mermaid-svg-ynznx95KtWlpV66U .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ynznx95KtWlpV66U .icon-shape .label rect,#mermaid-svg-ynznx95KtWlpV66U .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ynznx95KtWlpV66U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ynznx95KtWlpV66U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ynznx95KtWlpV66U :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
第一步:Vue2.7兼容 Vue3 语法
第二步:Vite + Vue3并行运行
第三步:全量迁移移除 Vue2
第一步:Vue2.7 兼容 Vue3 语法
Vue 2.7 是 Vue2 的最后一个版本,它 backport 了 Vue3 的 Composition API:
// Vue 2.7 中也可以用 Composition API
import { ref, computed } from 'vue';
export default {
setup() {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
return { count, doubleCount };
}
};
第二步:Vite + Vue3 并行运行
用 Vite 搭建 Vue3 项目,和 Vue2 项目并行运行,逐步把页面从 Vue2 迁移到 Vue3。
第三步:全量迁移
所有页面都迁移到 Vue3 后,移除 Vue2 项目,完成迁移。
2.5 Vue3 的生态对比
| 构建工具 | webpack | Vite |
| 状态管理 | Vuex | Pinia |
| 路由 | Vue Router 3 | Vue Router 4 |
| SSR | Nuxt 2 | Nuxt 3 |
| UI 组件库 | Element UI | Element Plus |
| 工具库 | 零散 | VueUse |
生态升级的本质:从"零散生态"到"统一生态"——Vue3 的生态更统一、更现代、更高效。
2.6 Vue3 的版本历史
| Vue 3.0 | 2020.09 | 首个正式版本,Composition API |
| Vue 3.1 | 2021.06 | 兼容性改进 |
| Vue 3.2 | 2021.08 | 性能优化,<script setup> 稳定 |
| Vue 3.3 | 2023.05 | TS 改进,泛型组件 |
| Vue 3.4 | 2023.12 | defineModel,SSR 改进 |
| Vue 3.5 | 2026.07 | Reactive Props Destructure,onEffectCleanup |
版本选择建议:
- 生产环境:用最新的稳定版本(Vue 3.5+);
- 新项目:直接用最新版本;
- 老项目:逐步升级,不要跨大版本跳跃。
版本升级注意事项:
| 3.0 → 3.1 | 兼容性改进,直接升级 |
| 3.1 → 3.2 | <script setup> 稳定,推荐升级 |
| 3.2 → 3.3 | TS 改进,升级后检查类型 |
| 3.3 → 3.4 | defineModel 新特性,可选升级 |
| 3.4 → 3.5 | Reactive Props Destructure,推荐升级 |
升级建议:小版本升级通常是安全的,升级前务必跑一遍完整测试;大版本升级(如 Vue2→Vue3)需要按迁移路线图执行,不要一次性大爆炸。
3. 响应式原理:Proxy vs Object.defineProperty
3.1 Vue2 的痛点
Vue2 的响应式基于 Object.defineProperty——它有两个无法根治的问题:
// Vue2 的响应式实现(简化)
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// 依赖收集
return val;
},
set(newVal) {
// 触发更新
val = newVal;
}
});
}
// 痛点 1:无法监听新增属性
Vue.set(vm.someObject, 'newKey', 'value'); // 必须用 Vue.set
// 痛点 2:无法监听数组索引赋值
vm.someArray[0] = 'newValue'; // 不触发更新
这两个痛点在大型应用中会变成"玄学 bug"——为什么数据变了视图没更新?答案通常是"你新增了属性或改了数组索引"。
3.2 Vue3 的解决方案:Proxy
Vue3 用 JavaScript 原生的 Proxy 替代 Object.defineProperty——它从根本上解决了这两个问题:
#mermaid-svg-NQpEe0z4aTreOGrw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NQpEe0z4aTreOGrw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NQpEe0z4aTreOGrw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NQpEe0z4aTreOGrw .error-icon{fill:#552222;}#mermaid-svg-NQpEe0z4aTreOGrw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NQpEe0z4aTreOGrw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NQpEe0z4aTreOGrw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NQpEe0z4aTreOGrw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NQpEe0z4aTreOGrw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NQpEe0z4aTreOGrw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NQpEe0z4aTreOGrw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NQpEe0z4aTreOGrw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NQpEe0z4aTreOGrw .marker.cross{stroke:#333333;}#mermaid-svg-NQpEe0z4aTreOGrw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NQpEe0z4aTreOGrw p{margin:0;}#mermaid-svg-NQpEe0z4aTreOGrw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-NQpEe0z4aTreOGrw .cluster-label text{fill:#333;}#mermaid-svg-NQpEe0z4aTreOGrw .cluster-label span{color:#333;}#mermaid-svg-NQpEe0z4aTreOGrw .cluster-label span p{background-color:transparent;}#mermaid-svg-NQpEe0z4aTreOGrw .label text,#mermaid-svg-NQpEe0z4aTreOGrw span{fill:#333;color:#333;}#mermaid-svg-NQpEe0z4aTreOGrw .node rect,#mermaid-svg-NQpEe0z4aTreOGrw .node circle,#mermaid-svg-NQpEe0z4aTreOGrw .node ellipse,#mermaid-svg-NQpEe0z4aTreOGrw .node polygon,#mermaid-svg-NQpEe0z4aTreOGrw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NQpEe0z4aTreOGrw .rough-node .label text,#mermaid-svg-NQpEe0z4aTreOGrw .node .label text,#mermaid-svg-NQpEe0z4aTreOGrw .image-shape .label,#mermaid-svg-NQpEe0z4aTreOGrw .icon-shape .label{text-anchor:middle;}#mermaid-svg-NQpEe0z4aTreOGrw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NQpEe0z4aTreOGrw .rough-node .label,#mermaid-svg-NQpEe0z4aTreOGrw .node .label,#mermaid-svg-NQpEe0z4aTreOGrw .image-shape .label,#mermaid-svg-NQpEe0z4aTreOGrw .icon-shape .label{text-align:center;}#mermaid-svg-NQpEe0z4aTreOGrw .node.clickable{cursor:pointer;}#mermaid-svg-NQpEe0z4aTreOGrw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NQpEe0z4aTreOGrw .arrowheadPath{fill:#333333;}#mermaid-svg-NQpEe0z4aTreOGrw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NQpEe0z4aTreOGrw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NQpEe0z4aTreOGrw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NQpEe0z4aTreOGrw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NQpEe0z4aTreOGrw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NQpEe0z4aTreOGrw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NQpEe0z4aTreOGrw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NQpEe0z4aTreOGrw .cluster text{fill:#333;}#mermaid-svg-NQpEe0z4aTreOGrw .cluster span{color:#333;}#mermaid-svg-NQpEe0z4aTreOGrw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NQpEe0z4aTreOGrw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NQpEe0z4aTreOGrw rect.text{fill:none;stroke-width:0;}#mermaid-svg-NQpEe0z4aTreOGrw .icon-shape,#mermaid-svg-NQpEe0z4aTreOGrw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NQpEe0z4aTreOGrw .icon-shape p,#mermaid-svg-NQpEe0z4aTreOGrw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NQpEe0z4aTreOGrw .icon-shape .label rect,#mermaid-svg-NQpEe0z4aTreOGrw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NQpEe0z4aTreOGrw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NQpEe0z4aTreOGrw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NQpEe0z4aTreOGrw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Vue3: Proxy
对象
整个对象 Proxy 代理
新增属性 ✅ 响应
数组索引 ✅ 响应
Vue2: Object.defineProperty
对象
逐属性 defineProperty
新增属性 ❌ 不响应
数组索引 ❌ 不响应
Proxy 的核心优势:
| 劫持粒度 | 单个属性 | 整个对象 |
| 新增属性 | ❌(需 Vue.set) | ✅ 原生支持 |
| 删除属性 | ❌(需 Vue.delete) | ✅ 原生支持 |
| 数组索引 | ❌ | ✅ 原生支持 |
| Map/Set | ❌ | ✅ 原生支持 |
| 性能 | 初始化时递归遍历 | 懒监听,按需代理 |
3.3 响应式的两个核心 API
Vue3 提供了 ref 和 reactive 两个核心 API:
// ref:基本类型(或需要整体替换的对象)
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0(模板中自动解包,脚本中需 .value)
// reactive:对象类型
import { reactive } from 'vue';
const state = reactive({
name: 'Vue3',
version: '3.5'
});
console.log(state.name); // 'Vue3'(无需 .value)
选型建议:
- 基本类型用 ref(number/string/boolean);
- 对象用 reactive(object/array);
- 不确定用 ref——更通用,且模板中自动解包。
3.4 响应式原理深度解析
Vue3 的响应式系统基于三个核心概念:Proxy、依赖收集、触发更新。
// Vue3 响应式原理(简化)
function reactive(target) {
return new Proxy(target, {
get(target, key) {
// 依赖收集:记录当前 effect 依赖了这个 key
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
// 触发更新:执行所有依赖这个 key 的 effect
trigger(target, key);
return result;
}
});
}
关键流程:
与 Vue2 的本质区别:
- Vue2:初始化时递归遍历所有属性,给每个属性 defineProperty——初始化慢;
- Vue3:懒代理——只有被访问的属性才会被 Proxy 代理——初始化快。
3.5 常见响应式陷阱
| 解构 reactive 丢失响应式 | reactive 解构后变成普通变量 | 用 toRefs 保持响应式 |
| 直接替换整个 reactive 对象 | 替换后原对象不再响应 | 用 ref 或 Object.assign |
| 数组索引赋值不更新 | Vue2 的问题,Vue3 已解决 | Vue3 原生支持 |
| 新增属性不更新 | Vue2 的问题,Vue3 已解决 | Vue3 原生支持 |
| 深层对象响应式性能差 | 深层 Proxy 递归代理 | 用 shallowReactive 浅层响应式 |
最佳实践:
// 正确:用 toRefs 保持解构后的响应式
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Vue3' });
const { count, name } = toRefs(state);
// count 和 name 仍是响应式的
// 正确:不确定用 ref
const count = ref(0);
count.value++; // 模板中自动解包
3.6 响应式系统的性能调优
Vue3 的响应式系统虽然比 Vue2 好很多,但在极端场景下仍有性能问题:
| 大型对象深层嵌套 | 深层 Proxy 递归代理,初始化慢 | shallowReactive 浅层响应式 |
| 频繁更新的大数组 | 每次更新都触发依赖收集 | shallowRef + 手动触发 |
| 第三方库对象 | 不需要响应式但被代理了 | markRaw 标记为不响应式 |
| 大量计算属性 | 每个 computed 都有依赖追踪 | 减少 computed 嵌套,用普通函数 |
性能调优最佳实践:
import { shallowReactive, markRaw } from 'vue';
// 1. 大型对象用 shallowReactive
const bigList = shallowReactive({ items: [] });
// 只有 bigList.items 的引用变化才触发更新,深层属性不追踪
// 2. 第三方对象用 markRaw
import Map from 'mapbox-gl';
const map = markRaw(new Map(container));
// map 不会被 Proxy 代理,性能更好
// 3. 频繁更新的数据用 shallowRef
const data = shallowRef(null);
// 手动触发更新
data.value = newData; // 只有引用变化才触发
什么时候需要调优:
- 渲染性能 profiling 显示响应式系统是瓶颈;
- 有大型数据对象(> 1000 个属性);
- 有频繁更新的动画/实时数据。
3.7 响应式系统的内部实现
Vue3 的响应式系统基于三个核心类:
| ReactiveEffect | 响应式副作用(组件渲染、watch、computed) |
| Dep | 依赖收集器(记录哪些 effect 依赖了这个 key) |
| RefImpl | ref 的实现类 |
依赖收集流程:
与 Vue2 的本质区别:
- Vue2:每个属性一个 dep.id,初始化时递归遍历所有属性;
- Vue3:每个对象的每个 key 一个 Dep,懒收集——只有被访问的 key 才会被追踪。
3.8 响应式系统的常见问题
| 1 | 数据变了视图没更新 | 解构 reactive 丢失响应式 | 用 toRefs |
| 2 | 新增属性不响应 | Vue2 的问题,Vue3 已解决 | Vue3 原生支持 |
| 3 | 深层对象性能差 | 深层 Proxy 递归代理 | 用 shallowReactive |
| 4 | 数组索引不响应 | Vue2 的问题,Vue3 已解决 | Vue3 原生支持 |
| 5 | ref 和 reactive 混用 | 类型不清晰 | 统一用 ref 或 reactive |
| 6 | 监听 props 不响应 | props 是只读的 | 用 watch(() => props.xxx) |
调试技巧:
import { isRef, isReactive, toRaw } from 'vue';
// 检查是不是 ref
console.log(isRef(count)); // true
// 检查是不是 reactive
console.log(isReactive(state)); // true
// 获取原始对象(跳过 Proxy)
const rawState = toRaw(state);
3.9 Vue 3.5 的响应式改进
Vue 3.5 对响应式系统做了重大改进:
| 结构重设计 | 用版本计数和双向链表重构响应式系统 |
| 数组追踪优化 | 优化数组的依赖收集和触发 |
| onEffectCleanup | 新 API,用于副作用清理 |
| watch 暂停/恢复 | 新功能,可以暂停和恢复 watch |
| SSR 改进 | 服务端渲染的响应式性能优化 |
onEffectCleanup 示例:
import { watchEffect, onEffectCleanup } from 'vue';
watchEffect((onCleanup) => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => { /* … */ });
// 清理:下次重新执行前调用
onCleanup(() => {
controller.abort(); // 取消上一次请求
});
});
4. Composition API 详解
4.1 什么是 Composition API
Composition API 是 Vue3 的核心新特性——它解决了 Options API 在大型应用中的代码组织问题:
// ========== Vue2 的 Options API ==========
// 同一个功能的代码散落在 data、methods、computed、watch 里
export default {
data() {
return { count: 0, user: null, loading: false };
},
methods: {
increment() { this.count++; },
async fetchUser() { /* … */ }
},
computed: {
doubleCount() { return this.count * 2; }
},
watch: {
count(newVal) { /* … */ }
}
};
// 问题:同一功能的代码散落在四个地方,大型组件难以维护
// ========== Vue3 的 Composition API ==========
// 同一功能的代码集中在一起,且可以提取为独立函数
import { ref, computed, watch } from 'vue';
function useCounter() {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() { count.value++; }
watch(count, (newVal) => { /* … */ });
return { count, doubleCount, increment };
}
function useUser() {
const user = ref(null);
const loading = ref(false);
async function fetchUser() { /* … */ }
return { user, loading, fetchUser };
}
export default {
setup() {
// 同一功能的代码集中使用
const { count, doubleCount, increment } = useCounter();
const { user, loading, fetchUser } = useUser();
return { count, doubleCount, increment, user, loading, fetchUser };
}
};
4.2 Composition API 的核心优势
| 代码组织 | 同一功能的代码集中在一起,不再散落在四个选项里 |
| 逻辑复用 | 提取为独立的 composable 函数,跨组件复用 |
| TypeScript | 天然支持类型推导,比 Options API 更好 |
| Tree-shaking | 未使用的 API 可以被打包工具移除 |
4.3 常用的 Composition API
| ref | 基本类型响应式 |
| reactive | 对象类型响应式 |
| computed | 计算属性 |
| watch | 侦听器 |
| watchEffect | 立即执行的侦听器 |
| onMounted/onUnmounted | 生命周期钩子 |
| provide/inject | 依赖注入 |
| defineProps/defineEmits | 组件通信(<script setup> 中) |
4.4 Composition API 实战示例:倒计时
// composables/useCountdown.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useCountdown(initialSeconds = 60) {
const seconds = ref(initialSeconds);
const isRunning = ref(false);
let timer = null;
function start() {
isRunning.value = true;
timer = setInterval(() => {
if (seconds.value > 0) {
seconds.value—;
} else {
stop();
}
}, 1000);
}
function stop() {
isRunning.value = false;
if (timer) {
clearInterval(timer);
timer = null;
}
}
function reset(newSeconds = initialSeconds) {
stop();
seconds.value = newSeconds;
}
onMounted(() => {
// 组件挂载时自动开始
start();
});
onUnmounted(() => {
// 组件卸载时清理定时器
stop();
});
return {
seconds,
isRunning,
start,
stop,
reset
};
}
在组件中使用:
<script setup>
import { useCountdown } from '@/composables/useCountdown';
const { seconds, isRunning, reset } = useCountdown(30);
</script>
<template>
<div>
<p>{{ seconds }} 秒</p>
<button @click="reset(30)" :disabled="isRunning">重置</button>
</div>
</template>
4.5 composable 提取实践
什么时候提取 composable:
| 同一组件内复用 | 不需要,函数即可 |
| 跨组件复用 | ✅ 提取为 composable |
| 有独立状态逻辑 | ✅ 提取为 composable |
| 与 UI 强耦合 | 不提取,保持组件内 |
composable 的命名规范:
- 以 use 开头(useUser、useCart、useCountdown);
- 函数式,接收参数,返回状态和方法;
- 不返回 UI,只返回逻辑。
4.6 Composition API vs Options API 选型
| 新项目 | Composition API + <script setup> | 官方推荐,长期方向 |
| 小型组件 | Options API 也可以 | 简单组件差异不大 |
| 大型组件 | Composition API | 代码组织优势明显 |
| 需要跨组件复用 | Composition API | composable 函数复用 |
| 团队熟悉 Vue2 | 渐进迁移 | 先 Options API 跑通,再逐步迁移 |
核心结论:Composition API 是 Vue3 的长期方向——新项目直接用,老项目按节奏迁移。
4.7 Composition API 的性能考虑
| setup 执行时机 | setup 在组件创建时执行一次,不要在里面做重计算 |
| computed 缓存 | computed 有缓存,依赖不变不重新计算 |
| watch 清理 | watch 要在 onUnmounted 里清理,避免内存泄漏 |
| effect 清理 | Vue 3.5 的 onEffectCleanup 用于副作用清理 |
性能最佳实践:
// 1. 重计算用 computed
const heavyComputed = computed(() => {
// 重计算只执行一次,缓存结果
return expensiveCalculation(props.data);
});
// 2. watch 要清理
watch(source, (newVal) => {
const timer = setInterval(() => {
// …
}, 1000);
onUnmounted(() => {
clearInterval(timer); // 清理定时器
});
});
// 3. 不要在 setup 里做异步操作
// 用 onMounted 或顶级 await
4.8 Composition API 的最佳实践
| 1 | 按功能组织代码 | 同一功能的代码集中在一起 |
| 2 | 提取 composable | 跨组件复用的逻辑提取为 composable |
| 3 | 使用 TypeScript | 类型推导更自然 |
| 4 | 避免过度抽象 | 不要为了提取而提取 |
| 5 | 命名规范 | composable 以 use 开头 |
| 6 | 单一职责 | 每个 composable 只做一件事 |
最佳实践示例:
// 好的 composable:单一职责
function useUser() {
const user = ref(null);
const loading = ref(false);
async function fetchUser(id) {
// …
}
return { user, loading, fetchUser };
}
// 不好的 composable:做了太多事
function useEverything() {
// 用户、购物车、设置、主题… 都放一起
// 违反单一职责
}
4.9 Composition API 与 React Hooks 对比
| 响应式 | 自动追踪(Proxy) | 手动追踪(useEffect 依赖数组) |
| 依赖数组 | 不需要 | 需要手动写 |
| 闭包问题 | 没有(Proxy 自动追踪) | 有(旧值闭包) |
| 生命周期 | onMounted/onUnmounted | useEffect/useLayoutEffect |
| 代码复用 | composable 函数 | 自定义 Hook |
| 学习曲线 | 平缓 | 陡峭 |
核心区别:Vue3 的响应式是自动的,React Hooks 的依赖是手动的——这是两者最大的区别,也是 Vue3 开发体验更好的根本原因。
5. <script setup> 语法糖
5.1 什么是 <script setup>
<script setup> 是 Vue3 的编译时语法糖——它让 Composition API 更简洁:
<!– 传统的 Composition API –>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() { count.value++; }
return { count, doubleCount, increment };
}
};
</script>
<!– <script setup> 语法糖 –>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() { count.value++; }
// 无需 return,顶层变量自动暴露给模板
</script>
5.2 <script setup> 的优势
| 更少样板 | 无需 export default 和 setup() |
| 自动暴露 | 顶层变量自动暴露给模板,无需 return |
| 更好 TS | 纯 TypeScript 声明 props 和 emits |
| 更好 IDE | 类型推导更快,更少 LSP 工作 |
5.3 Vue 3.5 的新特性:Reactive Props Destructure
Vue 3.5 新增了响应式 props 解构——解构 props 后仍保持响应式:
<script setup>
// Vue 3.5+:解构后仍保持响应式
const { name, age } = defineProps({
name: String,
age: Number
});
// 不需要 .value,且 name 和 age 仍是响应式的
console.log(name);
</script>
这是 Vue 3.5 的重要改进——以前解构 props 会丢失响应式,现在编译器自动处理。
5.4 <script setup> 进阶用法
1. 顶级 await 支持:
<script setup>
// 直接 await,编译器自动处理 Suspense
const post = await fetch(`/api/post/1`).then(r => r.json());
</script>
2. 自定义指令:
<script setup>
const vFocus = {
mounted: (el) => el.focus()
};
</script>
<template>
<input v-focus />
</template>
3. 命名组件:
<script setup>
// 不需要额外定义 name,文件名即组件名
// 如果需要,可以用 <script> 块补充
</script>
<script>
export default {
name: 'MyComponent'
};
</script>
4. 类型化 props 和 emits:
<script setup lang="ts">
// TypeScript 类型声明
interface Props {
name: string;
age?: number;
}
const props = withDefaults(defineProps<Props>(), {
age: 18
});
const emit = defineEmits<{
(e: 'update', value: string): void;
(e: 'delete', id: number): void;
}>();
</script>
5.5 <script setup> 常见问题
| 1 | 怎么定义组件 name? | 文件名即 name,或用 <script> 块补充 |
| 2 | 怎么获取 DOM 引用? | ref="xxx" + const xxx = ref(null) |
| 3 | 怎么使用第三方插件? | 在 main.js 中 app.use(plugin) |
| 4 | 怎么访问全局属性? | getCurrentInstance().appContext.config.globalProperties |
| 5 | 怎么定义异步组件? | defineAsyncComponent |
| 6 | 怎么处理 v-model? | defineModel()(Vue 3.4+) |
最佳实践:
<script setup>
import { ref, onMounted } from 'vue';
// DOM 引用
const inputRef = ref(null);
onMounted(() => {
inputRef.value.focus(); // 组件挂载后聚焦
});
</script>
<template>
<input ref="inputRef" />
</template>
5.6 <script setup> 与 Options API 混用
在迁移过程中,可能需要在同一个组件里混用 Options API 和 Composition API:
<script>
// 传统 Options API
export default {
name: 'MyComponent',
data() {
return { oldCount: 0 };
}
};
</script>
<script setup>
// Composition API
import { ref } from 'vue';
const newCount = ref(0);
</script>
混用注意事项:
- setup() 和 Options API 的 data/methods 是隔离的;
- 在 Options API 里可以访问 setup() 返回的变量(通过 this);
- 不推荐长期混用——迁移完成后应该全量 Composition API。
5.7 <script setup> 的最佳实践
| 1 | 使用 TypeScript | 类型推导更自然 |
| 2 | 顶层 await | 用 Suspense 处理加载状态 |
| 3 | 不要过度暴露 | 用 defineExpose 控制暴露的内容 |
| 4 | 命名组件 | 文件名即组件名,或用 <script> 块补充 |
| 5 | 自定义指令 | 顶层变量自动注册为自定义指令 |
| 6 | 类型化 props/emits | 用纯 TypeScript 声明 |
最佳实践示例:
<script setup lang="ts">
// 1. TypeScript 类型声明
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0
});
// 2. 类型化 emits
const emit = defineEmits<{
(e: 'submit', data: FormData): void;
(e: 'cancel'): void;
}>();
// 3. 暴露给父组件的内容
defineExpose({
focus: () => {
// 父组件可以通过 ref 调用
}
});
</script>
5.8 <script setup> 的常见陷阱
| 1 | 顶级 await 导致 hydration 失败 | SSR 中避免顶级 await |
| 2 | props 解构丢失响应式 | Vue 3.5+ 已修复,用 Reactive Props Destructure |
| 3 | 模板引用不工作 | 确保 ref 变量名和模板里的 ref 属性一致 |
| 4 | 自定义指令不生效 | 确保指令名以 v 开头 |
| 5 | defineProps 不提示 | 确保 VS Code 安装了 Volar 插件 |
| 6 | 类型不推导 | 确保 <script setup lang="ts"> |
最佳实践:
- 安装 Volar(Vue 官方 VS Code 插件);
- 使用 TypeScript 获得更好的类型推导;
- 保持组件简洁,不要在一个组件里写太多逻辑。
6. 组件通信:Props/Emits/Slots
6.1 Props:父传子
<!– 子组件 –>
<script setup>
const props = defineProps({
name: {
type: String,
required: true,
default: 'Vue3'
},
count: {
type: Number,
default: 0
}
});
</script>
<!– 父组件 –>
<ChildComponent name="张三" :count="10" />
6.2 Emits:子传父
<!– 子组件 –>
<script setup>
const emit = defineEmits(['update', 'delete']);
function handleUpdate() {
emit('update', { id: 1, value: 'new' });
}
</script>
<!– 父组件 –>
<ChildComponent @update="handleUpdate" @delete="handleDelete" />
6.3 Slots:内容分发
<!– 子组件 –>
<template>
<div>
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
</template>
<!– 父组件 –>
<ChildComponent>
<template #header>标题</template>
<template #default>内容</template>
<template #footer>底部</template>
</ChildComponent>
6.4 provide/inject:依赖注入
当组件层级很深时,props 一层层传递很麻烦——provide/inject 让祖先组件直接向后代组件提供数据:
<!– 祖先组件 –>
<script setup>
import { provide, ref } from 'vue';
const theme = ref('dark');
const user = ref({ name: '张三', role: 'admin' });
// 提供给所有后代组件
provide('theme', theme);
provide('user', user);
</script>
<!– 深层后代组件 –>
<script setup>
import { inject, watch } from 'vue';
// 注入祖先提供的数据
const theme = inject('theme');
const user = inject('user');
console.log(theme.value); // 'dark'
</script>
最佳实践:
- 用 provide/inject 做跨层级共享,不要滥用(破坏组件封装);
- 用 Symbol 作为 key,避免命名冲突;
- 响应式数据用 ref/reactive 提供,注入后保持响应式。
6.5 组件通信选型决策
#mermaid-svg-8X83K7MuqhNlANqT{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8X83K7MuqhNlANqT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8X83K7MuqhNlANqT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8X83K7MuqhNlANqT .error-icon{fill:#552222;}#mermaid-svg-8X83K7MuqhNlANqT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8X83K7MuqhNlANqT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8X83K7MuqhNlANqT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8X83K7MuqhNlANqT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8X83K7MuqhNlANqT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8X83K7MuqhNlANqT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8X83K7MuqhNlANqT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8X83K7MuqhNlANqT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8X83K7MuqhNlANqT .marker.cross{stroke:#333333;}#mermaid-svg-8X83K7MuqhNlANqT svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8X83K7MuqhNlANqT p{margin:0;}#mermaid-svg-8X83K7MuqhNlANqT .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-8X83K7MuqhNlANqT .cluster-label text{fill:#333;}#mermaid-svg-8X83K7MuqhNlANqT .cluster-label span{color:#333;}#mermaid-svg-8X83K7MuqhNlANqT .cluster-label span p{background-color:transparent;}#mermaid-svg-8X83K7MuqhNlANqT .label text,#mermaid-svg-8X83K7MuqhNlANqT span{fill:#333;color:#333;}#mermaid-svg-8X83K7MuqhNlANqT .node rect,#mermaid-svg-8X83K7MuqhNlANqT .node circle,#mermaid-svg-8X83K7MuqhNlANqT .node ellipse,#mermaid-svg-8X83K7MuqhNlANqT .node polygon,#mermaid-svg-8X83K7MuqhNlANqT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8X83K7MuqhNlANqT .rough-node .label text,#mermaid-svg-8X83K7MuqhNlANqT .node .label text,#mermaid-svg-8X83K7MuqhNlANqT .image-shape .label,#mermaid-svg-8X83K7MuqhNlANqT .icon-shape .label{text-anchor:middle;}#mermaid-svg-8X83K7MuqhNlANqT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8X83K7MuqhNlANqT .rough-node .label,#mermaid-svg-8X83K7MuqhNlANqT .node .label,#mermaid-svg-8X83K7MuqhNlANqT .image-shape .label,#mermaid-svg-8X83K7MuqhNlANqT .icon-shape .label{text-align:center;}#mermaid-svg-8X83K7MuqhNlANqT .node.clickable{cursor:pointer;}#mermaid-svg-8X83K7MuqhNlANqT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8X83K7MuqhNlANqT .arrowheadPath{fill:#333333;}#mermaid-svg-8X83K7MuqhNlANqT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8X83K7MuqhNlANqT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8X83K7MuqhNlANqT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8X83K7MuqhNlANqT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8X83K7MuqhNlANqT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8X83K7MuqhNlANqT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8X83K7MuqhNlANqT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8X83K7MuqhNlANqT .cluster text{fill:#333;}#mermaid-svg-8X83K7MuqhNlANqT .cluster span{color:#333;}#mermaid-svg-8X83K7MuqhNlANqT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8X83K7MuqhNlANqT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8X83K7MuqhNlANqT rect.text{fill:none;stroke-width:0;}#mermaid-svg-8X83K7MuqhNlANqT .icon-shape,#mermaid-svg-8X83K7MuqhNlANqT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8X83K7MuqhNlANqT .icon-shape p,#mermaid-svg-8X83K7MuqhNlANqT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8X83K7MuqhNlANqT .icon-shape .label rect,#mermaid-svg-8X83K7MuqhNlANqT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8X83K7MuqhNlANqT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8X83K7MuqhNlANqT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8X83K7MuqhNlANqT :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
是
否
是
否
否
是
否
是
否
组件通信需求
父子组件?
父→子?
Props
子→父?
Emits
v-model / 双向绑定
跨层级?
provide/inject
全局状态?
Pinia
事件总线 / 状态管理
选型建议:
| Props | 父→子,简单数据流 |
| Emits | 子→父,事件通知 |
| v-model | 父子双向绑定(表单) |
| provide/inject | 跨层级共享(主题、用户信息) |
| Pinia | 全局状态(用户、购物车、设置) |
| 事件总线 | 跨组件事件(已少用,用 Pinia 替代) |
6.6 组件设计原则
| 单一职责 | 每个组件只做一件事 |
| Props 下行,Events 上行 | 数据从父到子,事件从子到父 |
| 组件粒度 | 不要太大,也不要太小 |
| 可复用性 | 组件要可复用、可配置 |
| 组合优于继承 | 用插槽和组合替代继承 |
组件拆分示例:
UserProfile.vue(大组件)
├── UserAvatar.vue(头像)
├── UserInfo.vue(用户信息)
├── UserActions.vue(操作按钮)
└── UserOrders.vue(订单列表)
为什么要拆分:
- 更好的复用性;
- 更好的可维护性;
- 更好的测试性;
- 更好的性能(细粒度更新)。
6.7 组件通信的最佳实践
| 1 | Props 要类型化 | 用 TypeScript 声明 Props 类型 |
| 2 | Emits 要明确 | 声明所有可能的事件 |
| 3 | 避免 prop drilling | 用 provide/inject 或 Pinia 替代 |
| 4 | 不要滥用 provide/inject | 只用于跨层级共享 |
| 5 | 全局状态用 Pinia | 不要用 props 传全局状态 |
| 6 | 组件要单一职责 | 不要做太多事 |
最佳实践示例:
<script setup lang="ts">
// 好的:类型化 Props 和 Emits
interface Props {
title: string;
visible: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: true
});
const emit = defineEmits<{
(e: 'close'): void;
(e: 'confirm', value: string): void;
}>();
</script>
7. Pinia 状态管理
7.1 从 Vuex 到 Pinia
Pinia 是 Vue3 的官方推荐状态管理库——它替代了 Vuex:
| API 风格 | mutations + actions | 直接 actions |
| TypeScript | 支持有限 | 原生支持 |
| 模块化 | modules 嵌套 | 扁平 store |
| DevTools | 支持 | 支持(更好的时间旅行) |
| 体积 | 较大 | 更小 |
7.2 Pinia 的核心概念
// stores/counter.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0);
// getters
const doubleCount = computed(() => count.value * 2);
// actions
function increment() {
count.value++;
}
function reset() {
count.value = 0;
}
return { count, doubleCount, increment, reset };
});
7.3 在组件中使用 Pinia
<script setup>
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
// 直接使用
console.log(counter.count); // 0
console.log(counter.doubleCount); // 0
// 调用 action
counter.increment();
console.log(counter.count); // 1
</script>
7.4 Pinia 实战:用户 Store
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { loginApi, logoutApi, getUserInfoApi } from '@/api/user';
export const useUserStore = defineStore('user', () => {
// state
const token = ref(localStorage.getItem('token') || '');
const userInfo = ref(null);
// getters
const isLoggedIn = computed(() => !!token.value);
const userName = computed(() => userInfo.value?.name || '');
const isAdmin = computed(() => userInfo.value?.role === 'admin');
// actions
async function login(loginForm) {
const res = await loginApi(loginForm);
token.value = res.token;
localStorage.setItem('token', res.token);
await fetchUserInfo();
return res;
}
async function fetchUserInfo() {
if (!token.value) return;
userInfo.value = await getUserInfoApi();
}
async function logout() {
await logoutApi();
token.value = '';
userInfo.value = null;
localStorage.removeItem('token');
}
return {
token,
userInfo,
isLoggedIn,
userName,
isAdmin,
login,
fetchUserInfo,
logout
};
});
7.5 Pinia 持久化插件
// plugins/piniaPersist.js
export function piniaPersist({ store }) {
// 监听 store 变化,自动持久化到 localStorage
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
// 初始化时从 localStorage 恢复
const saved = localStorage.getItem(store.$id);
if (saved) {
store.$patch(JSON.parse(saved));
}
}
// main.js
import { createPinia } from 'pinia';
import { piniaPersist } from '@/plugins/piniaPersist';
const pinia = createPinia();
pinia.use(piniaPersist);
7.6 Pinia 最佳实践
| 1 | 按功能拆分 store | 不要把所有状态放一个 store |
| 2 | 使用组合式语法 | defineStore('id', () => {}) 比选项式更灵活 |
| 3 | 持久化按需 | 只持久化需要持久化的 store |
| 4 | 不要滥用 store | 组件局部状态用 ref/reactive,全局状态才用 Pinia |
| 5 | 服务端交互放 action | API 请求放在 action 里,不要放组件里 |
| 6 | getter 纯函数 | getter 不要有副作用,只做计算 |
常见误区:
- 所有状态都放 Pinia:错——组件局部状态用 ref/reactive 更轻量;
- store 里放 UI 状态:错——loading、modal 开关等 UI 状态应该放组件里;
- 直接修改 state:可以,但大型项目建议用 action 统一修改。
7.7 Pinia 与服务端交互
最佳实践:API 请求放在 Pinia 的 action 里:
// stores/user.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import api from '@/api';
export const useUserStore = defineStore('user', () => {
const userList = ref([]);
const loading = ref(false);
const error = ref(null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
const res = await api.get('/users');
userList.value = res.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
async function createUser(userData) {
loading.value = true;
try {
const res = await api.post('/users', userData);
userList.value.push(res.data);
return res.data;
} catch (err) {
error.value = err;
throw err;
} finally {
loading.value = false;
}
}
return {
userList,
loading,
error,
fetchUsers,
createUser
};
});
在组件中使用:
<script setup>
import { useUserStore } from '@/stores/user';
import { onMounted } from 'vue';
const userStore = useUserStore();
onMounted(() => {
userStore.fetchUsers();
});
</script>
7.8 Pinia 的 DevTools 与调试
Vue DevTools 是调试 Pinia 的利器:
| 时间旅行 | 回溯到任意时间点的状态 |
| 状态检查 | 查看每个 store 的当前状态 |
| Action 追踪 | 查看 action 的调用栈和参数 |
| 状态编辑 | 直接在 DevTools 里修改状态 |
调试技巧:
// 1. 在 store 里打印状态
console.log(userStore.$state);
// 2. 订阅 store 变化
userStore.$subscribe((mutation, state) => {
console.log('User store changed:', mutation, state);
});
// 3. 重置 store
userStore.$reset();
常见问题:
- store 不响应:检查是否在 setup 外使用;
- 状态不同步:检查是否直接修改 state;
- DevTools 不显示:检查 Vue DevTools 版本是否支持 Pinia。
7.9 Pinia 的常见陷阱
| 1 | store 不响应 | 确保在 setup 或组件里使用 |
| 2 | 直接修改 state 不触发 | Pinia 允许直接修改,但大型项目建议用 action |
| 3 | store 之间互相引用 | 可以,但要注意循环依赖 |
| 4 | 持久化不生效 | 检查插件配置和存储 key |
| 5 | SSR 中 store 不共享 | 用 Nuxt 的 useState 替代 |
| 6 | TypeScript 类型不推导 | 确保 store 有正确的类型标注 |
最佳实践:
- 按功能拆分 store:不要把所有状态放一个 store;
- 使用组合式语法:defineStore('id', () => {}) 比选项式更灵活;
- API 请求放 action:不要在组件里直接调 API;
- getter 纯函数:getter 不要有副作用,只做计算。
8. Vite 构建工具
8.1 为什么是 Vite
Vite 是 Vue3 推荐的构建工具——它解决了 webpack 的启动慢问题:
| 启动时间 | 慢(打包后启动) | 极快(原生 ESM,无需打包) |
| 热更新 | 慢(重新打包) | 极快(按需编译) |
| 构建 | webpack | Rollup(生产构建) |
| 配置 | 复杂 | 简单 |
8.2 Vite 的核心优势
#mermaid-svg-IFGjd4LaRzUIWpci{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IFGjd4LaRzUIWpci .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IFGjd4LaRzUIWpci .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IFGjd4LaRzUIWpci .error-icon{fill:#552222;}#mermaid-svg-IFGjd4LaRzUIWpci .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IFGjd4LaRzUIWpci .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IFGjd4LaRzUIWpci .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IFGjd4LaRzUIWpci .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IFGjd4LaRzUIWpci .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IFGjd4LaRzUIWpci .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IFGjd4LaRzUIWpci .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IFGjd4LaRzUIWpci .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IFGjd4LaRzUIWpci .marker.cross{stroke:#333333;}#mermaid-svg-IFGjd4LaRzUIWpci svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IFGjd4LaRzUIWpci p{margin:0;}#mermaid-svg-IFGjd4LaRzUIWpci .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-IFGjd4LaRzUIWpci .cluster-label text{fill:#333;}#mermaid-svg-IFGjd4LaRzUIWpci .cluster-label span{color:#333;}#mermaid-svg-IFGjd4LaRzUIWpci .cluster-label span p{background-color:transparent;}#mermaid-svg-IFGjd4LaRzUIWpci .label text,#mermaid-svg-IFGjd4LaRzUIWpci span{fill:#333;color:#333;}#mermaid-svg-IFGjd4LaRzUIWpci .node rect,#mermaid-svg-IFGjd4LaRzUIWpci .node circle,#mermaid-svg-IFGjd4LaRzUIWpci .node ellipse,#mermaid-svg-IFGjd4LaRzUIWpci .node polygon,#mermaid-svg-IFGjd4LaRzUIWpci .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IFGjd4LaRzUIWpci .rough-node .label text,#mermaid-svg-IFGjd4LaRzUIWpci .node .label text,#mermaid-svg-IFGjd4LaRzUIWpci .image-shape .label,#mermaid-svg-IFGjd4LaRzUIWpci .icon-shape .label{text-anchor:middle;}#mermaid-svg-IFGjd4LaRzUIWpci .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IFGjd4LaRzUIWpci .rough-node .label,#mermaid-svg-IFGjd4LaRzUIWpci .node .label,#mermaid-svg-IFGjd4LaRzUIWpci .image-shape .label,#mermaid-svg-IFGjd4LaRzUIWpci .icon-shape .label{text-align:center;}#mermaid-svg-IFGjd4LaRzUIWpci .node.clickable{cursor:pointer;}#mermaid-svg-IFGjd4LaRzUIWpci .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IFGjd4LaRzUIWpci .arrowheadPath{fill:#333333;}#mermaid-svg-IFGjd4LaRzUIWpci .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IFGjd4LaRzUIWpci .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IFGjd4LaRzUIWpci .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IFGjd4LaRzUIWpci .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IFGjd4LaRzUIWpci .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IFGjd4LaRzUIWpci .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IFGjd4LaRzUIWpci .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IFGjd4LaRzUIWpci .cluster text{fill:#333;}#mermaid-svg-IFGjd4LaRzUIWpci .cluster span{color:#333;}#mermaid-svg-IFGjd4LaRzUIWpci div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IFGjd4LaRzUIWpci .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IFGjd4LaRzUIWpci rect.text{fill:none;stroke-width:0;}#mermaid-svg-IFGjd4LaRzUIWpci .icon-shape,#mermaid-svg-IFGjd4LaRzUIWpci .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IFGjd4LaRzUIWpci .icon-shape p,#mermaid-svg-IFGjd4LaRzUIWpci .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IFGjd4LaRzUIWpci .icon-shape .label rect,#mermaid-svg-IFGjd4LaRzUIWpci .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IFGjd4LaRzUIWpci .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IFGjd4LaRzUIWpci .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IFGjd4LaRzUIWpci :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Vite 开发模式
启动
启动 dev server
按需编译(浏览器请求哪个文件编译哪个)
快(毫秒级)
webpack 开发模式
启动
打包整个应用
启动 dev server
慢(大型应用数分钟)
核心原理:Vite 利用浏览器原生的 ES Module——开发时不需要打包,浏览器请求哪个文件,Vite 就编译哪个文件,启动时间与应用大小无关。
8.3 创建 Vue3 项目
# 使用 create-vue(官方脚手架)
npm create vue@latest my-app
# 或使用 Vite
npm create vite@latest my-app — –template vue
8.4 Vite 配置与优化
vite.config.js 常用配置:
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,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\\/api/, '')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
}
}
});
Vite 优化要点:
| 路径别名 | @ 指向 src,简化导入路径 |
| 开发代理 | 解决开发环境跨域问题 |
| 代码分割 | manualChunks 拆分 vendor 包 |
| 生产 sourcemap | 生产环境关闭,减小体积 |
| 环境变量 | .env.development / .env.production |
8.5 Vite 常见问题
| 1 | 开发环境跨域 | 配置 server.proxy 代理 |
| 2 | 路径别名不生效 | 配置 resolve.alias |
| 3 | 生产构建慢 | 配置 manualChunks 拆分 vendor |
| 4 | 静态资源路径不对 | 配置 base 为子路径 |
| 5 | HMR 不生效 | 检查文件路径和缓存 |
| 6 | 环境变量不生效 | 用 import.meta.env.VITE_XXX |
环境变量使用:
// .env.development
VITE_API_BASE_URL=http://localhost:8080/api
// .env.production
VITE_API_BASE_URL=https://api.example.com
// 代码中使用
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
8.6 Vite 与 webpack 迁移
迁移步骤:
| 1 | 安装 Vite + @vitejs/plugin-vue |
| 2 | 创建 vite.config.js |
| 3 | 迁移 index.html 到根目录 |
| 4 | 迁移静态资源到 public/ |
| 5 | 替换 webpack 特有的 loader/plugin |
| 6 | 测试开发环境和生产构建 |
常见迁移坑:
| require 不支持 | 用 import 替代 |
| require.context 不支持 | 用 import.meta.glob 替代 |
| process.env 不支持 | 用 import.meta.env 替代 |
| 静态资源路径不对 | 迁移到 public/ 或用 import 引入 |
8.7 Vite 的插件生态
| @vitejs/plugin-vue | Vue 3 支持 |
| @vitejs/plugin-vue-jsx | JSX 支持 |
| @vitejs/plugin-react | React 支持 |
| vite-plugin-windicss | Windi CSS(原子化 CSS) |
| vite-plugin-mock | Mock 数据 |
| vite-plugin-compression | 生产构建压缩 |
| vite-plugin-pwa | PWA 支持 |
常用插件配置:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import WindiCSS from 'vite-plugin-windicss';
export default defineConfig({
plugins: [
vue(),
vueJsx(),
WindiCSS()
]
});
8.8 Vite 的性能优化
| 依赖预构建 | Vite 自动预构建依赖,提升冷启动速度 |
| 代码分割 | manualChunks 拆分 vendor 包 |
| 懒加载 | 路由级懒加载,按需加载 |
| 压缩 | 生产构建自动压缩(gzip/brotli) |
| CDN | 静态资源用 CDN 加速 |
| 缓存 | 利用浏览器缓存,提升二次访问速度 |
性能优化最佳实践:
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 拆分第三方库
vendor: ['vue', 'vue-router', 'pinia'],
// 拆分 UI 组件库
ui: ['element-plus', 'ant-design-vue']
}
}
},
// 构建压缩
minify: 'terser',
terserOptions: {
compress: {
drop_console: true // 生产环境移除 console
}
}
}
});
9. Vue Router 4
9.1 Vue Router 4 的变化
Vue Router 4 是 Vue3 对应的路由版本:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
9.2 在组件中使用路由
<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
// 编程式导航
function goAbout() {
router.push('/about');
}
// 获取路由参数
console.log(route.params.id);
</script>
9.3 Vue Router 4 进阶:导航守卫
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/user';
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true } // 需要登录
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, requiresAdmin: true } // 需要管理员
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore();
// 需要登录的页面
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next('/login');
}
// 需要管理员的页面
else if (to.meta.requiresAdmin && !userStore.isAdmin) {
next('/dashboard');
}
// 正常放行
else {
next();
}
});
export default router;
路由懒加载:
// 路由级代码分割,按需加载
const routes = [
{
path: '/about',
component: () => import('@/views/About.vue') // 懒加载
}
];
9.4 Vue Router 4 常见问题
| 1 | 路由守卫不生效 | 检查守卫注册位置和 next() 调用 |
| 2 | 路由参数不响应 | 用 watch(() => route.params.id) 监听 |
| 3 | 动态路由不更新 | 给 router-view 加 :key="$route.fullPath" |
| 4 | 路由跳转死循环 | 检查守卫条件,避免无限重定向 |
| 5 | 404 页面不生效 | 配置 path: '/:pathMatch(.*)*' 通配路由 |
| 6 | 路由元信息不生效 | 检查 meta 配置和类型声明 |
最佳实践:
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
// 监听路由参数变化
watch(() => route.params.id, (newId) => {
// 根据新 id 重新加载数据
loadData(newId);
});
</script>
9.5 Vue Router 的懒加载与代码分割
路由懒加载是优化首屏加载的关键手段:
// 1. 路由级懒加载
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
}
];
// 2. 按组分包
const routes = [
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
{
path: 'users',
component: () => import('@/views/admin/Users.vue')
},
{
path: 'settings',
component: () => import('@/views/admin/Settings.vue')
}
]
}
];
// 3. 预加载(prefetch)
const Dashboard = defineAsyncComponent({
loader: () => import('@/views/Dashboard.vue'),
loadingComponent: LoadingSpinner,
delay: 200, // 200ms 后显示 loading
timeout: 3000 // 3s 超时
});
代码分割最佳实践:
| 路由级懒加载 | 每个路由一个 chunk |
| 按组分包 | 相关路由放一个 chunk |
| 大组件懒加载 | 弹窗、表格等大组件懒加载 |
| 第三方库分包 | vendor 包单独打包 |
10. Nuxt 3 SSR/元框架
10.1 什么是 Nuxt
Nuxt 是基于 Vue3 的元框架(Meta-framework)——它提供 SSR(服务端渲染)、SSG(静态站点生成)、文件路由等能力:
| SSR | 服务端渲染,首屏更快 |
| SSG | 静态站点生成,SEO 友好 |
| 文件路由 | 文件即路由,无需配置 |
| 自动导入 | 组件、composables 自动导入 |
| 布局系统 | 可复用的布局组件 |
10.2 Nuxt 3 的核心优势
#mermaid-svg-moR14p6xOPQC7yBq{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-moR14p6xOPQC7yBq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-moR14p6xOPQC7yBq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-moR14p6xOPQC7yBq .error-icon{fill:#552222;}#mermaid-svg-moR14p6xOPQC7yBq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-moR14p6xOPQC7yBq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-moR14p6xOPQC7yBq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-moR14p6xOPQC7yBq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-moR14p6xOPQC7yBq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-moR14p6xOPQC7yBq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-moR14p6xOPQC7yBq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-moR14p6xOPQC7yBq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-moR14p6xOPQC7yBq .marker.cross{stroke:#333333;}#mermaid-svg-moR14p6xOPQC7yBq svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-moR14p6xOPQC7yBq p{margin:0;}#mermaid-svg-moR14p6xOPQC7yBq .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-moR14p6xOPQC7yBq .cluster-label text{fill:#333;}#mermaid-svg-moR14p6xOPQC7yBq .cluster-label span{color:#333;}#mermaid-svg-moR14p6xOPQC7yBq .cluster-label span p{background-color:transparent;}#mermaid-svg-moR14p6xOPQC7yBq .label text,#mermaid-svg-moR14p6xOPQC7yBq span{fill:#333;color:#333;}#mermaid-svg-moR14p6xOPQC7yBq .node rect,#mermaid-svg-moR14p6xOPQC7yBq .node circle,#mermaid-svg-moR14p6xOPQC7yBq .node ellipse,#mermaid-svg-moR14p6xOPQC7yBq .node polygon,#mermaid-svg-moR14p6xOPQC7yBq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-moR14p6xOPQC7yBq .rough-node .label text,#mermaid-svg-moR14p6xOPQC7yBq .node .label text,#mermaid-svg-moR14p6xOPQC7yBq .image-shape .label,#mermaid-svg-moR14p6xOPQC7yBq .icon-shape .label{text-anchor:middle;}#mermaid-svg-moR14p6xOPQC7yBq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-moR14p6xOPQC7yBq .rough-node .label,#mermaid-svg-moR14p6xOPQC7yBq .node .label,#mermaid-svg-moR14p6xOPQC7yBq .image-shape .label,#mermaid-svg-moR14p6xOPQC7yBq .icon-shape .label{text-align:center;}#mermaid-svg-moR14p6xOPQC7yBq .node.clickable{cursor:pointer;}#mermaid-svg-moR14p6xOPQC7yBq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-moR14p6xOPQC7yBq .arrowheadPath{fill:#333333;}#mermaid-svg-moR14p6xOPQC7yBq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-moR14p6xOPQC7yBq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-moR14p6xOPQC7yBq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-moR14p6xOPQC7yBq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-moR14p6xOPQC7yBq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-moR14p6xOPQC7yBq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-moR14p6xOPQC7yBq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-moR14p6xOPQC7yBq .cluster text{fill:#333;}#mermaid-svg-moR14p6xOPQC7yBq .cluster span{color:#333;}#mermaid-svg-moR14p6xOPQC7yBq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-moR14p6xOPQC7yBq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-moR14p6xOPQC7yBq rect.text{fill:none;stroke-width:0;}#mermaid-svg-moR14p6xOPQC7yBq .icon-shape,#mermaid-svg-moR14p6xOPQC7yBq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-moR14p6xOPQC7yBq .icon-shape p,#mermaid-svg-moR14p6xOPQC7yBq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-moR14p6xOPQC7yBq .icon-shape .label rect,#mermaid-svg-moR14p6xOPQC7yBq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-moR14p6xOPQC7yBq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-moR14p6xOPQC7yBq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-moR14p6xOPQC7yBq :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Nuxt SSR
浏览器请求
服务端渲染 HTML
直接返回完整 HTML
首屏快 + SEO 好
传统 SPA
浏览器请求 HTML
下载 JS Bundle
浏览器渲染
首屏慢 + SEO 差
适用场景:
- 内容型网站(博客、文档、营销页):SSG 静态生成;
- 电商/门户:SSR 服务端渲染,首屏快;
- 大型应用:Nuxt 的文件路由和自动导入提升开发效率。
10.3 Nuxt 3 实战:目录结构
my-nuxt-app/
├── assets/ # 未编译的资源(样式、图片)
├── components/ # 组件(自动导入)
├── composables/ # composable 函数(自动导入)
├── layouts/ # 布局组件
├── middleware/ # 路由中间件
├── pages/ # 页面(文件路由)
├── plugins/ # 插件
├── public/ # 静态文件
├── server/ # 服务端 API
└── nuxt.config.ts # Nuxt 配置
文件路由示例:
pages/
├── index.vue # → /
├── about.vue # → /about
├── products/
│ ├── index.vue # → /products
│ └── [id].vue # → /products/:id
└── blog/
└── [slug].vue # → /blog/:slug
自动导入:
<script setup>
// 不需要 import,Nuxt 自动导入
const products = ref([]);
// useFetch 是 Nuxt 内置的 composable
const { data: posts } = await useFetch('/api/posts');
</script>
10.4 Nuxt 3 常见问题
| 1 | 服务端/客户端不兼容 | 用 process.server 或 client-only 组件 |
| 2 | SSR hydration 失败 | 检查模板不一致,避免服务端渲染时访问 window |
| 3 | API 请求重复执行 | 用 useFetch 的缓存策略 |
| 4 | 路由守卫不生效 | 用 middleware/ 目录的中间件 |
| 5 | 静态生成失败 | 检查 nuxt generate 错误日志 |
| 6 | SEO 不友好 | 用 useHead 设置 meta 标签 |
最佳实践:
<script setup>
// SSR 兼容:只在客户端执行
onMounted(() => {
// 这里的代码只在客户端执行
initChart();
});
// SEO:设置页面标题和 meta
useHead({
title: '首页 – My App',
meta: [
{ name: 'description', content: '这是我的 Nuxt 3 应用' }
]
});
</script>
10.5 Nuxt 3 的部署方案
| Node.js 服务器 | 运行 node .output/server/index.mjs | SSR 应用 |
| Vercel/Netlify | 自动部署 | 静态站点/SSR |
| Docker | 容器化部署 | 企业级部署 |
| 静态生成 | nuxt generate 生成静态文件 | 博客/文档 |
部署示例:
# 1. 构建生产版本
npm run build
# 2. 启动生产服务器
node .output/server/index.mjs
# 3. 静态生成
npm run generate
# 输出到 .output/public/
部署注意事项:
- 环境变量要正确配置;
- 服务端和客户端的 API 地址要区分;
- 静态资源路径要正确配置;
- SSR 应用需要 Node.js 运行环境。
11. 新特性:Teleport/Suspense/Fragment
11.1 Teleport:传送门
Teleport 让你把组件渲染到 DOM 的其他位置——常用于模态框、通知:
<template>
<button @click="showModal = true">打开模态框</button>
<!– 模态框渲染到 body 下,而不是组件内部 –>
<Teleport to="body">
<div v-if="showModal" class="modal">
模态框内容
<button @click="showModal = false">关闭</button>
</div>
</Teleport>
</template>
11.2 Suspense:异步组件
Suspense 让你处理异步组件的加载状态:
<template>
<Suspense>
<!– 异步组件 –>
<template #default>
<AsyncComponent />
</template>
<!– 加载中显示 –>
<template #fallback>
<div>加载中…</div>
</template>
</Suspense>
</template>
11.3 Fragment:多根节点
Vue3 支持组件模板有多个根节点——不再需要包裹一层 <div>:
<!– Vue2:必须单根 –>
<template>
<div>
<header>…</header>
<main>…</main>
<footer>…</footer>
</div>
</template>
<!– Vue3:多根节点(Fragment) –>
<template>
<header>…</header>
<main>…</main>
<footer>…</footer>
</template>
11.4 v-model 多参数与新特性
Vue3 的 v-model 支持多参数——一个组件可以有多个 v-model:
<!– 子组件 –>
<script setup>
const modelValue = defineModel(); // 默认 v-model
const title = defineModel('title'); // v-model:title
</script>
<template>
<input v-model="modelValue" />
<input v-model="title" />
</template>
<!– 父组件 –>
<ChildComponent v-model="content" v-model:title="pageTitle" />
其他新特性:
| defineModel() | 简化 v-model 声明 |
| defineSlots() | 类型化插槽 |
| defineExpose() | 暴露组件内部方法 |
| useTemplateRef() | 获取模板引用(Vue 3.5+) |
| onEffectCleanup() | 副作用清理(Vue 3.5+) |
12. Vue3 性能优化
12.1 编译时优化
Vue3 的编译器做了大量优化:
| 静态提升 | 静态节点提升到渲染函数外,避免重复创建 |
| Patch Flag | 动态节点标记,只更新变化的部分 |
| Block Tree | 树状结构,跳过静态子树 |
| 缓存事件 | 事件处理函数缓存,避免重复绑定 |
12.2 响应式优化
| shallowRef/shallowReactive | 浅层响应式,只监听顶层 |
| markRaw | 标记对象为"不响应式" |
| computed 缓存 | 计算属性自动缓存,依赖不变不重新计算 |
12.3 运行时优化
| v-once | 只渲染一次,后续不更新 |
| v-memo | 记忆组件,依赖不变不重新渲染 |
| 异步组件 | 路由级代码分割 |
| 懒加载 | 组件按需加载 |
12.4 性能优化实战案例
案例 1:大型列表渲染优化
<!– 优化前:每次滚动都重新渲染 –>
<template>
<div v-for="item in largeList" :key="item.id">
{{ item.name }}
</div>
</template>
<!– 优化后:虚拟滚动,只渲染可视区域 –>
<template>
<RecycleScroller
:items="largeList"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div>{{ item.name }}</div>
</RecycleScroller>
</template>
案例 2:图片懒加载
<script setup>
// 使用 Intersection Observer 实现图片懒加载
const imgRef = ref(null);
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
observer.observe(imgRef.value);
});
</script>
<template>
<img ref="imgRef" data-src="/large-image.jpg" alt="…" />
</template>
性能优化检查清单:
| 1 | 路由懒加载 | ⭐⭐⭐⭐⭐ |
| 2 | 组件懒加载 | ⭐⭐⭐⭐ |
| 3 | 虚拟滚动(长列表) | ⭐⭐⭐⭐ |
| 4 | 图片懒加载 | ⭐⭐⭐⭐ |
| 5 | v-memo 优化大列表 | ⭐⭐⭐ |
| 6 | shallowRef/shallowReactive | ⭐⭐⭐ |
| 7 | markRaw 跳过响应式 | ⭐⭐ |
12.5 性能监控与分析
性能指标:
| FCP(First Contentful Paint) | 首次内容绘制 | < 1.8s |
| LCP(Largest Contentful Paint) | 最大内容绘制 | < 2.5s |
| FID(First Input Delay) | 首次输入延迟 | < 100ms |
| CLS(Cumulative Layout Shift) | 累积布局偏移 | < 0.1 |
性能分析工具:
| Chrome DevTools Performance | 分析渲染性能 |
| Vue DevTools | 分析组件渲染次数 |
| Lighthouse | 综合性能评分 |
| Web Vitals | 监控核心指标 |
性能优化流程:
12.6 常见性能问题与解决方案
| 1 | 首屏加载慢 | 路由懒加载、代码分割、静态生成 |
| 2 | 列表渲染慢 | 虚拟滚动、v-memo、shallowRef |
| 3 | 响应式更新慢 | shallowReactive、markRaw、减少深层响应式 |
| 4 | 打包体积大 | manualChunks、按需加载、Tree-shaking |
| 5 | 内存泄漏 | 清理定时器、清理事件监听、onUnmounted |
| 6 | 频繁重渲染 | computed 缓存、v-memo、避免不必要的响应式 |
性能优化口诀:
先测量,再优化;先找瓶颈,再动手;先做高收益,再做低收益。
13. 迁移:从 Vue2 到 Vue3
13.1 迁移路线图
#mermaid-svg-AwV1T9CrwcqLTyrp{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AwV1T9CrwcqLTyrp .error-icon{fill:#552222;}#mermaid-svg-AwV1T9CrwcqLTyrp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AwV1T9CrwcqLTyrp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AwV1T9CrwcqLTyrp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AwV1T9CrwcqLTyrp .marker.cross{stroke:#333333;}#mermaid-svg-AwV1T9CrwcqLTyrp svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AwV1T9CrwcqLTyrp p{margin:0;}#mermaid-svg-AwV1T9CrwcqLTyrp .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-AwV1T9CrwcqLTyrp .cluster-label text{fill:#333;}#mermaid-svg-AwV1T9CrwcqLTyrp .cluster-label span{color:#333;}#mermaid-svg-AwV1T9CrwcqLTyrp .cluster-label span p{background-color:transparent;}#mermaid-svg-AwV1T9CrwcqLTyrp .label text,#mermaid-svg-AwV1T9CrwcqLTyrp span{fill:#333;color:#333;}#mermaid-svg-AwV1T9CrwcqLTyrp .node rect,#mermaid-svg-AwV1T9CrwcqLTyrp .node circle,#mermaid-svg-AwV1T9CrwcqLTyrp .node ellipse,#mermaid-svg-AwV1T9CrwcqLTyrp .node polygon,#mermaid-svg-AwV1T9CrwcqLTyrp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AwV1T9CrwcqLTyrp .rough-node .label text,#mermaid-svg-AwV1T9CrwcqLTyrp .node .label text,#mermaid-svg-AwV1T9CrwcqLTyrp .image-shape .label,#mermaid-svg-AwV1T9CrwcqLTyrp .icon-shape .label{text-anchor:middle;}#mermaid-svg-AwV1T9CrwcqLTyrp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AwV1T9CrwcqLTyrp .rough-node .label,#mermaid-svg-AwV1T9CrwcqLTyrp .node .label,#mermaid-svg-AwV1T9CrwcqLTyrp .image-shape .label,#mermaid-svg-AwV1T9CrwcqLTyrp .icon-shape .label{text-align:center;}#mermaid-svg-AwV1T9CrwcqLTyrp .node.clickable{cursor:pointer;}#mermaid-svg-AwV1T9CrwcqLTyrp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AwV1T9CrwcqLTyrp .arrowheadPath{fill:#333333;}#mermaid-svg-AwV1T9CrwcqLTyrp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AwV1T9CrwcqLTyrp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AwV1T9CrwcqLTyrp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AwV1T9CrwcqLTyrp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AwV1T9CrwcqLTyrp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AwV1T9CrwcqLTyrp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AwV1T9CrwcqLTyrp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AwV1T9CrwcqLTyrp .cluster text{fill:#333;}#mermaid-svg-AwV1T9CrwcqLTyrp .cluster span{color:#333;}#mermaid-svg-AwV1T9CrwcqLTyrp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AwV1T9CrwcqLTyrp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AwV1T9CrwcqLTyrp rect.text{fill:none;stroke-width:0;}#mermaid-svg-AwV1T9CrwcqLTyrp .icon-shape,#mermaid-svg-AwV1T9CrwcqLTyrp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AwV1T9CrwcqLTyrp .icon-shape p,#mermaid-svg-AwV1T9CrwcqLTyrp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AwV1T9CrwcqLTyrp .icon-shape .label rect,#mermaid-svg-AwV1T9CrwcqLTyrp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AwV1T9CrwcqLTyrp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AwV1T9CrwcqLTyrp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AwV1T9CrwcqLTyrp :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
1 依赖升级Vue2.7 + Vite
2 响应式迁移defineProperty → Proxy
3 API 迁移Options → Composition
4 生态迁移Vuex → Pinia + Router 4
5 新特性采用Teleport/Suspense
6 性能优化编译时 + 响应式
13.2 常见迁移坑
| 1 | this.$set 还在用 | Vue3 不需要了,直接赋值 |
| 2 | Options API 全保留 | 逐步迁移到 Composition API |
| 3 | Vuex 还在用 | 迁移到 Pinia |
| 4 | Vue Router 3 还在用 | 迁移到 Vue Router 4 |
| 5 | webpack 还在用 | 迁移到 Vite |
| 6 | 生命周期改名 | beforeDestroy → onBeforeUnmount |
13.3 迁移检查清单
第一步:环境准备
- 升级 Node.js 到 18+
- 升级 npm/pnpm/yarn
- 安装 Vue 3 + Vite
第二步:依赖升级
- Vue 2.x → Vue 3.x
- Vue Router 3 → 4
- Vuex → Pinia
- 组件库升级到 Vue3 版本(Element Plus / Ant Design Vue 等)
第三步:代码迁移
- 生命周期改名(beforeDestroy → onBeforeUnmount)
- 移除 Vue.set / Vue.delete
- 逐步迁移到 Composition API
- 修复 v-model 语法变化
- 修复事件总线($on/$off 已移除)
第四步:测试验证
- 单元测试迁移
- E2E 测试迁移
- 功能回归测试
- 性能对比测试
13.4 迁移实战案例
案例:中型企业管理系统迁移
| 准备 | 升级 Node.js、安装 Vue3 + Vite | 1 天 |
| 依赖 | 升级组件库(Element Plus)、Pinia | 2 天 |
| 代码 | 迁移 50 个页面、100 个组件 | 2 周 |
| 测试 | 功能回归、性能对比 | 1 周 |
| 上线 | 灰度发布、监控 | 3 天 |
| 总计 | 约 4 周 |
迁移中的坑:
13.5 迁移后的优化与改进
迁移到 Vue3 后,可以做这些优化:
| 性能优化 | 利用 Proxy 响应式和编译时优化提升性能 |
| 代码组织 | 用 Composition API 重构大型组件 |
| TypeScript | 逐步引入 TypeScript,提升类型安全 |
| 开发体验 | 用 Vite 提升开发效率 |
| 生态升级 | 用 Pinia 替代 Vuex,用 Nuxt 3 做 SSR |
迁移后的收益:
- 性能提升 1~2 倍:Proxy 响应式 + 编译时优化;
- 开发效率提升:Vite 极快启动 + 热更新;
- 代码质量提升:Composition API + TypeScript;
- 维护性提升:Vue2 已 EOL,Vue3 持续迭代。
14. 总结与未来方向
14.1 Vue3 的核心价值
| 性能 | Proxy 响应式 + 编译时优化 |
| 代码组织 | Composition API 解决大型应用 |
| TypeScript | 原生支持 |
| 生态 | Vite + Pinia + Nuxt3 |
14.2 未来方向
| Vue 3.5+ | 持续改进(Reactive Props Destructure 等) |
| Vue Vapor Mode | 无虚拟 DOM 的编译时优化 |
| Nuxt 3 | SSR/SSG 生态成熟 |
| TypeScript | 持续增强类型推导 |
14.3 最后的话
Vue3 不是"Vue2 的升级版",而是从头重写的新一代前端框架——Proxy 响应式、Composition API、Vite、Pinia、Nuxt3,每一层都是一次换代。Vue2 已 EOL,Vue3 是事实上的标准——新项目直接选 Vue3,老项目按节奏迁移。
升级 Vue3 的最大收益不是"性能提升",而是"代码组织"——Composition API 让大型应用的代码不再散落在四个选项里,而是集中在同一功能块。这是 Vue3 最被低估、也是最有价值的改进。
14.4 学习路线与资源推荐
学习路线:
| 入门 | Vue3 基础语法 + Composition API | 1~2 周 |
| 进阶 | Pinia + Vue Router + Vite | 1~2 周 |
| 实战 | 完整项目开发 | 2~4 周 |
| 深入 | 响应式原理 + 源码阅读 | 持续 |
推荐资源:
| Vue 官方文档 | https://cn.vuejs.org — 最权威的文档 |
| Vue 官方博客 | https://blog.vuejs.org — 最新版本动态 |
| Vue Mastery | 付费视频课程,质量高 |
| Vue School | 付费视频课程,实战导向 |
| GitHub awesome-vue | 生态资源大全 |
生态推荐:
| UI 组件库 | Element Plus / Ant Design Vue / Naive UI |
| 状态管理 | Pinia(官方推荐) |
| 路由 | Vue Router 4(官方推荐) |
| 构建工具 | Vite(官方推荐) |
| 元框架 | Nuxt 3(SSR/SSG) |
| 工具库 | VueUse(实用 composable 集合) |
14.5 总结与展望
Vue3 的核心价值:
未来展望:
| Vue Vapor Mode | 无虚拟 DOM 的编译时优化,性能再提升 |
| Vue 3.6+ | 持续改进,更多新特性 |
| Nuxt 4 | Nuxt 4 已发布,基于 Vue 3 |
| AI 集成 | Vue DevTools 集成 AI 辅助开发 |
最后的建议:
- 新项目:直接用 Vue3 + <script setup> + Pinia + Vite;
- 老项目:按节奏迁移,不要一次性大爆炸;
- 学习路径:先掌握基础语法,再深入原理和源码;
- 生态选择:优先用官方推荐的生态(Vite/Pinia/Nuxt)。
Vue3 不是"Vue2 的升级版",而是"新一代前端框架"——它从响应式原理、代码组织、类型支持、构建工具每一层都做了换代。2026 年的今天,Vue2 已 EOL,Vue3 是事实上的标准——这是你的不二之选。
14.6 常见问题 FAQ
| 1 | Vue3 学习曲线陡吗? | 平缓,模板语法和 Vue2 类似,Composition API 是新东西但不难学 |
| 2 | Vue2 的项目要马上迁移吗? | 不强制,但 Vue2 已 EOL,建议按节奏迁移 |
| 3 | Vue3 性能比 Vue2 好多少? | 渲染性能提升 1~2 倍,内存占用更低 |
| 4 | Vue3 支持 TypeScript 吗? | 原生支持,从设计之初就考虑了类型推导 |
| 5 | Vue3 能用 Element UI 吗? | 用 Element Plus(Vue3 版本),不是 Element UI |
| 6 | Vue3 的构建工具是什么? | 推荐 Vite,开发启动极快 |
| 7 | Pinia 和 Vuex 哪个好? | Pinia 是官方推荐,Vuex 已停止维护 |
| 8 | Vue3 支持 SSR 吗? | 支持,用 Nuxt 3 做 SSR/SSG |
学习建议:
- 新手:从官方文档入门,掌握基础语法;
- Vue2 老司机:重点学 Composition API 和 Pinia;
- 进阶:深入响应式原理和源码;
- 实战:做一个完整项目,把生态工具都用一遍。
修订记录
| v1.0 | 2026-09-21 | 初版发布,覆盖响应式原理、Composition API、Pinia、Vite、Nuxt3、迁移 |
本文档基于 2026 年 9 月 Vue 官方资料整理;Vue 3.5 “Tengen Toppa Gurren Lagann” 为当前最新小版本;具体 API 请以官方文档为准。
网硕互联帮助中心




评论前必须登录!
注册