1. 别再纠结要不要学Vue3,先搞清楚它到底改了什么
如果你还在用Vue2写业务,或者刚入门前端就听说Vue3是大趋势,那这篇内容可以帮你少走很多弯路。Vue3的安装方式、组件写法、响应式原理和工程化配置,跟Vue2完全不是一个套路。我最早接触Vue3的时候也被绕晕过:明明会Vue2,一打开Vue3的官方文档却发现各种API看不明白。后来把Composition API、setup语法糖这些核心概念逐个啃了一遍,再回头看,发现Vue3的设计逻辑其实非常清晰——它就是把“组件逻辑”从选项配置里解放出来,让代码组织更像写普通函数。
这里适合的读者是:会一点HTML/CSS/JavaScript,了解Vue2基础但想系统学Vue3的人,以及想从Vue2迁移到Vue3但还没动手的开发者。这篇内容覆盖环境搭建、核心API、组件开发、高频项目场景和面试准备,基本能帮你把Vue3的主干摸透。
Vue3到底解决了什么?Vue2时代,一个组件的data、methods、computed、watch是分散在不同选项里的,一个复杂业务功能会被拆成好几块。Vue3把这一切收拢到setup里,你想看一个功能的完整逻辑,上下滚动几行代码就能看完。这个变化不只是语法层面的,它直接改变了前端开发者组织业务的思维方式。
2. 环境搭建:从零跑起一个Vue3项目
2.1 创建项目的三种方式,我推荐哪个
Vue3的安装方式有很多种,最常见的三种是:
- 使用npm create vue@latest创建官方脚手架
- 使用npm create vite@latest创建Vite项目后手动安装Vue3依赖
- 在已有项目中直接npm install vue@next
我一般推荐第一种。Vue官方脚手架目前默认生成的就是Vite + Vue3 + TypeScript的工程,内置了ESLint、Prettier、Router、Pinia等选项。如果你只是想在已有老项目里用Vue3写一个页面,第三种更轻量。
创建命令很简单:
npm create vue@latest运行时它会问你项目名称、是否使用TypeScript、是否启用Router、是否启用Pinia这些选项。对于初学者,我建议第一遍全部选No,先跑通最小项目,再看官方文档逐步加功能。上来就选全套,很容易被一堆配置文件淹没,反而不利于理解Vue3本身。
2.2 Vite配置和环境变量,局域网访问空白问题
Vite是Vue3的默认开发服务器,启动速度比Webpack快得多。这里需要重点提醒一个坑:开发时在局域网里访问项目,经常遇到页面空白。很多人以为是代码问题,其实是Vite默认只监听localhost,没有监听局域网IP。
解决方法是修改项目根目录下的vite.config.ts,在server字段里加上host配置:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { host: '0.0.0.0', port: 5173, }, })host: '0.0.0.0'表示监听所有网络接口,这样手机和同一局域网的其他电脑都能访问。不过要注意,如果项目里用了一些浏览器特性,旧版Edge或者特殊浏览器环境下的兼容性可能会出问题。有网友反馈说Vue3项目在Edge浏览器里有时候无法关闭窗口右上角的最小化按钮,这其实和项目本身无关,更多是浏览器扩展或者系统级别的显示问题,建议先排查浏览器设置和硬件加速开关。
2.3 SCSS安装与全局样式管理
Vue3项目中用SCSS是刚需,安装依赖只用两个包:
npm install -D sassVite原生支持.scss文件,不需要额外配置loader。装完sass之后直接写lang="scss"就能用:
<style lang="scss" scoped> $primary-color: #409eff; .card { background: $primary-color; &:hover { opacity: 0.9; } } </style>如果你想让全局变量、mixin在所有组件里直接可用,需要在vite.config.ts里配置css.preprocessorOptions.additionalData:
css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/variables.scss" as *;`, }, }, },注意这里用的是@use而不是@import,Sass新语法已经不推荐@import了。此外,路径别名@需要你提前配置好,否则编译会报找不到模块。
3. 核心API上手:响应式数据的本质
3.1 ref和reactive,别再用混了
Vue3里最基础的两个响应式API是ref和reactive。很多人刚开始搞不清区别,简单粗暴的理解是:ref可以处理基本类型和引用类型,reactive只处理对象。
实际操作中,我建议所有场景都用ref,尤其是数组和对象。原因有二:一是ref在模板里会自动解包,不用写.value;二是在函数传参和结构赋值时,响应式不会断。reactive最大的坑是如果把对象里的属性解构出来,会丢失响应性。
import { ref, reactive } from 'vue' // ref写法 const count = ref(0) const user = ref({ name: '张三', age: 18 }) count.value++ // 修改时要加.value console.log(user.value.name) // reactive写法 const userReactive = reactive({ name: '张三', age: 18 }) userReactive.age = 20 // 这里不需要.vaalue但是在方法里,如果你写成这样,麻烦就来了:
const { age } = userReactive age修改不会触发页面更新这就是解构丢失响应性的典型问题。我踩过这个坑之后,规定团队里新代码一律使用ref,排查问题的成本低很多。
3.2 computed和watch的使用边界
computed用于派生状态,比如购物车里根据商品数量和单价算总价。Vue3的computed用法和Vue2类似,但注意它返回的是一个ref对象:
import { ref, computed } from 'vue' const price = ref(100) const quantity = ref(2) const total = computed(() => price.value * quantity.value) console.log(total.value) // 200 price.value = 200 console.log(total.value) // 400computed默认是只读的,如果你尝试给computed赋值,控制台会警告。如果需要修改computed派生出来的值,得用可写computed,传入get和set两个函数,但实际业务中很少用到,一般建议拆成原始状态分别维护。
watch用于监听响应式数据变化执行副作用,比如搜索框输入后订阅变化、表单值变动触发校验。Vue3的watchAPI第一个参数可以传ref、reactive对象、getter函数或数组:
import { ref, watch } from 'vue' const keyword = ref('') watch(keyword, (newVal, oldVal) => { console.log(`关键字从${oldVal}变成了${newVal}`) })搜索场景还有一个高频需求:搜索条件保留问题。路由跳转再返回时,页面刷新导致搜索条件丢失。常见的解决办法是把搜索条件同步到URL query参数里,用vue-router的router.replace更新地址,页面重新加载时再从route.query初始化。这样刷新前和刷新后的状态都能保留,浏览器前进后退也一致。
4. 组件开发的正确姿势
4.1 选项式还是组合式,怎么选
Vue3依然支持选项式的data、methods写法,很多从Vue2迁移的项目也还这么写。但如果你想真正跟上Vue3的节奏,我建议新项目直接用组合式API配合setup语法糖。
<template> <div>{{ user.name }} - {{ user.age }}</div> </template> <script setup lang="ts"> interface User { name: string age: number } const user = ref<User>({ name: '张三', age: 18 }) function updateName(name: string) { user.value.name = name } </script>这样一来,父组件传错类型、子组件的事件名称拼写错误都能在编译阶段暴露出来,比运行时报错好排查得多。 ### 4.2 动态组件、插槽、多行表单的实践方案 动态添加和删除form表单的一行数据,在后台管理系统里特别常见。做法其实很简单:把表单项绑定到一个数组上,每一行数据对应数组里的一个元素。 ```vue <script setup lang="ts"> interface FormRow { name: string quantity: number } const rows = ref<FormRow[]>([{ name: '', quantity: 1 }]) function addRow() { rows.value.push({ name: '', quantity: 1 }) } function removeRow(index: number) { if (rows.value.length <= 1) return // 至少保留一行 rows.value.splice(index, 1) } function clearValidation() { // 表单校验清空逻辑 } </script>模板里用v-for渲染每一行,每一行的el-form-item都要绑定唯一的prop路径,比如form.rows.${index}.name,这样Element Plus等UI库的rules校验才能精确到单行。网上有人问Vue3里动态表单的日期校验rules怎么写,我用过比较可靠的方式是:在rules里给对应字段配置type: 'date'或validator,日期范围校验用自定义validator函数处理,而不是直接依赖预设type。
4.3 useSlots和打印模板组件
开发通用组件时经常要处理插槽。Vue3里访问插槽内容用useSlots:
import { useSlots } from 'vue' const slots = useSlots() if (slots.header) { console.log('存在header插槽') }打印模板是另一类高频需求。Vue3项目里做打印,我的做法是封装一个打印组件,把需要打印的内容包进去,用CSS媒体查询控制打印样式,再通过window.print()触发打印。核心参数是@page的大小设置和打印区域的样式隔离。如果直接用浏览器打印整个页面,通常会带很多页面的边栏、弹窗等无用内容。封装打印区域组件后,可以用display:none隐藏页面其他部分,保留打印区域,这样打印出来干净整齐。组件里还支持动态传入ref,让打印时只打印该区域内容。
5. 项目实践:后台管理系统、商城、微前端等场景
5.1 登录跳转和权限控制,onSuccess接不到回调
很多Vue3新手在写登录时会碰到一个问题:第三方登录组件或验证码组件明明成功回调了,页面却不跳转。排查思路通常是,先确认回调是否真的触发了组件定义的事件。Vue3组件事件名和模板的@事件名是严格对应的,如果事件名不一致,比如子组件emit了success,父组件监听的是on-success,就会静默失败。
Element Plus的上传组件就有一个经典问题:on-success监听不到。原因很简单,新版Element Plus上传组件的回调是对象属性方式,而不是事件监听方式。你得这样写:
<el-upload :on-success="handleSuccess">也就是用冒号绑定属性,而不是@on-success。这是Element Plus 2.x和Vue2版本的ElementUI一个明显的API差异。
登录跳转本身建议用路由守卫统一控制,而不是每个页面单独判断:
router.beforeEach((to, from) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { return '/login' } })这样写的好处是,新增页面时不需要记得给每个路由加登录判断,只要没有token就一律拦截到登录页。
5.2 商城购物车数量、Tabs标签页样式
商城项目里购物车数量不小于0是一个常见的边界校验需求。修改数量时不能只做加加减减,还要处理数量为1时不能再减少、数量为最大值时不能继续增加的情况:
const quantity = ref(1) function decrease() { if (quantity.value <= 1) { ElMessage.warning('商品数量不能小于1') return } quantity.value-- } function increase() { if (quantity.value >= 99) { ElMessage.warning('单个商品最多购买99件') return } quantity.value++ }核心是两个守卫条件:下限保护和上限保护。不写这两个判断,用户点击减号会出现0甚至负数,点击加号可能超过库存上限。除了UI层校验,接口层也需要再校验一次,防止绕过前端直接调接口提交非法数据。
Tabs标签页样式修改是后台管理系统常遇到的问题。大多数UI库的Tabs样式默认比较简陋,要自定义成带色块、带图标的标签页,需要覆盖类名或者设置一个专用的样式类。我的做法是给Tabs组件加一个自定义class,然后在scoped样式里用:deep()穿透修改内部元素:
<el-tabs class="custom-tabs"> ... </el-tabs> <style scoped lang="scss"> .custom-tabs { :deep(.el-tabs__item) { height: 40px; line-height: 40px; } :deep(.el-tabs__active-bar) { background-color: $primary-color; } } </style>:deep()是Vue3中深度选择器的写法,对应Vue2的::v-deep。如果你要修改轮播图、评分组件等自定义UI库组件的内部DOM,都会用到这个。
5.3 OnlyOffice在线编辑、PDF预览、ECharts图表
企业项目里在线文档编辑经常用OnlyOffice。Vue3集成OnlyOffice的思路不复杂,核心是挂载一个script然后创建编辑器实例。我这里给出一个最小可用的封装思路:
<script setup lang="ts"> const containerRef = ref<HTMLElement>() function openEditor(docUrl: string, token: string) { const docEditor = new window.DocsAPI.DocEditor(containerRef.value, { document: { fileType: 'docx', key: '文档唯一标识', url: docUrl, permissions: { edit: true }, }, documentType: 'word', editorConfig: { lang: 'zh-CN', token: token, }, }) } </script> <template> <div ref="containerRef"></div> </template>这里有两点需要注意:一是配置项变化时一定要重新创建编辑器,否则OnlyOffice不会识别新文档;二是令牌Token必须从后端获取并注入到editorConfig里,否则编辑保存会被拒绝。PDF预览在Vue3里最简单的方式是用iframe直接打开PDF文件地址,但复杂场景比如带书签、高亮标记时,需要考虑pdf.js等渲染方案。
ECharts在Vue3项目里的用法和Vue2差不多,注意在组件卸载时调用dispose释放实例。如果是uniapp + Vue3 + ECharts的组合,不能直接用echarts包,要使用renderjs或者引入echarts-for-miniprogram类的适配方案。最近有人问我如何用ECharts把图片导出,常见做法是调用chart.getDataURL()把图表渲染成图片下载,注意canvas画布跨域图片时需要开启服务器CORS,否则导出图片是空白的。
5.4 微前端、Three.js以及Vue2老项目迁移
Vue3 + Vite + 微前端这个组合目前是主流方案。常见技术选型有qiankun、无界wujie和模块联邦。qiankun在Vite子应用下需要配置vite-plugin-qiankun插件,子应用需要导出生命周期。如果主应用是Vue3 + Vite,子应用是Vue2 + Webpack,跨技术栈接入通常没有问题,微前端的本质是子应用独立打包成umd格式或者通过import-html-entry加载。
Vue3 + Three.js + TypeScript做3D机房可视化项目,在2018年之前可能需要自己封装大量逻辑,现在Three.js配合vue3虽然仍有上手门槛,但资料已经很丰富了。核心就是让Three.js的渲染循环和Vue组件的生命周期绑定,初始化时创建场景和相机,组件销毁时清理renderer。建议有兴趣的同学先跑通三个最小案例:立方体旋转、轨道控制器、鼠标点击选中物体,这三个案例覆盖了3D项目80%以上基础操作。
很多成熟项目是用Vue2写的,能不能平滑转Vue3?答案是能,但别想着一次到位。Vue团队提供了@vue/compat运行时的兼容模式,这个模式会尽量兼容Vue2的选项式API和部分行为。我的迁移路线是:第一步升级构建工具从Webpack到Vite,第二步把项目依赖逐个升级到兼容Vue3的版本,第三步启用compat模式跑通全量测试,第四步在业务迭代过程中逐步把选项式改成组合式。这个方案可以让项目在迁移期间保持可交付状态,而不是停滞一个月做完大改造再上线。
5.5 若依框架的TS报错问题和首屏加载优化
若依(RuoYi)框架是目前国内很多后台项目的基础模板。它从Vue2版本升级到Vue3版本后,很多项目用上了TypeScript。常见的TS报错大多是类型不匹配,比如对象可能是undefined时就取属性、props没有完整定义类型。这类报错推荐一个铁律:先定义后使用,宁可写代码时麻烦一点,也不要让TS出打补丁式的any。项目里的tsconfig把strict设为true时,报警告不要急着关闭,逐条修复,后面维护成本会低很多。
首屏加载优化也是后台管理系统的高频话题。Vue3项目首屏太慢,通常有几个原因:UI库全量引入、路由懒加载没开、组件重复打包、没有做gzip压缩。VXETable是一个功能很全的表格组件,体积较大,用的时候需要注意按需加载,避免首屏把整个表格库都拉下来。我在实践中会把路由改成评分懒加载,把第三方库从单独打包成vendor chunk,再开启gzip压缩。一个优化前后的对比数据是,首屏资源从2.8MB降到900KB,加载时间从4秒降到了1.5秒左右。
6. Vue3源码解析和经典面试题
6.1 响应式系统到底怎么实现的
Vue3的响应式核心是Proxy,这让它可以拦截对象属性的增删、修改和遍历操作。Vue2用的是Object.defineProperty,只能拦截已有属性的读取和赋值,新增和删除属性都监听不到,所以需要额外调用$set。Vue3没有这个尴尬,这是两者响应式能力最本质的差异。
源码层面看,Vue3的reactive函数通过new Proxy(target, handlers)创建代理,当读取属性时触发track收集依赖,修改属性时触发trigger执行更新。ref的本质是给基本类型值套了一层对象结构,内部其实也用了reactive的函数。computed则基于一个特殊的effect函数实现惰性缓存,只有依赖变化时才会重新计算。
理解这套机制对日常开发很有用,至少能解释几个怪现象:为什么直接给reactive数组push进去一个新对象,页面能更新(因为Proxy拦截了)方法);为什么reactive对象解构后修改属性不更新(因为解构出来的是原始值,不再走代理);为什么ref在模板里不用写.value(因为模板编译器底层做了自动解包)。
6.2 Vue3最经典的六个面试问题
网上流传的Vue3面试最经典6个问题,我按实际面试常问顺序整理如下:
Vue3和Vue2的响应式原理有什么区别?答案是Proxy和Object.defineProperty的对比,以及Proxy能拦截哪些操作。
Composition API相比Options API的优势?核心是逻辑复用和组织方式,同一个功能的代码可以集中放在一起。
watch和watchEffect的区别?watch需要显式指定数据源,watchEffect会根据函数内部用到的响应式依赖自动追踪。
ref和reactive的区别?可以从使用场景、响应式原理、解构是否丢失响应性三个维度回答。
说说Vue3的生命周期钩子变化?比如beforeDestroy改成了beforeUnmount,选项式API里增加了setup函数等。
defineExpose和defineOptions是干什么的?defineExpose暴露组件实例属性给父组件,defineOptions用于定义组件名字、inheritAttrs等选项。
这六个问题问得最多,我面试别人也会用这几个问题快速摸清候选人对Vue3的掌握程度。
6.3 新手上手源码的思路
很多初学者想直接啃Vue3源码,打开packages目录就被十几个子目录吓退了。我的建议是不要从头读到尾,而是带着问题找答案。先看一下reactivity目录下的reactive.ts和ref.ts两个文件,对照API文档理解每一个导出函数的实现。然后去找effect.ts里track和trigger两个函数,知道这两部分是响应式驱动的发动机。最后回到runtime-core目录看组件实例从创建到挂载的过程。每看完一小节,就在本地写个Demo验证一下行为,遇到不符合预期的地方再回头看源码,效果比通读好得多。
7. 实战收尾,说点我的个人心得
这篇文章写到这里,基本把Vue3入门路上会碰到的核心技术点和高频问题都盘了一遍。最后分享几个我自己的习惯。第一,学Vue3最重要的不是背API,而是动手敲一个最小商城项目,把登录、列表、详情、购物车、订单这一条链路做完,比看十遍文档都管用。第二,遇到报错先看控制台堆栈,很多问题其实是你把Vue2的写法直接搬到了Vue3,最常见的错误就是把@on-success这种事件监听方式和Element Plus的属性回调混在一起。第三,如果是公司里的老项目要升级,别急着全量重写,先把依赖兼容性跑通,再说用组合式API逐步重构。
如果你正在学习Vue3,我的建议是不要贪多,先把ref、reactive、computed、watch、组件通信这五个核心技能练扎实,再去碰状态管理和路由。地基打得稳,后面学什么都快。