尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从面条代码到模块化开发:前端工程化重构的实战之路

发布时间:2026/9/19 19:46:16

资讯中心
01
ARTICLE

从面条代码到模块化开发:前端工程化重构的实战之路

从面条代码到模块化开发:前端工程化重构的实战之路
周末接到一个老同事的电话说线上有个活动页面出了故障。我打开项目仓库一看一个 order.js 文件三千多行十几个全局变量函数之间互相调用跟意大利面一样纠缠成一团。改一处要顺着调用链摸半天最后改完还要在心里默念千万别把别的地方带崩。这种代码前端圈里有个非常形象的名字——面条代码。你手上如果也有这样的项目看这篇文章就对了。这篇文章想聊的就是前端模块化开发这件事面条代码到底是怎么产生的模块化开发的核心思路是什么以及我实际参与过的几个老项目是怎么一步一步从一团乱麻改造成清晰可维护的结构化代码的。内容不涉及特别高深的理论更多是我踩过的坑和摸索出来的实操经验适合正在维护老项目的前端开发者也适合刚入行想建立代码结构意识的新人。1. 面条代码为什么会存在先认清问题再谈改造1.1 面条代码的四大典型特征说起面条代码我脑海里会自动浮现几个画面。这里我总结成四个特征你对照自己手里的项目看看是不是这样。第一个特征是一个文件从头写到尾。页面所有逻辑全部塞在几个文件里一个 js 文件动辄几千行HTML 里可能还埋着几百行内联脚本。这种文件什么都有初始化、渲染、事件绑定、请求接口、处理数据甚至字符串拼接 DOM 都堆在里面谁进去改谁头疼。第二个特征是全局变量满天飞。为了在函数之间共享数据大家习惯把变量挂在全局作用域或者往 window 对象上挂属性。demo.js 里定义let userInfo nullorder.js 里也定义let userInfo null两个文件引入顺序不对就直接互相覆盖排查起来像是在玩推理游戏。第三个特征是函数之间隐式依赖严重。A 函数内部悄悄调用了 B 函数而 B 函数又依赖 C 模块修改的一个全局状态。看代码时你根本不知道这个函数还牵扯了多少“看不见的手”改一个参数可能要连带改五六个地方。第四个特征是复制粘贴泛滥。某个功能在 projectA 里实现了第二天换了个页面需求差不多直接把代码复制过去改改变量名就上线。时间一长同样的逻辑散落在各个角落修 bug 的时候要找到所有副本漏一个就出问题。1.2 为什么会变成这样成因其实不只是“懒”很多人觉得面条代码就是开发者懒、水平不行其实不完全是这么回事。我见过很多面条代码都是在极端条件下长出来的。业务方三天一小需求五天一大需求排期永远不够先上线再说成了默认选择。代码能不写注释就不写后续维护的人看着一团乱麻也无从下手。再加上不少前端项目起点很低最初就是一个 HTML 文件带两个 js 文件没想到后来会膨胀成一个大应用。还有一个很重要的原因前端这门技术本身就经历了一段野蛮生长的时期。早期大家写页面就是往 HTML 里塞 script 标签全局函数一把梭后来工程化工具普及了语法和工具升级了但很多人的编码思维还停留在“能跑就行”的阶段。说白了面条代码是历史债务、业务压力和编码意识三者共同作用的结果。认清这一点再去谈模块化改造就不会简单粗暴地认为“重写一遍就好了”而是会去思考怎么用合理的约束和结构让代码在持续迭代中维持健康状态。2. 模块化开发到底在解决什么问题核心是边界和依赖2.1 模块化的本质可以用三个词概括边界、职责、依赖很多初学者以为模块化就是把代码拆成多个文件文件拆得越碎越“模块化”。这是个大误区。如果你只是把一个三千行的大文件拆成六个五百行的文件但文件之间依然互相修改对方的变量、相互调用没有章法那不过是把一大坨面条分成了几小坨本质上还是面条。我理解的模块化核心是三个词边界、职责、依赖。边界是指每个模块要有一个清晰的对外接口外部只知道它能干什么不需要关心它内部怎么实现。就像一台电视机你能看到的只有电源键、音量键和几个接口至于内部电路怎么走你不需要知道也不应该去动它。职责是指每个模块只应该负责一件相对独立的事。工具函数模块就只放工具函数请求模块就只负责发请求业务组件就只关心自己的渲染和交互。如果一个模块今天处理日期格式明天又跑去做数据缓存后天还顺手操纵了一把这个组件的状态这个模块的职责就乱套了。依赖是指模块与模块之间的关系必须显式、清晰、可控。一个模块如果要依赖另一个模块应该通过 import、require 这样的方式显式引进来而不是靠“运气”——比如正好另一个文件先执行了所以全局变量存在。依赖方向也要尽量单向上层可以依赖下层下层绝对不应该反过来依赖上层。2.2 从全局函数到 ES Modules前端模块化经历了哪几步聊到这里有必要把前端模块化的技术演进梳理一遍这样你能明白现在用的方案到底解决了哪些历史问题。最早也是最原始的做法就是 script 标签按顺序引入所有代码共享全局作用域。这种方式的缺点是显而易见的命名冲突、依赖关系不明确、代码执行顺序完全靠人肉保证。你永远不知道哪个全局变量被谁改了。接着出现了闭包和立即执行函数IIFE技巧。开发者用闭包把变量的作用域圈起来只把需要暴露的方法挂到全局的一个命名空间对象上。比如var MyModule (function() { var privateVar 1; return { get: function() { return privateVar; } }; })();这种方式解决了部分变量污染问题但模块之间的依赖关系还是要靠 script 标签的引入顺序来保证。再往后Node.js 带来了 CommonJS 规范用module.exports和require管理模块。这个方案在服务端非常好用因为模块文件都在本地同步读取没有压力。但浏览器环境不行浏览器里没法同步加载远端文件于是又涌现了 AMD如 RequireJS和 CMD如 SeaJS这些异步加载方案。不过这些方案现在已经不太主流了这里不展开细讲。真正一统天下的是 ES6 引入的 ES Modules 规范也就是import和export语法。它把模块化变成了语言层面的标准支持静态分析打包工具可以做 tree-shaking 把没用到的代码摇掉。现在你在 Vue、React 项目里天天见到的import xxx from xxx就是 ES Modules 的写法。配合 Vite、Webpack、Rollup 这些打包工具开发时模块随意拆分构建时再统一打包优化前端模块化终于走上了一条标准化、工程化的路。2.3 到底该用原生 ES Modules 还是上打包器判断依据很简单有的读者会问我现在写原生 HTML 页面没有用 Vue 或 React能用模块化吗答案是肯定的。现代浏览器基本都原生支持 ES Modules你可以在 script 标签上写typemodule然后在代码里正常使用 import 和 export。不过要注意一个问题原生 ES Modules 在浏览器里会触发多次 HTTP 请求模块文件多的时候性能会受影响而且浏览器要求所有模块必须通过 HTTP 协议加载直接双击打开本地 HTML 文件会报 CORS 错误。所以我的建议是这样。如果你的项目很小就是几个页面每个页面逻辑不多那直接用原生 ES Modules 足够开发时起个本地静态服务器就行。如果项目已经有一定规模开始涉及样式处理、图片资源、代码压缩、浏览器兼容那就直接上 Vite 或者 Webpack 这类构建工具。不要觉得构建工具学习成本高现在 Vite 的体验已经非常顺滑了几分钟就能初始化一个项目。模块化开发本来就离不开工程化工具的支撑二者是配套的。3. 实操改造从“意大利面”到“乐高积木”的完整步骤3.1 改造前必须做的事先画依赖关系图我见过最失败的重构方式就是拿到老代码直接开删开改改到一半发现一个全局变量被另外三个文件引用着只能慌忙还原最后分支都合不进去。所以动手之前第一件事永远是梳理现状把依赖关系摸清楚。我常用的方法是把每个文件的职责、暴露的全局变量、调用了哪些其他文件的函数全部抄在一张纸或者一个文档里。比如你发现 order.js 里定义了let userInfo然后 checkout.js 里也读了这个变量那它们之间就有隐式依赖。再比如 formatPrice 这个函数被五个文件用到那你就可以判断它是公共工具函数应该抽到一个独立模块里。画依赖关系图的目的是找出三个关键信息。第一是哪些代码是公共的应该被下沉到基础层。第二是哪些模块之间的依赖是“双向”的这意味着耦合过紧需要想办法把公共部分抽出来或者调整依赖方向。第三是哪些代码其实根本没人调用是僵尸代码趁改造直接删掉。这一步看起来很费时间但请相信我它省下的时间远大于你花的时间。没有这张图后面每拆一个模块都是在赌运气。3.2 按职责分层拆模块推荐一套可以直接套用的目录结构梳理完现状之后就要开始拆模块了。我个人的建议是按职责分层去拆而不是按页面去拆。这两种思路听着差别不大做起来效果完全不同。按页面拆容易出现一个问题两个页面用到了同一个接口和同一套数据处理的逻辑你拆的时候各拆各的公共代码又被复制了一份。我这里给出一套自己在中小型项目中反复使用过的目录结构你可以直接抄作业src/ api/ // 接口请求层 user.js cart.js components/ // 通用UI组件 button/ modal/ utils/ // 工具函数 format.js storage.js store/ // 全局状态可选 index.js views/ // 页面级组件 cart/ index.js item.js main.js关键原则是utils 和 api 是基础层任何模块都可以依赖它们components 是通用组件库可以被不同页面复用views 是页面层负责把各种模块组合起来。依赖方向是自上而下的views 依赖 componentscomponents 不允许反过来依赖 views。这样的分层结构最直观的好处是你找代码有明确的方向感。格式化相关的问题去 utils/format.js接口改动去 api/ 目录购物车 UI 样式问题去 components/cart 里找。新人接入项目半天就能摸清代码脉络。3.3 模块之间怎么通信先定规则再动手模块拆开之后最棘手的问题就是A 模块的数据变化了B 模块怎么知道这里需要提前定好通信规则否则拆到一半会发现模块之间直接 import 来 import 去边界又糊了。我的通信规则优先级是这样的。第一优先是父子之间通过属性传值和事件回调。React 里就是 props 和回调函数Vue 里就是 props 和 $emit。这种方式最直白、最可控也是我首选的方案。第二优先是组件内部状态提升到父组件由父组件做数据协调。如果多个兄弟组件要共享同一份数据就找到它们共同的父级把数据放在父级。第三优先才是引入全局状态管理工具比如 Pinia、Redux、Zustand。很多人一上来就上状态管理库其实很多场景根本不需要滥用全局状态反而会让数据流变得不可追踪。另外模块之间如果需要通信但又不是严格的父子关系可以用一个轻量级的事件总线EventBus或者自定义事件。但要注意事件总线用多了代码会变成“到处都是发消息的但不知道谁会接收”可维护性同样灾难。所以我的建议是事件通信能不用就不用如果非用不可一定要把事件名集中定义在一个文件里方便查证。3.4 实战演示把一团乱麻的购物车代码拆成四个模块理论说了这么多不如来一个真实的拆解演示。假设你现在拿到一个购物车页面里面是类似下面这样的“面条”代码。这段代码混了数据读取、计算、字符串拼接 DOM、事件绑定、本地存储操作全部耦合在一起。改造前的样子大概是这样的// cart.js 一个典型的“面条文件” let cart []; let totalPrice 0; init(); function init() { cart JSON.parse(localStorage.getItem(cart) || []); renderCart(); bindEvents(); } function renderCart() { const container document.getElementById(cart-list); let html ; cart.forEach(function(item) { totalPrice item.price * item.count; html div classcart-item span item.name /span span¥ (item.price * item.count).toFixed(2) /span button>// src/services/cartService.js const CART_KEY cart; export function getCart() { return JSON.parse(localStorage.getItem(CART_KEY) || []); } export function saveCart(cart) { localStorage.setItem(CART_KEY, JSON.stringify(cart)); } export function updateItemCount(cart, id, delta) { return cart.map(function(item) { if (item.id id) { return { ...item, count: Math.max(0, item.count delta) }; } return item; }); } export function calcTotalPrice(cart) { return cart.reduce(function(sum, item) { return sum item.price * item.count; }, 0); }第二步把金额格式化这种和业务无关的逻辑抽到 util 模块。// src/utils/format.js export function formatPrice(price) { return ¥ price.toFixed(2); }第三步把购物车项做成一个独立的 UI 模块只接收数据并渲染不关心数据从哪来。// src/components/CartItem.js import { formatPrice } from ../utils/format.js; export function createCartItem(item, handlers) { const div document.createElement(div); div.className cart-item; div.innerHTML span${item.name}/span span${formatPrice(item.price * item.count)}/span button classbtn-increase/button button classbtn-decrease-/button ; div.querySelector(.btn-increase).addEventListener(click, function() { handlers.onIncrease(item.id); }); div.querySelector(.btn-decrease).addEventListener(click, function() { handlers.onDecrease(item.id); }); return div; }第四步在页面入口处把这些模块组合起来。页面文件只负责拿到数据、调用模块、渲染结果它不知道数据是怎么存储的也不知道价格是怎么格式化的。// src/views/cart/index.js import { getCart, saveCart, updateItemCount, calcTotalPrice } from ../../services/cartService.js; import { formatPrice } from ../../utils/format.js; import { createCartItem } from ../../components/CartItem.js; const container document.getElementById(cart-list); const totalPriceEl document.getElementById(total-price); let cart getCart(); function render() { container.innerHTML ; cart.forEach(function(item) { const el createCartItem(item, { onIncrease: function(id) { cart updateItemCount(cart, id, 1); saveCart(cart); render(); }, onDecrease: function(id) { cart updateItemCount(cart, id, -1); saveCart(cart); render(); } }); container.appendChild(el); }); totalPriceEl.innerText formatPrice(calcTotalPrice(cart)); } render();拆完之后你会发现每一个函数都很短每一份职责都有明确的归属。以后改存储方式只动 cartService改价格展示格式只动 format.js改购物车项的 UI 结构只动 CartItem 模块。各个模块之间通过显式的参数传递和回调函数通信不再共享任何全局状态。这才是模块化要的最终效果单个模块坏了你只需要修一个文件其他文件一概不受影响。4. 模块化改造路上躲不开的坑我替你踩过了4.1 循环依赖最常见的模块化事故循环依赖就是 A 模块引用了 BB 模块又引用了 A。这在小的文件拆分时非常容易出现尤其是拆分业务模块时把共享的状态放到了 A 模块B 模块要读这个状态于是 B import A而 A 又依赖 B 的一个方法循环依赖就产生了。循环依赖最典型的表现有两种。一种是运行时变量为 undefined因为 ES Modules 在编译阶段就把模块之间的关系确定了但执行顺序上总有一个先来后到某个模块在初始化时读取了另一个还没初始化好的模块变量就会拿到 undefined。另一种是直接报错Cannot access xxx before initialization。处理循环依赖我的思路有三个层次。第一层是审视设计很多循环依赖的产生是因为模块边界没划好公共部分应该抽到第三个模块里让 A 和 B 都去依赖这个公共模块依赖关系立刻变成单向。第二层是在模块内部延迟引用把import写进函数体内部而不是文件中这样只有在函数被调用时才会真正执行引用避免初始化时的死锁。第三层是实在绕不开时可以考虑把共享状态提升到状态管理库中用全局状态替代模块间的互相引用。每次写完代码我都会习惯性看一眼模块之间的依赖示意图发现有圈子就立刻拆掉。这个习惯帮我避免了大量循环依赖事故。4.2 模块粒度的把握拆得太碎和拆得太粗都是坑模块粒度是我见过最多人纠结的问题。有的团队把每个工具函数都单独放到一个文件里一个 utils 目录下面挂了七八十个文件找起来眼都花有的团队则反向操作一个 components/common.js 里塞了几十个“通用”组件本质上又成了一个大杂烩。我自己的判断标准很简单一个模块是否值得单独存在看它有没有“两个以上”的引用方或者它是否承载了一段足够复杂、值得独立测试的业务逻辑。一个只被用了一次的函数先别急着抽成公共模块留在使用它的模块里就够了。等第二次需要用到时再抽出来也不迟。这就是所谓的“三次法则”或者说“两次法则”重复出现第二次可以先眼熟出现第三次才值得去抽。拆模块就像整理房间太乱了要分类收纳但收纳过度反而会让日常取用变得麻烦。适度是关键。4.3 依赖方向失控底层模块反向依赖了上层模块这个问题在项目规模变大之后会逐渐浮出水面。你精心设计了分层结构utils 在最底层业务组件在上层。但某一天业务方要求在某一个通用按钮组件里加一个埋点上报的逻辑而埋点上报依赖一个业务层的全局配置模块。开发同学图省事直接在按钮组件里 import 了业务配置模块依赖方向瞬间就反了。短期看代码能跑长期看这个通用按钮组件再也没法复用到其他项目了而且一旦业务配置模块变化所有用到这个按钮的地方都可能跟着出问题。更可怕的是这种方式会形成“破窗效应”一个人这么写了后来的人都会觉得这样写没问题分层架构很快就被击穿。解决这个问题我的办法是两条。第一在代码评审时对跨层依赖保持敏感看到 utils 或 components 里的文件 import 了业务模块一定要追问一句这个依赖真的必要吗第二必要时引入 ESLint 规则限制各目录之间的依赖方向技术手段比口头约定可靠得多。4.4 全局状态不是万能的别把它当听话匣子模块化改造过程中很多人遇到“多模块要共享数据”的情况第一反应就是引入 Pinia 或者 Redux把所有共享数据都放进全局状态里。这种做法的副作用是数据流变得不可预测。任何模块都可以修改全局状态你不知道是谁改了这个值、什么时候改的排查 bug 难度反而直线上升。我自己总结的经验是全局状态只放三类东西第一类是用户会话信息比如登录态、用户资料第二类是跨模块且需要响应式更新的全局 UI 状态比如主题、语言、侧边栏开关第三类是多个非兄弟组件都会读取的缓存数据比如字典数据。除此之外一切局部数据都尽量放到组件内部或就近的模块里。能局部就不要全局这是我一直坚持的原则。5. 模块化之后你的代码会发生什么肉眼可见的变化5.1 新人上手和任务拆分的效率明显提升代码结构化之后最直观的变化就是新人上手速度变快了。以前一个新同事入职看代码库可能要两周才敢动代码因为不知道改一个全局函数会不会踩到别人埋的雷。模块化改造之后新同学进来我先让他看目录结构再挑一个相对独立的页面模块去改。他只需要关注这一个模块的职责和它依赖的几个基础模块上手的路径短了很多。任务拆分也变得更清晰。以前一个需求可能对应着去改那一个三千行的大文件几个人没法并行只能排队。现在模块拆开了接口层一个人改UI 层一个人改状态管理一个人改大家各改各的模块冲突概率也大大降低。5.2 测试终于可以动手写了面条代码之所以难测试是因为一个函数依赖了太多上下文。比如你要测试那个 renderCart它要操作 DOM、访问 localStorage、还依赖外部定义的全局变量你根本没法把这些依赖隔离出来。模块化之后每个模块的输入输出都是清晰的。测试 cartService 的时候我传一个购物车数组进去断言输出结果是否正确就行。测试 formatPrice 就更简单了传入数字断言字符串是否带上了货币符号。不需要 mock DOM不需要模拟全局变量纯函数测试无比清爽。这也是模块化带来的一个隐藏收益it makes your code testable。5.3 重构和迁移时胆子变大了模块化改造完成之前公司说要做技术栈升级从老项目迁移到新框架我是不敢接的。因为代码耦合太严重根本理不清哪些逻辑可以平移、哪些逻辑要重写。改造完成之后分层清晰依赖明确迁移就变成了一件可以按模块逐个推进的事。先把 api 层挪过去再把 utils 层挪过去最后按页面逐步换壳。每一步都有明确的边界和验证标准出了事能立刻定位到是哪一块。这种“心里有底”的感觉是模块化带给我最大的安全感。代码不再是一团需要靠意志力去维护的混合物而是一套可以独立替换、升级、复用的组件系统。最后再分享一个我个人的实操心得。模块化改造最忌讳的是“大爆炸式重写”。你花两周时间偷偷把整个系统的代码全部重写了一遍最后合入的时候冲突几百个测试全挂业务方还催着上线心态直接崩掉。我现在的习惯是“走一步看一步”不从存量代码下手而是先用模块化的思维约束增量代码每次新增功能时按模块拆分来写。存量代码在每次修改相关功能时顺手局部重构像一个修理工一样今天修一个窗口明天补一个墙角三个月下来整个房子已经焕然一新。这种渐进式的改造风险小、见效快也更容易让团队成员接受。前端模块化不是一个一蹴而就的工程它更像一种持续演进的编码习惯你坚持一个月就会发现自己写的代码已经很难再看回那个意大利面堆起来的样子了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。