ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

写了三年Vue代码还是一团糟?从病灶到重构的实战指南

写了三年Vue代码还是一团糟?从病灶到重构的实战指南 写这篇文章的起因挺简单——我在一个技术社群里看到有人问“写了三年 Vue为什么每次回头改自己的代码还是想重写”底下跟了几十条共鸣。我点进他的仓库看了几个文件说实话脸有点发烫因为我刚工作头两年写的代码几乎一模一样。如果你也在 Vue 的生态里泡了三年前后打开自己半年甚至三个月前的项目第一反应不是“这代码是我写的”而是“这代码居然能跑”那这篇文章就是写给你的。先把结论放在这儿写了三年代码还是一团糟绝大多数情况下不是你不够努力而是你一直在“写代码”从来没有真正地“理代码”。三年重复劳动攒下的只是熟练度不是结构化能力。下面我会按我自己的复盘过程从病灶诊断、停滞原因、改造方法到可复用的自查清单一点一点拆开讲。1. 用三年经验还是三年重复先给代码做个亚健康筛查我见过很多三年经验的 Vue 开发者功能做得飞快组件库用得贼溜面试题背得滚瓜烂熟但一打开他的业务项目状态满天飞组件嵌套七八层props 和 emit 像蛛网一样连到各个角落。这种“熟练的混乱”比“生疏的干净”更可怕因为你已经学会了在混乱中打补丁却丧失了识别混乱的能力。为什么我敢说你大概率也中招了咱们做个简单的自我筛查不查别的就查这几项新需求来时你是先理清影响范围再动手还是先找到“相似代码”复制一份改改你的页面组件里是否有一大半逻辑只是把数据从接口搬到模板中间没有任何抽象Vuex/Pinia 里的 state 是否出现过“某个组件独享却放在全局”的数据全局样式文件是不是已经上千行而且很多样式只在某一个页面用到你是不是经常在组件里写$emit连续传四五个事件名而接收方只是换一层再$emit一次这五个问题只要中了两个以上你的代码大概率已经在亚健康状态了。为什么用“亚健康”这个词因为项目还能跑功能还能交付甚至线上也没出过什么大事故你很难说服自己去重构它。直到某一天一个十分钟能搞定的小需求你整整改了一下午还引入了两个回归 bug——这时候你才意识到出问题了。我在最初写 Vue 的那段时期就经历了这个从“莫名自信”到“自我怀疑”的过程。后来我把这个过程叫“代码的亚健康筛查”不是看有没有 bug而是看维护成本有没有在悄悄膨胀。因为 bug 是显性的维护成本是隐性的拖到最后才爆发。有一句话我后来常在评审会上讲代码写出来是给机器跑的但更是给人读的。机器只在乎能不能运行人在乎的是能不能维护。如果一段代码除了你自己没人敢动那不叫技术壁垒那叫技术债。筛查完之后接下来的问题是我们到底是怎么把代码写成这个样子的2. 复盘我刚工作时的老项目一团糟的五个真实病灶为了写这篇文章我专门翻出了刚入行第一年写的几个 Vue 项目。看完那些代码我意识到我当年犯的错几乎可以按类型归类每一条都不是孤立的它们互相影响、彼此放大。我挑五个最典型的出来讲。2.1 组件拆分跟着“视觉模块”走职责边界永远模糊这是我最早犯的、影响最深远的错。当年我拆分组件的唯一标准是“页面上哪些地方长得很像”比如这有个卡片我今天想抽成Card.vue那里有个弹窗我就抽成Modal.vue。这种拆法看似有理实际是陷阱。举个例子我做过一个订单列表页面里面每行有一组操作按钮包括查看详情、退款、改地址、联系客服。我当时傻乎乎地抽了一个OrderActionButtons.vue四个按钮四个$emit接收方是父组件父组件又把这四个事件原封不动地转发给页面容器。后面需求变更这几个按钮要按不同的用户权限显隐我改了三天差点崩溃。问题根源在哪我拆组件的时候是站在“长什么样”的角度而不是“干什么事”的角度。操作一个订单本身是一个完整的业务动作它涉及状态流转、权限判断、接口调用、结果反馈。你把它拆成四个按钮放出去就是把这套完整逻辑剁碎了撒到组件树上各个节点然后再靠事件把它们一串串串起来。正确思路应该反过来先把业务职责画出来再看哪些展示层的中转是多余的。按这种思路四个按钮背后的逻辑应该收拢到一个组合式函数或者一个负责订单操作能力的模块里组件只负责把状态渲染出来、把用户动作抛进去。可惜当年的我完全没这个概念。2.2 props 和 emit 满天飞组件通信全靠“血缘关系”硬凑写 Vue 一段时间后大家基本都有这个感觉props 往下传容易事件往上传麻烦一旦涉及跨层级的兄弟组件通信更是无从下手。于是很多人——包括当年的我——直接选择硬凑。我的老项目里有个典型例子商品筛选栏在页面顶部商品列表在页面中部购物车入口在页面底部。三个位置的数据有明显关联筛选条件变了列表要重新加载加入购物车了底部角标要更新。当年我的实现是把这个状态一股脑放在页面父组件里往下传 props往上收 emit然后页面父组件变成了一个什么都知道的“上帝对象”。最崩溃的是一次需求要求筛选栏同时控制列表和左侧分类栏我一晚上连加四个 props 三个 emit测试一跑改了一个联动条件另一个不知道哪里跟着变了。后来我把这个状态抽到了一个共享 store 里代码量删掉了三分之一联调时间从一下午变成半小时。这里不是说 props/emit 不能用而是跨层级、跨分支的通信不该靠组件树一层层往上抛再一层层往下抛。这种约定一旦深到三四层你的组件依赖关系就会变成一团乱麻。组件的可复用性也被摧毁离了父组件这组件什么都干不了。2.3 路由与页面组件里塞满了本该属于业务模块的逻辑第三个病灶是“路由即页面页面即一切”。当年我用 Vue Router 时习惯把所有跟当前路由相关的数据请求、权限校验、埋点统计全部写在页面组件里。比如一个UserProfile.vue里面有用户信息获取、联系人列表加载、操作日志查询……七八个请求混在一个created钩子里后面接手的人根本分不清哪段逻辑服务于哪个模块。随之而来的问题就是这个页面没法做单元测试因为任何一个请求失败都可能拖垮整页初始化也没法做局部复用因为哪怕你只想复用这个页面的“操作日志”部分也只能把整个页面一起搬走。后来我慢慢意识到路由组件应该只是一个“薄壳”它只负责把 URL 参数解析出来、把对应的业务模块组合到一起、再把页面级状态管理起来。至于用户信息该拉取哪些数据、操作日志如何分页那是业务模块自己的事。路由文件的职责越少你的应用边界就越清晰。2.4 样式作用域失控全局样式文件越写越厚Vue 单文件组件有个福利就是style scoped当年我一度以为有它在就高枕无忧了。结果后面出现的灵异事件是一个按钮的字体颜色我改Button.vue里明文写的#333没用最后发现是全局app.css里某段选择器优先级更高一个页面布局错乱排查半天发现是另一个组件里的样式没加 scoped漏出去了把全局都污染了。我的老项目全局样式文件累计到两千多行之后基本就处于“没人敢动”的状态。为什么因为不知道哪个全局类偷偷被项目里某处依赖着改一处炸一片。这种心态一旦形成代码质量就在加速腐化。正确的做法说起来其实很简单全局样式只放真正全局的东西——CSS 变量、reset、原子类工具、主题色板。任何布局类、组件类样式要么进 scoped要么进 CSS Modules。更重要的是项目里要有约定新增样式默认写在组件内部真的需要全局复用再提到公共层。而不是反过来先写全局出了问题再加 scoped。2.5 复制粘贴式开发抽了两层就嫌麻烦干脆把相似代码各写各的最后这个病灶说它最伤项目都不为过。我在写第三年项目的时候偶尔还会犯这个毛病看到两个页面写接口请求的逻辑几乎一样直接复制一份把 URL 改了参数改了就完事了。为什么不抽因为抽取意味着改公共文件、意味着考虑多种调用场景、意味着写文档和测试。那时候我总觉得“能跑就行下次再抽”结果“下次”永远没有来。于是项目里出现了一批“双胞胎函数”——getUserInfo和getUserProfile其实干的事一模一样只是返回字段稍有不同。再后来新需求需要同时改动这两个函数我改了第一个忘了第二个线上 bug 就来了。这种 bug 是最冤枉的因为它纯粹是重复代码带来的维护问题。现在我给自己立了一条规矩复制粘贴第三次的时候就必须停下来抽象。第一次复制是不得已第二次复制是警钟第三次复制就是对自己团队的不负责任。抽象不一定非得设计模式可以只是一个公共函数、一个 composable、一个 mixin——只要你控制住了重复代码就自然变干净。3. 写了三年没有长进不是天赋问题是反馈回路断了看完上面五个病灶你可能会想这些道理我现在也懂但为什么过去三年我都没想到我的答案是你可能根本没给自己想到的机会。因为这三年里你的成长反馈回路一直是断的。3.1 需求倒逼项目节奏让你根本停不下来重构回想一下大多数公司对开发者的考核还是“按时交付需求”你周而复始地接需求、改 bug、发版本。在这种节奏里重构永远排在最后甚至永不排期。就算某个周你突然有半天空档你也不会去动那些陈年烂代码因为你怕动完出回归没人给你兜底。逻辑上这没问题——业务要稳定优先。但代价是你的代码结构一直没有被修剪随着时间推移项目里的坏味道会指数级增殖。我第一次意识到这事是在一个迭代中旧需求改了 A 模块结果五个下游页面收到了影响我加班修了三天终于恢复上线第四天又有新需求我发现自己根本没有重建缓冲区的机会。那时候我才明白这条路径的尽头不是变好而是越来越差。3.2 代码评审流于形式坏味道被一次次默许我也经历过不少代码评审但大部分评审会关心的都是“功能对不对”“命名规不规范”“有没有明显 bug”很少有人问“这个模块的职责边界是否清晰”“这段逻辑该放在组件里还是抽出去”。这不能怪评审人毕竟大家都很忙但后果就是每一次 merge 都是一次对坏味道的默认许可。代码评审要有产出就必须从“审对错”升级到“审结构”。我在带小团队之后强制规定每个 MR 必须附带一段“这个改动影响哪些模块、为什么这么设计”的说明评审人必须回答“如果我是新接手的人我能看懂这个结构吗”。这么做之后至少挡住了一半反模式进场。3.3 技能的敌人是“熟练”——你停止了重构练习第三个原因是技能本身的特性。刚学 Vue 的时候你每天都能学到新东西但当你对生命周期、指令、响应式原理都熟得不能再熟时继续提升的路径就只剩重构、抽象、架构这些“无趣但重要”的东西。很多人到了这一步就不愿意往前走了因为重构带来的快感远不如写新功能来得直接。如果只是停留在“会写”的层面三年和一年的差别其实不大。真正拉开差距的是你是否能把一个功能从“能跑”演进到“好维护”。这个过程需要刻意练习必须故意找一些烂代码去重写去模拟真实场景里的熵增。这些年我保持的习惯是每个月至少抽一天不接新需求只做“顺手重构”——看到不合理的组件边界就调整看到重复代码就抽离看到不再需要的事件总线就删掉。这么做看起来进展慢但其实是在给项目的未来投资。4. 从坏味道到好结构四个立刻能落地的改造动作复盘完问题关键是改变。我给你四个改造动作它们不依赖大型重构项目也不需要专门排期日常开发中随手就能做。每一个我都给了具体的代码思路看完照着改就行。4.1 用“单一职责”重新切分组件而不是按“长得像”这个动作的核心是把组件拆分的依据从“界面长相”换成“业务职责”。你去看一个页面先不要想“哪些地方看起来可以复用”而是问自己“这个页面承担了几个核心业务动作”。一个业务动作的完整链路应该尽量待在一个模块/组件/组合函数里。举个例子假设有个ProductList.vue同时负责“商品列表渲染”“筛选条件管理”“分页加载”“加入购物车”那这个组件明显太胖了。正确的切法可能长这样template div ProductFilter changeapplyFilter / table tr v-foritem in filteredProducts :keyitem.id td{{ item.name }}/td td{{ item.price }}/td td button clickaddToCart(item)加入购物车/button /td /tr /table /div /template script setup import { ref, computed, onMounted } from vue; // 筛选和购物车逻辑从 composables 导入组件只负责组织 UI import { useProductFilter } from ./useProductFilter; import { useCart } from /stores/cart; const { filters, applyFilter, filteredProducts } useProductFilter(); const cart useCart(); function addToCart(product) { cart.add(product); } /script注意看这个组件的工作被拆开了useProductFilter负责筛选和列表过滤的状态useCartStore 负责购物车组件本身只负责把 UI 组装起来。下次你想改筛选逻辑进useProductFilter想改购物车规则进 cart Store。谁也不会踩谁的脚。这个改动难不难其实不复杂。难点在于你要习惯“先画职责边界再写具体代码”。我知道很多人开始会觉得“绕”但用上两周你就会发现改需求的效率反而高了因为你知道去哪里改。4.2 用组合式函数Composables给script setup减肥Vue 3 的 Composition API 最大的价值就是能把组件里的逻辑按“业务能力”抽出去而不是按 options 的位置data/watch/methods被动分成三堆。我见过太多人嘴上用着script setup写出来的东西却还是“每个页面一个大而全的脚本”。改造成一个通用的拆法凡是“读接口、存数据、处理 loading/error”这套逻辑全部抽到组合式函数里。// useListData.js import { ref, onMounted } from vue; import request from /utils/request; export function useListData(url, { immediate true } {}) { const list ref([]); const loading ref(false); const error ref(null); async function fetchData() { loading.value true; error.value null; try { const { data } await request.get(url); list.value data; } catch (e) { error.value e.message; } finally { loading.value false; } } if (immediate) { onMounted(fetchData); } return { list, loading, error, refresh: fetchData }; }组件里只需要写const { list, loading, error, refresh } useListData(/api/products);页面里需要的其他能力比如表单校验、分页、权限判断可以继续抽成各自的 composable。当你把一个页面组件从 500 行减到 150 行且每行都在做“组装”而不是“具体逻辑”的时候那个代码的清晰度是完全不同的。有人会担心抽取 composable 会不会引入不必要的过度抽象我的经验是如果一段逻辑在两个以上组件中出现过或者组件本身已经超过 300 行就可以考虑抽。没有硬性标准但这个阈值能帮你避免很多无意义的重构。4.3 用 defineModel 和自定义事件收拢组件通信不要层层中转Vue 3.4 推出了defineModel这算是在组件通信上帮了大忙。以前我们写一个受控输入组件需要 props emit 两件套现在一个defineModel就能把 v-model 的读和写都做掉极大减少模板代码。不过通信的关键问题不是 API 用哪一套而是数据的流向要清晰。我给自己定的原则是父传子、影响子组件内部展示的数据用 props。子反馈给父、触发父组件行为的数据用 emit/defineModel。兄弟组件之间共享、跨页面传递、需要持久化的数据用 Pinia。绝不通过“爷传父、父传子、子再传孙”这种传递链完成跨级通信。改代码时把那些纯传声筒式的中间组件里的$emit(update:xxx)换成父组件的实际业务逻辑或者干脆把跨级共享的数据直接放到 store 里。你会发现删掉的代码可能比你想象的还多而且这些代码大概率永远不需要再写回来。4.4 用 Pinia 承接跨组件状态别再用事件总线硬撑Vue 2 时代很多人用 Event Bus 解决跨组件通信Vue 3 时代我强烈建议别再用这个方案。Event Bus 看起来很轻量但它把事件源和事件处理者完全解耦了项目一复杂你根本不知道一个事件谁在发、谁在听、冒泡顺序是什么。排查线上问题的时候你会像在深海里捞针。Pinia 的写法很直观而且天然和 Vue Devtools 集成状态变化一眼就看得到。我做过一个项目把之前的 Event Bus 换成 Pinia 之后连带修掉了两个“偶发逻辑错误”因为它们都是事件时序错乱导致的。用 Store 至少能保证状态是单向流动的组件里 dispatch actionaction 改 state视图被动更新。出现问题查起来就是一条直线。我还遇到过一个场景筛选条件变更后列表需要重新请求购物车角标需要更新操作日志需要记录。如果用 Event Bus你至少要定义三个事件名然后在三处分别监听还要担心监听器有没有被正确销毁。如果统一放到一个“筛选”相关的 store 里三个后续动作分别对应 store 里的一个 action逻辑一目了然测试也好写。5. 给代码做体检一套可以复用的自查清单与工具配合最后这部分分享一套我用下来的“代码体检”流程。它不是那种玄学式“你觉得代码好不好”而是有具体检查项、能落到代码里的流程。每季度做一次项目健康度肉眼可见。5.1 检查清单照着打钩一个文件一个文件过我一般会在一个不忙的周五下午挑几个核心业务模块做体检。检查分五层每一层都有明确的量化标准第一层组件层[ ] 组件是否超过 400 行如果是拆分职责。[ ] 组件模板里是否有复杂 v-if/v-for 嵌套超过三层如果是考虑抽子组件或计算属性。[ ] 组件内是否直接调用超过三个不同的 api 接口如果是考虑把数据流抽成 composable。[ ] 组件是否依赖父组件传递的“一个组件用不到的 props”如果是砍掉。第二层状态层[ ] Pinia/Vuex 中是否存在只在单个组件中使用、却挂在全局的 state如果是挪到组件局部状态。[ ] 是否存在两个 store 相互引用的循环依赖如果是需要重构。[ ] 是否存在通过watch修改另一个组件状态的行为如果有基本可以判定状态流设计出了问题。第三层路由层[ ] 路由组件的script setup里除了“读参数、组合模块、管理页面状态”是否还有别的逻辑如果是把业务逻辑往下移。[ ] 路由表中是否存在永远用不到的死路由/重定向删掉。第四层样式层[ ] 全局样式文件是否有超过 500 行的如果是看看其中有没有能放进 scoped 的。[ ] 组件里是否用到了没有 scoped 的style如果是加上 scoped 或者搬进 CSS Modules。[ ] 是否存在完全没被使用的类名用工具扫一遍删干净。第五层依赖与工程层[ ] 是否安装了重复功能的依赖库比如两个日期处理库、两个请求库留一个就好。[ ] package.json 里有没有“装了就再也没用过”的包定期清掉。[ ] 项目根目录是否有混乱的、职责不明的utils目录整理成按业务域划分的目录结构。这套清单并不复杂但它逼着你把以前“感觉不对”的地方量化成行动项。有一次我按清单体检完项目删掉 800 行冗余样式、拆出一个 600 行的大组件顺手更新了三个过时依赖效果非常明显。5.2 让工具帮你盯着比人自觉靠谱人总会懒工具不会。我强烈建议你的项目至少配齐这几样ESLint Prettier不只是格式化。配上 Vue 官方推荐的eslint-plugin-vue规则像vue/no-unused-components、vue/no-parsing-error这种错误在 CI 阶段就能拦下来。Vue Devtools检查组件层级、Pinia 状态变化、路由切换时间线。如果你发现某个组件在 devtools 里看 props 特别多、非响应式数据特别杂那基本就是重构信号。复杂度检查工具比如eslint-plugin-complexity一个函数圈复杂度超过 10 就报 warning。这能逼着你把过长的函数拆开而不是靠意志力硬撑着不写长函数。代码量趋势工具定期统计每个模块的行数。如果某个目录的行数用肉眼可见的速度增长说明结构在失控需要人工介入。这些工具不能替代思考但它们能成为“低门槛的守门人”。我见过很多团队装了 ESLint 只是为了格式化把最有价值的规则全关了。这很可惜。你要理解工具存在的意义不是找茬而是让每一行代码都默认对齐团队的约定。写在最后每次写新代码前先想一个问题如果你问我这三年多踩坑之后最大的改变是什么我不会说是“学会了重构”或“懂得了设计模式”而是每次写新代码前我会先假设三个月后会有个新人或者我自己来接手这个文件然后问他三个问题这段代码的职责是什么它依赖了哪些东西需要从哪开始改如果这三句话我自己都说不利索那个代码可能还没到写的时候。代码烂不是道德问题是技术债问题。好代码也不是一下子写出来的是一次次修改、一次次重构、一次次删掉“当时觉得巧妙后来觉得愚蠢”的代码换来的。它需要刻意练习需要项目里有人定期做“体检”需要对坏味道保持零容忍。希望这篇复盘能帮你找到从“写了三年还是一团糟”到“终于敢把代码拿给别人评审”的方向。不需要一次做完所有改造但可以从把一个文件的职责边界画清楚开始。
返回列表