资讯动态

Vue 3 核心原理与实战:从环境搭建到手写响应式系统

发布时间:2026/9/19 13:25:30 来源:尧图企业网站定制
做前端时间久了手头总会攒下几套自己都觉得宝贝的笔记。前几天整理硬盘翻出来一份跟 Vue 死磕出来的从环境配置到手写响应式源码的完整记录从安装工具链、搭脚手架到路由守卫、生命周期、样式透传再到脱离 Vue 源码用原生 Proxy 手写 reactive、ref、effect、computed最后还带了一堆实战里踩出来的坑和面试题解法。这篇就是把那份笔记重新梳理了一遍把里面最核心、最值钱的东西捞出来分享给正在学 Vue 或者准备面试的朋友。内容覆盖了环境搭建、基础语法、进阶原理、实战案例和工程化调试不管你是刚接触 Vue 的小白还是已经在项目里写过一段时间但觉得原理上差点意思的开发者这份笔记应该都能帮你把知识体系补得相对完整一些。1. 学习路线与工程环境先把地基打牢1.1 环境安装从 Node 到 Vite 项目创建这节笔记的第一行写的是“别急着写代码先把 Node 装对”。很多新手一上来就直接 npm install结果装到一半报各种版本错误最后才发现是 Node 版本跟脚手架不匹配。Vue 3 的官方脚手架 Vite 对 Node 版本有要求18 以上是比较稳的我用的是 18 LTS 和 20 LTS两个版本切换着跑过不同的老项目都没出过兼容性问题。环境这块核心就三步。第一步装 Node.js第二步设置 npm 镜像第三步用 Vite 创建项目。镜像设置直接关系到后面安装依赖的速度国内网络环境下默认源经常慢到怀疑人生我习惯在用户目录下的 .npmrc 里配置registryhttps://registry.npmmirror.com配完镜像再跑创建命令就顺畅多了。创建 Vue 3 TypeScript 项目的标准命令是npm create vuelatest这个命令会进入一个交互式问答界面让你选择是否需要 TypeScript、JSX、Vue Router、Pinia、ESLint、Prettier 等。我第一次跑的时候一脸懵不知道每个选项对应什么后来搞明白了TypeScript 建议选上虽然前期会有类型报错的痛苦但项目大了以后收益非常明显Vue Router 和 Pinia 是路由和状态管理的基础设施如果确定项目里要用就直接选后面省得手动配ESLint 和 Prettier 是代码规范工具选上以后从第一天起就按规范写代码比自己后期再补要轻松得多。依赖安装这块有个坑千万别用 npm install 硬等。实测下来用 pnpm 会比 npm 快不少而且对磁盘空间更友好。如果项目里已经锁定了 npm那至少加上--registryhttps://registry.npmmirror.com参数或者干脆把 .npmrc 配置好不然装个依赖喝杯咖啡回来可能还没跑完。1.2 编辑器与插件想写得顺先把工具调好笔记里专门有一页是记录编辑器选型的因为工具确实直接影响写码效率。我自己主力是 VS Code但也在 IDEA 社区版里折腾过 Vue 项目这里把两个都说说。先说 VS Code。Vue 单文件组件SFC的语法高亮和类型提示核心靠的是一个叫 Volar 的插件也就是官方现在推荐的 Vue Language Features。老一点的教程会让你装 Vetur但 Vetur 对 Vue 3 和 TypeScript 的支持不完整很多场景下提示会失灵我已经弃用了。建议直接从插件市场搜 Vue Language Features 安装配套再装 ESLint、Prettier 和 Vue 3 Snippets。Vue 3 Snippets 的价值在于你输入vbase之类的缩写就能快速生成单文件组件的模板代码省去手敲 setup 样板的时间。再说 IDEA 社区版。这里必须说句实话社区版对 JavaScript 和 TypeScript 的支持不如旗舰版完整Vue 插件在社区版里只能提供基础的语法高亮智能提示和跳转经常失灵。如果电脑上只有社区版硬着头皮写简单页面问题不大但项目复杂以后我还是建议切到 VS Code或者如果有条件用旗舰版。笔记里我给自己留了一句提醒“不要在工具短板上浪费精力编辑器不顺手就换技术选型不顺手就换别硬扛。”DevTools 也算工具链的重要一环。Vue DevTools 是调试 Vue 项目必备的浏览器插件装上之后可以看到组件树、路由、Pinia 状态、响应式数据的变化过程。有些时候代码逻辑看起来没问题但页面不更新打开 DevTools 看组件状态就能直接定位是数据没变还是视图没刷新。这个插件建议提前装好后面调试章节还会提到怎么配合它高效排查问题。2. 从使用到细节路由、生命周期、样式与组件通信2.1 路由传参与路由守卫Vue Router 是单页面应用绕不开的一环。笔记里我花了不少篇幅记路由传参因为实际项目里最容易出 bug 的就是参数传递。路由传参大概有三种方式query、params 和动态路由path 里的冒号参数。query 方式最直观URL 长这样/detail?id123。取的时候用route.query.id。它的优点是刷新页面参数还在因为参数就挂在 URL 上适合那种需要分享链接的场景。缺点也明显URL 会变长敏感信息别往里塞。params 方式是router.push({ name: detail, params: { id: 123 } })取的时候用route.params.id。这里有个经典的坑如果用 params 传参跳转时 URL 上不会显示参数刷新页面后参数会丢失。很多新手在列表页用 params 把 id 传到详情页详情页一刷新就报错拿不到数据排查半天找不到原因。动态路由是/detail/:id这种写法跳转时router.push(/detail/ id)取参方式跟 params 一致用route.params.id但 URL 上能看到参数刷新也不丢所以需要传 id 这种核心参数的场景我更推荐用它。路由守卫方面笔记里重点记了拦截器的用法。所谓路由拦截器就是 Vue Router 的全局前置守卫router.beforeEach。最常见的使用场景是登录鉴权判断用户有没有登录没登录就重定向到登录页并带上当前地址作为回跳参数。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.meta配置页面级别的权限信息比在守卫里写一长串路径判断干净得多。我习惯在路由配置里给需要登录的页面加上meta: { requiresAuth: true }后期维护权限逻辑只需要看路由表就行。2.2 生命周期在不同场景中的实际差异Vue 的生命周期是面试常考、开发常踩坑的一块。Vue 3 组合式 API 里常用的是 onMounted、onUnmounted还有配合 keep-alive 使用的 onActivated、onDeactivated。onMounted 是组件挂载完成后执行的钩子DOM 已经渲染完毕这时候可以操作 DOM、发请求、初始化第三方库。我把大部分初始化逻辑都放在这个钩子里。onUnmounted 则是组件卸载时执行适合做清理工作比如清除定时器、解绑事件、销毁地图实例。父子组件的生命周期执行顺序也是高频考点。父组件 beforeMount 之后子组件先完成 mounted然后父组件才 mounted卸载时则反过来父组件先触发 beforeUnmount子组件先完成 unmounted父组件才 unmounted。简单记忆就是挂载时子先父后卸载时父先子后。这个顺序在涉及父子组件数据传递和资源释放时非常关键。还有一个容易忽略的点是 keep-alive。被 keep-alive 包裹的组件在切换时不会走 unmounted而是进入 deactivated重新展示时触发 activated。这意味着如果你的组件在 activated 里有数据刷新逻辑切回来时会自动刷新体验很好但如果在 unmounted 里做了资源释放那在 keep-alive 场景下就不会执行要换成 onDeactivated。笔记里我专门标注过“用 keep-alive 时定时器和事件监听的清理一定放 deactivated否则就会残留。”2.3 样式作用域、透传与组件通信样式作用域是单文件组件里很基础但又很多人没搞透的概念。style scoped会给当前组件的所有元素加一个 data 属性编译后选择器带上属性选择器从而实现样式隔离。比如div classcard编译后可能变成div classcard>.parent :deep(.child-inner) { color: red; }编译后它会变成[data-v-父组件标识] .child-inner不去要求子组件元素带父组件属性从而穿透到子组件内部。这个技巧在封装第三方 UI 组件想要定制外观时经常用到。透传是 Vue 3 里一个相当重要的机制。父组件传给子组件但子组件没有在 props 里声明的属性和事件监听器会自动落到子组件根元素上这就是透传。通过$attrs可以拿到这些透传下来的属性。比如封装一个自定义按钮组件除了自己定义的 size、type 之外用户传的 class、style、onClick 这些都可以通过$attrs再手动绑定到内部元素上做到灵活的透传。template button classbase-btn v-bind$attrs slot / /button /template这里有个和透传密切相关的小知识点是inheritAttrs。默认情况下没有在 props 里声明的属性会自动继承到根元素上如果根元素不想被动接收这些属性需要在组件选项里设置inheritAttrs: false然后手动用$attrs去决定绑在哪里。笔记里我记了一个很典型的场景组件内部有多个根元素时不设置 inheritAttrs 会报错或者导致属性继承到错误的元素上处理方式是显式关闭继承再手动分配 $attrs。组件通信的另一种常用方式是 provide / inject适合跨层级传递数据。父组件通过 provide 提供数据任意层级的子组件通过 inject 接收不需要像 props 那样一层层往下传。需要注意的是用 provide 传响应式数据时最好传 ref 或 reactive 对象否则可能拿到的是初始值的快照。如果 provide 的是普通值子组件拿到的不会随父组件更新而变化这是一个很隐蔽的 bug。3. 进阶核心手写响应式系统理解 Vue 的灵魂3.1 为什么是 Proxy响应式系统的设计起点如果你只想“会用” Vue那这一章可以不用看。但如果你想弄明白 Vue 3 那些魔法一样的响应式更新到底是怎么来的面试被问到时也能说出个一二三来那手写一遍响应式系统是最快的方式。我自己就是通过脱离 Vue 源码用原生 Proxy 手写了一版包含 reactive、ref、effect 和 computed 的迷你实现之后才真正把响应式原理吃透的。先想清楚一个最根本的问题什么是响应式一个数据变量被多个地方读取当这个变量变化时所有读取过它的地方都自动更新这就是响应式。Vue 3 的整个响应式机制就干这一件事。Vue 2 时期实现响应式靠的是Object.defineProperty它只能拦截对象的已有属性所以新增属性、删除属性都侦测不到Vue 2 需要额外用Vue.set和Vue.delete来补漏。还有一个麻烦是数组索引变化和 length 变化也拦截不到得靠重写数组方法来实现。Vue 3 改用 Proxy 之后这些问题天然就解决了。Proxy 可以代理整个对象不管是读取已有属性、新增属性、删除属性还是读取不存在的属性都能拦截。更重要的是Proxy 还能拦截hasin 操作符、ownKeysObject.keys、deleteProperty等 13 种操作覆盖面比 defineProperty 广得多。这就是 Vue 3 选 Proxy 而不是 defineProperty 的根本原因能力更完整能够更可靠地追踪所有变化。3.2 手写 reactive、ref 与 effect响应式系统里有三个核心概念依赖收集track、触发更新trigger和副作用函数effect。effect 表示一个依赖响应式数据的副作用函数页面渲染本质上就是一个副作用。track 表示读取数据时把当前的 effect 记录到数据的依赖列表里。trigger 表示修改数据时取出该数据的所有依赖 effect依次执行。数据结构上Vue 3 用 WeakMap 来存依赖结构是target - key - deps。WeakMap 的好处是 key 是对象当对象本身不再被引用时可以被正常垃圾回收避免内存泄漏。先写一个最简单的 reactiveconst targetMap new WeakMap() let activeEffect null function track(target, key) { if (!activeEffect) return let depsMap targetMap.get(target) if (!depsMap) { depsMap new Map() targetMap.set(target, depsMap) } let dep depsMap.get(key) if (!dep) { dep new Set() depsMap.set(key, dep) } dep.add(activeEffect) } function trigger(target, key) { const depsMap targetMap.get(target) if (!depsMap) return const dep depsMap.get(key) if (dep) { dep.forEach(effect effect()) } } function reactive(target) { return new Proxy(target, { get(target, key, receiver) { track(target, key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { const result Reflect.set(target, key, value, receiver) trigger(target, key) return result } }) }这段代码里要注意Reflect.get和Reflect.set的使用。直接target[key]取值时如果对象有 getterthis 指向的是 target 而不是代理对象这样可能导致漏掉对代理对象自身的依赖追踪。用 Reflect 并传入 receiver可以保证 this 指向代理对象行为更准确。然后写 effectfunction effect(fn) { const effectFn () { activeEffect effectFn fn() activeEffect null } effectFn() return effectFn }这里执行 effect 的时候先把当前副作用函数存到全局变量 activeEffect 里然后执行 fn。fn 一旦读取了响应式对象的属性就会触发 get进而调用 track把 activeEffect 收集到对应属性的依赖集合里。这样后续属性变化时trigger 就能找到这个 effect 并重新执行。有了 reactive 和 effectref 就是一层简单封装。ref 主要是为了包装基本类型值或者保持对象语义一致。基本类型的值没法用 Proxy 代理所以 ref 内部用对象包了一层value 属性用来存放真实值function ref(value) { return reactive({ get value() { return value }, set value(newVal) { value newVal } }) }真实 Vue 里的 ref 实现跟这个稍有出入但核心思路就是用一个对象包装一下然后通过 reactive 让这个对象具备响应式能力。另一个更直接的思路是ref 用一个{ value: 原值 }的对象作为 target 传给 reactive这样在模板里写ref.value就能触发依赖收集和更新。3.3 补全 computed 以及依赖清理computed 是一个带缓存的 effect。它有两个特点一是惰性求值不读取它的 .value 就不会计算二是依赖变化后不立即重新计算而是在下次读取时判断值是否真的变了变了才重新计算。手写一个简版 computedfunction computed(getter) { let cachedValue let dirty true const effectFn effect(() { dirty true cachedValue getter() }) return { get value() { if (dirty) { cachedValue effectFn() dirty false } return cachedValue } } }这个版本能跑但有个问题依赖变化时 getter 会立即执行并没有真正实现惰性。更完整的实现需要控制调度时机依赖变化时只把 dirty 置为 true不立即执行 getter等读取 value 时才重新计算。这正是 Vue 源码里 computed 用调度器来实现的核心逻辑。还有一个容易被忽略的问题就是依赖清理。再看前面那段 track 逻辑如果 effect 里通过 if 条件分支读取不同的属性那么第一次运行时读取了 A第二次读取条件变了转而读取 BA 的依赖集合里却还保留着这个 effect后续 A 变化时会触发这个已经不需要 A 的 effect产生多余的更新。解决这个问题的办法是每次执行 effect 前先把它从所有依赖集合里删除执行完再重新收集。实现上常用的技巧是给每个 effect 加一个 deps 数组记录它被哪些依赖集合收集过function cleanup(effectFn) { for (let i 0; i effectFn.deps.length; i) { const dep effectFn.deps[i] dep.delete(effectFn) } effectFn.deps.length 0 }然后在 track 里把 dep 存进 effectFn.deps在 effect 执行前先 cleanup。这个细节虽然代码量不大但却是手写响应式系统从“能用”到“正确”的关键一步。我自己第一次写的时候没有做依赖清理调试了半天才发现是旧依赖没清掉导致无限执行。建议读者手写的时候把这一步加上才能真正理解 Vue 源码里那些清理逻辑存在的意义。4. 业务实战地图那些高频出现的真实需求4.1 m3u8 播放从 hls.js 到浏览器兼容开发视频相关业务的时候经常遇到 m3u8 格式的流媒体文件。m3u8 本质是 HLS 协议下的索引文件里面记录的是一串 ts 分片文件的地址播放器按顺序拉取这些分片可以实现边下边播。原生 video 标签并不直接支持 m3u8只有 Safari 因为系统底层支持 HLS 才能直接播。Chrome、Edge、Firefox 这些主流浏览器都需要借助 JavaScript 库来解码播放。实际项目里我用得最多的是 hls.js它通过 MSEMedia Source Extensions技术把 m3u8 的流数据转换成浏览器可以播放的格式喂给 video 标签。在 Vue 项目里使用 hls.js 的典型写法是npm install hls.jstemplate video refvideoRef controls autoplay muted/video /template script setup import { ref, onMounted, onUnmounted } from vue import Hls from hls.js const videoRef ref(null) let hls null onMounted(() { const video videoRef.value if (Hls.isSupported()) { hls new Hls({ enableWorker: true, lowLatencyMode: true }) hls.loadSource(https://example.com/stream.m3u8) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () { video.play() }) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持直接赋值即可 video.src https://example.com/stream.m3u8 } }) onUnmounted(() { if (hls) { hls.destroy() } }) /script这个方案的好处是整个流程都是纯前端 JavaScript 实现不需要额外装浏览器插件也不需要自己在服务端做转码。有朋友问过为什么会有“免安装”这个说法其实指的就是这种纯 JS 解包播放的方案浏览器直接跑 hls.js不像过去有些播放方案要装 Flash 之类的插件。实际踩坑记录里m3u8 播放最常见的问题是跨域。m3u8 文件和 ts 分片接口如果没有配置Access-Control-Allow-Originhls.js 去请求时会被浏览器拦截表现为日志里报 CORS 错误视频一直黑屏不播放。排查办法是打开浏览器的 Network 面板看分片请求是否被 CORS 阻塞然后找服务端把头补上。另外一个坑是 autoplay 策略浏览器禁止带声音的视频自动播放所以自动播放必须加上 muted 属性用户手动点击后再取消静音。4.2 组件集成实录地图、富文本、矩阵树图业务里高频出现的还有地图、富文本编辑器和各类图表组件笔记里分别记了选型建议和接入要点。腾讯地图的 JS SDK 在 Vue 里的接入核心就两步。第一在 index.html 里引入 SDK 脚本或者在组件里动态创建 script 标签去加载加载完成后再初始化地图实例。第二组件卸载时一定要调用地图实例的销毁方法不然会残留事件监听和定时器页面长期切来切去会越来越卡。初始化代码大致如下script setup import { ref, onMounted, onUnmounted } from vue const mapRef ref(null) let mapInstance null onMounted(() { const script document.createElement(script) script.src https://map.qq.com/api/gljs?v1.expkey你的Key script.onload () { mapInstance new window.TMap.Map(mapRef.value, { center: new window.TMap.LatLng(39.984104, 116.307503), zoom: 14 }) } document.head.appendChild(script) }) onUnmounted(() { if (mapInstance) { mapInstance.destroy() } }) /script富文本编辑器我对比过 wangEditor 和 tinymce 两个方向。tinymce 功能非常全但引入依赖比较重licensing 也要注意wangEditor 是国产轻量方案对中文场景支持好UI 也更贴合国内业务适合中小型后台系统。在 Vue 3 里接入 wangEditor 的话用官方封装的wangeditor/editor-for-vue就行注意它内部用到了 DOM 操作服务端渲染场景SSR下要动态导入避免在服务端执行时访问不到 document。矩阵树图这块如果需求里出现“矩阵树图”这三个字九成是在说 ECharts 的 treemap或者是显式的树形矩阵可视化。ECharts 在 Vue 里的接入方式很成熟直接安装 echarts在 onMounted 里初始化实例把 option 传进去就行。要注意的是 option 的更新不要每次数据变化都重新 setOption 初始化整个图表那样会闪白性能也差。正确做法是在 init 之后通过 watch 数据变化再调用 setOption 更新配置并且加上notMerge: true参数来完全替换数据系列。还有一个 v屏幕工具类组件移动端 H5 项目里经常配合 Vant 组件库使用。有朋友问过 Vant 的 picker 怎么做复选这个需要特殊处理一下因为 Vant 的 Picker 默认是单列单选。复选的思路一般是把 Picker 升级为多列联动或者改用 Popup 加 Checkbox 组合来做多选界面。我笔记里记的方案是后者简单直接UI 上也好控制。4.3 对接第三方登录、SpringBoot 与宝塔部署前后端分离项目里前端对接后端接口是最常见的工程场景再往上走一步就是部署上线。登录这块很多项目会对接第三方登录比如微信扫码登录。官方提供的 JS 文件在普通 HTML 页面里很容易引入但放到 Vue 项目里会有点别扭因为它是传统脚本加载方式不是一个 npm 包。我的做法是动态加载官方脚本等脚本加载完成后再初始化登录组件。核心逻辑是function loadScript(src) { return new Promise((resolve, reject) { const script document.createElement(script) script.src src script.onload resolve script.onerror reject document.head.appendChild(script) }) }这样就能在 Vue 组件里按需加载避免在全局 HTML 里写死外部脚本影响首屏加载速度。回调处理上要注意第三方登录一般是前端拿到临时 code把 code 发给后端后端再换用户信息。整个流程里前端要保存好回跳地址登录成功后再重新路由到之前的页面。SpringBoot Vue 前后端分离项目的部署我自己在阿里云服务器上配合宝塔面板操作过多次流程已经比较固定。前端打包后是纯静态文件后端是 Java 的 jar 包两个服务分开部署Nginx 做入口。前端部分npm run build之后会生成 dist 目录把这个目录上传到服务器并用 Nginx 指向它。核心配置如下server { listen 80; server_name your-domain.com; root /www/wwwroot/vue-project/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这里try_files是前后端分离项目必须写的一行它的作用是让路由为 history 模式的单页应用在刷新时都回到 index.html由前端路由接管。如果不写用户直接访问/detail页面或者刷新页面Nginx 找不到对应文件会报 404。后端部分使用宝塔面板的话最省事的方案是用它的 Java 项目管理器来部署 SpringBoot 的 jar。打包上传后在面板里配置启动命令、端口和日志路径。部署完再把域名解析指向服务器配置 SSL 证书升级 HTTPS整个项目就可以上线访问了。跨域问题在前后端分离的部署阶段反而简单。开发环境本地起两个服务不同端口需要用 Vite 的 proxy 或者后端加 CORS 配置来解决生产环境只要走 Nginx 反向代理前端请求同域名的/api路径Nginx 再转发到后端服务就不存在跨域了这是生产环境最推荐的部署方式。5. 调试、工程化与面试备战5.1 打 debug 的三种姿势写 Vue 项目不会调试等于少了一条腿。我笔记里记了三种主要的调试方式各有适用场景。第一种是最直接也最常用的在代码里写 console.log配合 Vue DevTools 看数据变化。比如在 onMounted 里打印一下拿到的接口数据或者在 watch 回调里打印新旧值能快速确认数据流是否正常。这种方法在排查业务逻辑问题时效率很高。也经常碰到有人在浏览器里看到页面报错但代码没提示这时候 Console 面板的报错堆栈就是第一手线索。第二种是浏览器自带的 debugger 断点调试。在 Sources 面板里找到打包后的源码打断点刷新页面代码会停在断点处可以逐步查看变量作用域和调用栈。但这里有个坑生产环境打包后的代码是压缩混淆过的断点根本没法看。所以需要在 Vite 配置里开启 sourcemapexport default defineConfig({ build: { sourcemap: true } })开 sourcemap 之后浏览器里能映射回源码打断点就和平时写代码一样直观。注意生产环境上线时建议把 sourcemap 关了避免源码泄露。第三种是 VS Code 里的调试配置。在项目根目录建.vscode/launch.json{ version: 0.2.0, configurations: [ { type: chrome, request: launch, name: Vue Debug, url: http://localhost:5173, webRoot: ${workspaceFolder}/src } ] }这样可以直接在编辑器里给代码打红点然后按 F5 启动浏览器调试断点命中的每一步都能在编辑器左侧看到变量变化比在浏览器里看更沉浸。这套方案特别适合调试复杂的响应式副作用问题。还有个看起来有点奇怪的问题有朋友问 Node.js 服务端的 console.log 能不能反显到 Vue 页面上。本质上是前后端日志如何共享的问题。直接互访不太可能但有一个实用方案后端启动一个小型 WebSocket 服务把日志实时推送前端页面通过 WebSocket 连接到这个端口把日志打印到页面上。不过不建议生产环境这么干只在开发调试时临时用。5.2 工程化选型Pinia、ESLint、Prettier、Vitest创建项目时交互式问答里那串名词很多人只知道顺着选完但并不清楚每个东西是干什么的。这里把工程化相关的主要选型都展开说一下。Pinia 是 Vue 3 官方推荐的状态管理库用来替代 Vuex。和 Vuex 相比Pinia 去掉了 mutations在组件里直接调用 actions 修改状态API 更简洁对 TypeScript 类型推导更友好也彻底解决了 Vuex 的模块嵌套问题。用起来非常简单import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: , userInfo: null }), actions: { setToken(token) { this.token token } } })在组件里拿 storescript setup import { useUserStore } from /stores/user const userStore useUserStore() /scriptESLint 负责检查代码规范Prettier 负责格式化代码。两者配合起来ESLint 管逻辑规则比如未使用变量、隐式 any、强制分号Prettier 管排版规则比如缩进、单双引号、行宽。第一次配置的时候容易遇到 ESLint 和 Prettier 规则冲突的问题常见方案是安装 eslint-config-prettier 来关闭与 Prettier 冲突的 ESLint 规则让 Prettier 全权负责格式化ESLint 专注逻辑检查。Vitest 是 Vite 生态下的单元测试框架API 跟 Jest 高度兼容因为底层使用了 Vite 的转换能力启动速度快很多。创建一个基础测试文件import { describe, it, expect, vi } from vitest import { mount } from vue/test-utils import TodoItem from /components/TodoItem.vue describe(TodoItem, () { it(renders title, () { const wrapper mount(TodoItem, { props: { title: 测试任务 } }) expect(wrapper.text()).toContain(测试任务) }) })单测的核心价值不是追求覆盖率多高而是保证核心逻辑在重构之后没有被破坏。我一般只对工具函数、数据转换、接口请求封装这几类写单测UI 组件的测试按需补充避免花了大量时间写了一堆测试结果 UI 一调整就要跟着全改。5.3 Vue 与 React 对比这个对比题几乎每次面试都会出现它的核心考点是理解两者在设计理念上的差异。Vue 的模板语法更友好模板是 HTML 的扩展对后端转前端的开发者非常友好学习曲线平缓。React 则是彻底的 JavaScript 化用 JSX 把 HTML 写在 JavaScript 里面强调“UI 就是函数的映射”。数据流方面Vue 的响应式系统自动追踪依赖数据改了关联视图自动更新React 需要显式调用 setState 触发更新通过不可变数据immutable和 diff 算法来计算哪些 DOM 需要变。Fiber 是 React 16 之后引入的协调算法核心它的作用是让 React 的更新过程可中断、可恢复、可调度优先级。页面很复杂时更新任务会被拆分成一个个小单元每执行完一个单元就把控制权交还给浏览器让浏览器有时间处理用户交互和高优先级的渲染任务避免长时间占用主线程导致页面卡顿。Vue 没有类似的 Fiber 机制因为它靠模板编译静态分析在编译阶段就能标记出哪些节点是动态的更新时可以直接精确到组件级不需要像 React 那样从根节点递归对比整棵虚拟 DOM 树。这种编译期的前置工作让 Vue 的运行时负担相对较小。面试时回答这类问题建议具体到细节Vue 的优势是模板直观、响应式自动追踪、开发效率高React 的优势是生态庞大、函数式编程模型清晰、社区资源极丰富JSX 提供了更大的灵活性。没有绝对的好坏关键看团队技术积累和项目场景。6. 常见问题排查速查表最后整理一份实战中出现的典型问题速查按现象、原因、解决方案三列来记录遇到类似问题可以直接照着排查。现象根本原因解决方案刷新页面后详情页 404Nginx 没有配置 try_fileshistory 路由刷新落到了真实路径在 Nginx 的 location / 里加try_files $uri $uri/ /index.html;params 传参刷新后丢失params 参数不挂在 URL 上刷新后路由重建参数清空换成 query 或动态路由传参scoped 样式中修改子组件内部元素无效子组件元素带的是子组件的 data 属性父组件选择器匹配不到用:deep()选择器穿透样式作用域地图组件多次切换后页面卡顿组件卸载时没有销毁地图实例事件和定时器残留onUnmounted 里调用地图实例的 destroy 方法m3u8 播放黑屏且网络请求报 CORS 错误m3u8 或 ts 接口未配置跨域响应头服务端配置 Access-Control-Allow-Origin 等头自动播放视频没有声音或无法播放浏览器 autoplay 策略限制带声音的视频自动播放video 加 muted 属性自动播放用户交互后再取消静音console.log 打印的对象展开时才显示最终值浏览器控制台的惰性取值机制需要看快照时用JSON.parse(JSON.stringify(obj))深拷贝后再打印dev 环境接口跨域前端服务与后端服务端口不一致配置 Vite proxy 代理转发或后端添加 CORS 配置笔记里还留了几条提醒是给自己看的也一并写出来。第一调试前先清空浏览器缓存和 Service Worker很多“改完没生效”其实是被缓存坑了。第二改别人的代码之前先看 Git 提交记录搞清楚最近的变更可能引发的连带问题。第三遇到响应式不更新的问题优先用 Vue DevTools 看数据到底变没变而不是闷头改代码。关于后端 console.log 的日志实时看到前端页面的需求如果你临时调试非要看可以给后端加一个 WebSocket 日志转发前端用一个小组件展示最新的几条日志。但这个方案有两个前提第一仅限开发环境使用第二日志内容做脱敏处理不要把用户敏感信息发到浏览器。别问我为什么加这个提醒因为真有人把生产环境的日志接到页面上然后被安全审计一顿教育。后记一条自己总结的学习心得这份笔记写到后来我最大的体会是学 Vue 光看文档进步很慢真正让你脱胎换骨的是两个动作。第一个动作是动手写一个业务闭环的项目比如一个带登录、路由守卫、状态管理、接口请求、部署上线的管理系统把使用层面的知识串起来。第二个动作是动手拆源码从手写响应式系统开始把 reactive、ref、effect、computed 自己实现一遍哪怕只是一个简化版也会让你在面试和排查复杂问题时像开了天眼一样通透。最后一个建议也是我自己一直在用的习惯建一个个人踩坑记录文档每解决一个奇怪的问题就记一笔记录现象、原因、解决思路三列。时间久了这份笔记就是你最值钱的私货比任何教程都好用。Vue 生态更新快但底层的原理和解决问题的思路不会过时这笔账怎么算都不亏。

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

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

免费获取报价