资讯动态

Vue实战笔记:从工程化踩坑到原理面试的高频解析

发布时间:2026/9/9 11:56:31 来源:尧图企业网站定制
最近在整理自己的 Vue 学习笔记时被好几个高频问题卡了一下项目启动到 98% 突然不动、路由传参刷新后丢失、m3u8 视频流怎么在页面上播放、百度地图第二次加载就白屏。这些问题不是单纯看书能解决的得一个个上手试踩过坑才记得住。这篇笔记就是围绕这些高频关键词整理出来的包括工程化、路由、组件状态、第三方集成、原理面试几个方向。如果你正好在做 Vue 相关项目或者准备面试时想把这些细节串起来可以参考我这段时间的实操记录。1. 工程化与开发环境先把“跑不起来”和“卡住不动”解决掉1.1 Vue 安装与环境配置的版本选择很多新手装 Vue 环境时第一步就栽了不是 node 版本不对就是 npm 镜像源太慢。我的建议是先确认本机环境再动手Node.js 推荐 16.20.x 或 18.x 长期支持版Vue 3 Vite 对这些版本兼容性最好Node 20 以上的版本我用下来也行但有些老项目的 webpack 4 可能报 OpenSSL 错误需要加--openssl-legacy-provider参数npm 镜像尽量切成国内源npm config set registry https://registry.npmmirror.com不然装个依赖能等半天包管理器方面我个人的排序是pnpm yarn npm。pnpm 对磁盘占用和依赖隔离做得更好Vue 官方脚手架现在也默认推荐。项目创建时的命令大家都不陌生npm create vuelatest my-vue-app这里会问你用不用 TypeScript、Router、Pinia、Vitest 等选项不用一下子全选按项目实际需要来。如果只是后台管理系统Router 和 Pinia 先勾上TS 就看你团队习惯从零学的话可以先不加。1.2 启动到 98% 卡住的排查思路热词里有一句很典型98% after emitting CopyPlugin启动时卡住不动。这个我在一个老项目中真实遇到过现象是命令行停在 98%浏览器一直不刷新过十几分钟还是那样。先说原因CopyPlugin 是 webpack 在 emit 阶段把静态资源复制到 output 目录的插件走到这一步基本是“资源复制”和“文件读写”的瓶颈常见诱因有静态资源目录里放了超大文件几百 MB 的视频、安装包复制时卡住public目录文件过多每个文件都要 hash 和复制开发环境跑了一段时间node_modules/.cache缓存损坏系统内存不足webpack 进程反复 GC看起来就像卡死某些磁盘上存在权限问题文件无法正常写入。我的排查步骤供参考先看命令行最后几行报错如果只是停住没报错按CtrlC终止检查public目录下有没有大文件有就先移到项目外删除node_modules和缓存目录重装依赖在vue.config.js里把devtool调成false或eval-cheap-module-source-map有时代码压缩和 sourcemap 生成会加剧这个问题还不行就把devServer的热更新关掉试试先用writeToDisk: true看构建产物能否生成。另外一个隐藏坑是电脑开了多个大型软件内存不够Node 进程虽然没崩但 GC 频繁导致构建极慢关掉几个程序再启动就正常了。所以遇到 98% 卡住先看资源占用再说。1.3 HBuilderX 跑 Vue 到手机或模拟器“vue项目可以在hbuilder x上运行到手机或者模拟器”这个需求通常是做混合 App 开发的同学问的。HBuilderX 是 uni-app 的官方 IDE但它也能跑普通 Vue 项目只不过不是通过内置浏览器运行而是走了另一种逻辑。如果你手里就是一个纯 Vue 项目不是 uni-app想在手机上预览我的推荐做法是启动 Vite 或 webpack devServer 时把 host 设为0.0.0.0端口固定如8080手机和电脑连同一个局域网手机浏览器直接访问http://电脑IP:8080如果是要调试原生能力相机、定位、扫码那必须用 HBuilderX 的 5 App 或 uni-app 云打包把 Vue 代码编译进 WebView 里这就不只是“运行”的概念了而是“打包”。模拟器的话HBuilderX 自带 Android 模拟器但需要安装 Android SDK 环境启动速度一般我实际用下来还是 Chrome 的移动端模拟模式最顺手。2. 路由参数传递、刷新丢失和框架对比2.1 路由参数用 params 还是 queryVue Router 的参数传递几乎是面试必问也是日常开发高频使用点。热词里搜“vue路由参数”的人很多说明这块确实容易混。Vue Router 4Vue 3 对应版本中参数分为两种方式query方式参数拼接在 URL 的查询字符串中如?page1nameadmin刷新后还在因为 URL 本身就携带参数params方式配合路由配置中的动态路径段如/user/:id刷新后如果使用router.push({ name: user, params: { id: 1 } })数据在页面内传递但刷新时如果没正确处理会丢失。我自己的经验是需要收藏、分享、刷新后保持状态的参数一律用 query不需要暴露在 URL 里的内部参数用 params 或通过 store 传递。实际开发中遇到最多的问题就是组件的setup里直接读取route.params.id然后一刷新拿到的是undefined。原因通常是路由配置用了/user/:id但跳转时却用了router.push(/user/123)路径没问题另一种情况是/:id被拼成了/:id?可选参数没传时自然为空。排查方法也简单在页面里打印route.params和route.query看路由对象里当前到底有没有值。传一个对象作为参数时还有个坑query会被序列化到 URL对象里的函数、Date 会被转成字符串再次取回就不是原来类型了。最佳实践是只传纯 JSON 数据复杂对象最好用JSON.stringify压缩一次再传。2.2 路由懒加载和守卫的写法路由懒加载是性能优化的常规操作。Vue Router 4 里推荐这样写const routes [ { path: /order, name: Order, component: () import(/views/order/index.vue), meta: { title: 订单列表, requiresAuth: true } } ]这样做的好处是打包时会把对应页面单独拆包首屏只加载首页需要的 JS登录页面、订单模块等在访问时才异步加载。我在中型后台项目里实测过首屏 js 体积能减少 30% 到 50%速度提升明显。路由守卫方面常用的场景是登录鉴权没有 token 时跳转登录页有 token 但访问的是登录页则跳回首页。写法如下router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })注意to.fullPath会携带当前页面的完整路径和参数登录后可以精准跳回原页面。这个细节很实用不然登录完总是回到首页用户得再点一次。2.3 React Router 与 Vue Router 的差异及选型逻辑做前端的人很难不碰到框架对比的问题。Angular、React、Vue 的对比是热词React 和 Vue 路由的差异也是高频面试题。我个人的理解是这样的Vue Router 强调声明式配置路由表集中在一个routes数组里配合router-view非常直观模板里直接使用router-link跳转React Router 更偏向组件化思维用Routes和Route组件声明路由扩展性和嵌套更自由但心智模型需要从“配置驱动”切换到“组件驱动”数据层面Vue Router 有route.meta天然适合做权限标记React Router 6 常用useRoutes配合统一包装组件实现类似功能运行时两者都支持动态路由、懒加载、守卫/拦截器只是写法不同。选型逻辑上没什么“谁一定更好”团队熟什么用什么。如果项目里已经是 Vue 技术栈路由就顺理成章用 Vue RouterReact 项目则按 React Router 的生态走。真正的瓶颈通常不是路由框架本身而是路由分层是否清晰——项目一旦大了路由表没有按模块拆分维护起来就是灾难。我的建议是路由文件按业务模块拆成多个文件再用modules文件夹统一汇总。3. 组件与状态computed、watch 和 Element Plus 常见问题3.1 computed 的正确理解与使用场景热词里单独搜“vue computed”的人也很多说明这个 API 看着简单真要用好还是需要理解背后的机制。computed是一个“派生状态”核心特征是两个缓存和依赖追踪。它根据已有的响应式数据计算出一个新值并且只有它依赖的数据发生变化时才会重新计算其他情况下直接返回缓存结果。举个例子const firstName ref(张) const lastName ref(三) const fullName computed(() ${firstName.value} ${lastName.value})fullName只有在firstName或lastName改变时才重新计算。如果你用methods里的函数实现同样功能每次渲染都会重新执行一次这就是性能差距的来源。computed的另一个特点是支持get和set双向定制const fullName computed({ get: () ${firstName.value} ${lastName.value}, set: (val) { const [first, last] val.split( ) firstName.value first lastName.value last } })这种情况下你给fullName赋一个新值它内部会拆解后更新firstName和lastName。这种写法在表单联动场景比如某些组件需要双向绑定一个“组合字段”很有用。3.2 watch 数组第一项新旧值相同的问题热词里有这样一句话vue watch 数组的第一项为啥新值和旧值是一样的。这个问题我在真实开发中也遇到过非常典型。先看一段容易出问题的代码const list ref([{ name: a }, { name: b }]) watch(list, (newVal, oldVal) { console.log(新值, newVal) console.log(旧值, oldVal) })当你修改了数组第一项的某个属性时你会发现newVal和oldVal打印出来的内容是一样的。原因在于默认情况下 watch 监听的是引用当数组内部元素变化时数组本身的对象引用并没有改变。只有list.value newList这样整体重新赋值时新旧值才会不同。那如何拿到真正的旧值呢有几种方案监听数组的深度变化并且用深拷贝保存旧值watch( () [...list.value], // 每次重新创建数组引用 (newVal, oldVal) { console.log(新值, newVal) console.log(旧值, oldVal) }, { deep: true } )自己维护旧值快照let oldSnapshot JSON.parse(JSON.stringify(list.value)) watch(list, (newVal) { const old oldSnapshot oldSnapshot JSON.parse(JSON.stringify(newVal)) console.log(新值, newVal) console.log(旧值, old) }, { deep: true })注意JSON.parse(JSON.stringify(...))对大对象有性能损耗如果数组项很大且监听频繁可以改用lodash的cloneDeep或者只在需要时手动对比字段。3.3 el-select 远程搜索加滚动加载更多热词里提到 el-select 需要远程搜索且下拉框可以滚动请求更多数据这是后台管理系统中极其常见的需求。Element Plus 的el-select默认没有“滚动到底加载更多”的内置能力需要自己扩展。我的实现思路是在el-select的visible-change事件中绑定下拉面板 DOM监听滚动事件当滚动距离底部一定阈值时触发loadMore。核心代码大概长这样el-select v-modelselectedId filterable remote :remote-methodremoteSearch visible-changeonVisibleChange el-option v-foritem in options :keyitem.id :labelitem.name :valueitem.id / /el-selectconst onVisibleChange (visible) { if (visible) { nextTick(() { const dropdown document.querySelector(.el-select-dropdown .el-scrollbar__wrap) dropdown.addEventListener(scroll, handleScroll) }) } } const handleScroll (event) { const el event.target const distance el.scrollHeight - el.scrollTop - el.clientHeight if (distance 30 !loading.value) { page.value fetchOptions() } }这里有几个关键点要先确认数据总量是否还有更多不然到底部会一直请求远程搜索时要把页码重置为 1关键词为空时请求第一页滚动事件要在下拉关闭时移除否则会内存泄漏如果项目里有多个el-select建议把这段逻辑抽成自定义指令或封装组件不要在每个页面重复写。3.4 element 组件报错、el-upload 文件数和 table 全选的细节热词里还提到几个 Element Plus 相关的具体问题第一个是[vue warn]: Unknown custom element: el-carousel-item - did you register the component correctly?。这种报错通常是按需引入了组件但遗漏了CarouselItem或者ElCarouselItem。用unplugin-vue-components自动按需引入时一般不会犯这个错但如果是手动按需引入就要检查是否在app.use(ElementPlus)里把整个组件库引入了或者单独导入。我建议直接用app.use(ElementPlus)全量引入开发环境体验最好等项目上线再考虑按需优化。第二个是vue中怎么获取el-upload的文件数量。这比较简单因为el-upload的v-model:file-list绑定的数组就是当前文件队列表直接fileList.length就能拿到数量。但有一点要注意上传成功与未上传成功的文件都在这数组里如果只统计已上传成功的需要过滤status success的文件。第三个是vue 隐藏 selection。这是表格里动态控制多选列的需求Element Plus 的el-table-column用typeselection定义多选列想隐藏时直接v-iffalse即可或者用一个计算属性控制是否渲染该列。el-table-column v-ifshowSelection typeselection width55 /隐藏了 selection 列后之前选中的行还会保留在表格的selection状态里如果需求是隐藏后清空选中要用tableRef.value.clearSelection()手动清一下。4. 第三方集成m3u8 播放、地图和天气数据4.1 Vue 中播放 m3u8 视频流的实操在前端播放 m3u8 是需求也是技术活。m3u8 是 HLS 协议下的播放列表文件浏览器原生 video 标签无法直接播放需要借助 hls.js 或者 video.js 这类库。我的项目中用的是hls.js因为它 API 清晰、体积小按需加载动态语言片段的机制比较成熟。安装依赖npm install hls.js在 Vue 组件里封装一个播放器video refvideoRef controls muted autoplay classvideo-player/videoimport Hls from hls.js import { ref, onMounted } from vue const videoRef ref(null) const playM3u8 (url) { const video videoRef.value if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () { video.play().catch(() {}) }) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 HLS直接给 src 即可 video.src url video.play() } }几个实际踩过的坑m3u8 若涉及跨域服务端需要配置 CORS 头否则请求会被浏览器拦截直播流通常会有几秒到十几秒延迟如果需要低延迟直播要考虑 WebRTC 或 LL-HLS 方案切换清晰度的需求可以在hls.levels里操作组件卸载时记得hls.destroy()否则会继续请求切片浪费带宽和内存。如果项目里已经用了 video.js也可以用 video.js 的videojs-contrib-hls插件但维护状态一般我后来都直接用 hls.js更轻量可控。4.2 百度地图首次正常第二次白屏的排查热词里有一句非常形象vue 加载百度地图 首次打开正常,第二次一片空白。这个问题的本质是地图实例或脚本初始化被反复执行造成的。常见原因有三个使用了动态往head里插入百度地图 script 的方式第二次进入页面时 script 已经存在但BMap全局对象没有重新初始化导致地图容器拿不到正确的状态组件销毁时没有清除地图实例切到别的页面后地图实例仍占用底层 GL 上下文再次创建实例时 WebGL 上下文冲突白屏容器宽高为 0或者地图容器在组件onMounted时还没有完成渲染display:none 立即切到显示。我的处理方案是在组件销毁钩子里map.destroy()并且置空地图容器 DOM动态加载 script 时加一个全局标记同一页面只加载一次确保地图容器有明确宽度和高度百分比或者固定像素都要确认如果是在 Tab 页签里切换显示容器在隐藏状态时不要初始化地图等$nextTick且元素offsetWidth 0后再初始化。onMounted(() { nextTick(() { if (mapContainer.value.offsetWidth 0) return initMap() }) }) onBeforeUnmount(() { if (mapInstance.value) { mapInstance.value.destroy() } })光这一条就帮我解决了几次“页面切换后地图白屏”的问题。4.3 腾讯地图和天气预报数据的接入思路热词里也提到“vue前端怎么获取天气预报数据”。这个要看需求从哪里来如果是做天气面板通常有两条路用第三方 API如和风天气、高德天气、腾讯云天气直接请求后端不需要自己维护数据自建后端接口通过数据源抓取再格式化。前端直接对接第三方 API 时关键要处理好跨域和 API key 泄露问题。我的经验是不要在前端代码里放置长期有效的 API key如果项目没有后端至少把 key 放到.env环境变量中并限制该 key 的域名白名单。如果后端是 Spring Boot 或 Node.js最好由后端统一请求前端只用 axios 调自己的接口。腾讯地图在 Vue 里的用法和百度地图大同小异。引入腾讯地图的 JS API 后在组件里实例化地图再添加 Marker 覆盖物。如果只是显示一个位置用TMap和MultiMarker足够。需要注意的点腾讯地图新版本 API 引入方式变了需要在script的key参数里带上自己申请的 key地图容器宽高必须明确某些公司网络环境下地图 API 加载不稳定建议加一个超时重试机制。4.4 Vue 上传视频和封面图的实现要点上传视频和封面图在后台上传模块中很常见。本质是两个文件上传操作但视频上传往往涉及大文件和进度显示。我用 Element Plus 的el-upload实现时核心配置是这样el-upload action/api/upload/video :before-uploadbeforeVideoUpload :on-progresshandleVideoProgress :on-successhandleVideoSuccess :file-listvideoList el-button选择视频/el-button /el-uploadbefore-upload里做文件类型和大小校验比如只允许 mp4、webm不超过 200Mon-progress里通过event.percent更新进度条on-success里拿到返回的 url存到表单数据里。封面图上传就简单一些限制图片格式即可。如果想用视频的首帧作为封面可以用 canvas 截帧加载视频文件为ObjectUrl用 video 元素load到一定时间后drawImage到 canvascanvas.toBlob生成图片文件再上传。这个方案纯前端完成不需要后端参与生成的封面就是用户看到的画面体验很好。但有一个限制跨源视频或未加载完成的视频可能截帧失败所以截帧前要确保视频已经触发了loadeddata事件。5. Vue 原理与面试diff、生命周期和 devtools5.1 diff 算法里到底在做什么Vue 的 diff 算法是“八股文”里绕不开的知识点。我推荐这样理解当响应式数据变化后Vue 需要找出哪些虚拟 DOM 节点真正需要更新diff 就是对比新旧虚拟节点的高效算法。核心思想有三条同层比较只对比同一层级的节点不跨层级 diff层级复杂度较深时性能下降但实现简单绝大多数业务场景够用key 的作用通过 key 可以辨识节点身份。列表渲染如果不写 keyVue 只按顺序对比插入/删除时可能会复用错误节点导致状态错乱用 key 后可以用“就地更新”策略减少不必要的重建双端比较Vue 2 的 diff 是从头和尾同时开始对比Vue 3 在此基础上进一步优化引入区块树和静态标记在编译阶段就标注哪些节点是静态的减少对比范围。一个面试常问的细节为什么 key 不要用 index 作为 key。因为数组顺序一旦变化index 也跟着变化Vue 会认为原来的虚拟节点都变了导致大量重建而使用业务 id 作 keyVue 可以精准地只移动变化的那几个节点。至于热词里的vue运行周期其实就是生命周期问题。Vue 3 的组合式 API 中setup代替了beforeCreate和created其他的生命周期钩子都有一个对应的onXxx写法onMounted组件挂载后执行 DOM 操作、请求数据onUpdated数据变化导致 DOM 更新后执行onBeforeUnmount组件卸载前清理定时器、移除事件监听、销毁地图和播放器等。5.2 vue-devtools 插件安装及常见问题热词里反复出现vue devtools插件下载和谷歌插件下载可见这个工具的安装门槛并不低。Vue Devtools 的官方下载途径主要是 Chrome 扩展商店和 Edge 扩展商店。如果你的网络环境能正常访问扩展商店直接在商店搜索 “Vue.js devtools” 安装即可。装完后要确认以下几点扩展要能访问你本地调试页面需要打开“允许访问文件网址”选项开发环境下页面里的 Vue 实例会自动检测到 Devtools生产环境默认不显示Vue 3 项目需要安装较新的 Devtools 版本旧版可能识别不到 Vue 3 的应用。如果扩展商店打不开有些团队网络限制可以通过 npm 安装 devtools 中间件或者用独立应用模式但体验确实差一些。我更推荐在代码环境没问题的时候就把扩展装好日常开发排查数据、状态、路由都非常方便。cursor如何让vue文件代码高亮这个需求我最近也遇到过。Cursor 编辑器默认对.vue文件支持一般解决办法是安装 Volar 扩展Vue 官方推荐的 VS Code 插件装好之后自动识别template、script、style语法代码高亮和类型提示都有了。5.3 响应式原理与面试高频题Vue 3 的响应式原理是基于Proxy实现的。Proxy可以直接拦截对象的读取、设置、删除等操作在 get 时收集依赖在 set 时触发更新。而 Vue 2 用的是Object.defineProperty只能劫持已有的属性所以新增属性需要用Vue.set或$set数组的索引操作也需要特殊处理。这带来一个实际好处在 Vue 3 里直接给reactive对象新增属性、用索引修改数组响应式都是正常的。面试中还经常派生出这类问题v-if和v-show的区别v-if是真实的条件渲染不满足条件时 DOM 都不创建v-show只是切换display: none节点始终在 DOM 中。频繁切换的用v-show首屏加载时不需展示的用v-if组件通信方式父子用props和emit跨层级用provide/inject或 Pinia什么情况下需要nextTick修改响应式数据后DOM 更新是异步的如果马上去获取 DOM 尺寸或做某些依赖更新后 DOM 的操作需要await nextTick()。热词里还提到vue 在 .env.development 文件里怎么写判断条件。这个其实很简单在.env.development里写VITE_API_BASE_URL/api这样的键值对然后在代码里用import.meta.env.VITE_API_BASE_URL读取。如果需要按环境判断可以用import.meta.env.MODE development。5.4 前后端分离项目中的 Vue 定位热词里出现springboot vue前后端分离、django vue整合、golang gin 打包 vue dist 合并部署这类问题本质是在问Vue 项目怎么和后端一起跑、怎么部署。前后端分离的架构中Vue 负责页面渲染和交互后端只提供 JSON API。开发环境通过 webpack/vite 的代理解决跨域// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/users就会被代理到后端的localhost:8080/users本地联调不用频繁改地址。生产部署时执行npm run build生成dist目录里面是纯静态文件。部署方式有两种常见选择Nginx 托管静态文件并配置反向代理到后端服务把dist目录整个嵌入后端服务比如 Spring Boot 里把 dist 放到resources/static下Gin 里用gin.Static(/, ./dist)。我个人偏好 Nginx 方案因为静态资源和 API 分离运维管理更清晰也方便以后加 CDN 缓存。但如果你没有独立运维人员用后端直接托管静态文件确实更省事。热词里还有若依vue项目连接打印机这类特定项目的问题其实最终方案都差不多前端通过浏览器的打印 APIwindow.print()调用系统打印或者对接第三方打印服务。若依的后台管理模板里通常封装了打印部分你在src/views下找对应用户的列表页面在操作列加一个“打印”按钮调用printJS或者直接用浏览器原生打印即可。写在最后的个人经验翻完这组热词最大的体会是Vue 生态的问题永远不是单一的“不会用”而是“会了但不够熟”。很多问题看起来是“框架 bug”实际上是对运行机制、依赖原理、组件生命周期的理解不够透。拿地图白屏来说看了几天文档不如自己断点打一遍拿 watch 数组新旧值相同来说理解了引用类型和响应式追踪的逻辑就不会被这种问题再坑第二遍。我自己整理笔记的习惯是每解决一个问题就把它归到“工程化/路由/状态/第三方集成/原理”这五类里去。时间久了这些问题之间会形成关联排查新 bug 的速度也会快很多。希望这篇笔记能给你一些可直接抄作业的方案哪怕是踩坑记录也能帮你少走几步弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价