前端架构演进:从模块化到状态管理的工程化实践指南
发布时间:2026/10/20 17:07:08 锦皓数字建站

1. 前端架构演进从刀耕火种到现代工程化到底走过了哪几步先说一个我自己的判断前端架构这个词十年前听起来像在给切页面的工作贴金但今天它直接决定了一个产品能走多远、一个团队能塞进多少人、一次重构需要流多少血。我经历过从jQuery插件堆叠到微前端拆分的过程也见过因为架构选型失误一个看似简单的功能改动需要协调三个团队改四天的情况。这篇文章不聊虚的就用一条时间线把架构演进的逻辑拆清楚顺便把模块化设计在每个阶段里扮演的角色一并说透。1.1 刀耕火种时代为什么最初的Web页面根本谈不上架构最早的Web页面本质上就是一堆HTML文件配上几段JavaScript脚本。那时候没有构建工具没有模块系统更没有所谓的前端工程化。页面之间靠超链接串联JavaScript在全局作用域里互相可见大家约定俗成地用命名空间来避免变量冲突最常见的做法是往window上挂一个全局对象window.App window.App || {}; App.user { name: 张三, role: admin }; App.order { list: [], total: 0 };这个阶段的问题做过的都懂。脚本的加载顺序是生命线一个依赖前置脚本的文件如果被提前引入整个页面直接白屏。代码一旦超过几千行几乎无法定位变量是谁在什么时候改动的。我接手过一个老项目页面里13个script标签全站共用一个全局数组存储购物车数据后来为了加一个促销活动不得不把其中一个函数改成同步执行才能保证数据读取正确。那次的修复只动了三行代码但排查用了整整一天。这种模式其实也有它的时代合理性。页面以服务端渲染为主JavaScript只承担轻量交互表单校验、菜单展开、轮播图切换。当业务复杂度低的时候全局命名空间的冲突概率是可以靠开发纪律压住的。但到了2008年前后Ajax开始普及页面的动态交互成倍增长单页的代码量从几百行飙升到几千行上万行全局命名空间的方案彻底撑不住了。1.2 MVC与MVVM前后端协作模式倒逼出的第一次架构重构前端真正意义上的第一次架构演进是从MVC思想引入开始的。当时的背景是后端已经有了成熟的MVC分层而前端还是一盘散沙。最早的前端MVC框架把页面拆成Model数据、View视图、Controller控制器三部分试图让代码按照职责边界组织起来。但前端MVC有个很尴尬的问题DOM操作是命令式的数据和视图之间的同步需要手动维护。用户在输入框里敲一个字你要监听input事件手动更新Model重新渲染View再绑一遍事件。这套流程在复杂交互下冗长且容易出错。MVVM的出现解决了这个问题它的核心是数据绑定视图的状态自动跟随数据变化而变化开发者不再需要手动操作DOM。我最早接触MVVM是在一个后台管理系统里那个项目用了几百个表单字段之前用jQuery做联动校验的时候代码逻辑绕来绕去像一团意大利面。换成MVVM之后视图模型把数据状态集中管理表单联动变成了对响应式数据的自动计算整个改动量下降了大概四成。更重要的是团队协作方式变了开发者和UI之间终于有了清晰的接口约定不再是用PSD文件来回沟通像素级细节而是明确这个组件接收什么数据、输出什么行为。MVVM的代价也很明显。响应式依赖的数据监听机制意味着框架本身要吃掉一部分性能模板里的表达式逻辑一旦写复杂难以调试。它的普及价值在于让前端开发者第一次认真思考一个问题代码的职责边界到底该怎么划这一点为后续的组件化思想铺了路。1.3 SPA与工程化单页应用如何倒逼架构彻底升级真正把前端架构推到现代形态的是单页应用的普及。当整个应用不再做页面跳转而是通过JavaScript动态渲染内容时前端不再是页面脚本而是一个有完整生命周期、路由、状态管理的应用层。于是前端架构的问题域被彻底打开了路由怎么设计、状态怎么管理、代码怎么拆分、模块怎么按需加载。SPA带来的第一个冲击是代码体积爆炸。所有页面逻辑都打包在同一个运行环境里如果不做拆分首屏加载几个MB的JavaScript是常事。我做过一个数据可视化项目初期把所有图表组件全部打进一个bundle首屏加载时间达到了8秒Lighthouse跑分惨不忍睹。后来引入路由级代码分割每个页面按需加载自己的图表模块首屏时间直接降到了2秒以内。这个优化本质上就是架构层面的调整而不是某个函数写得快慢的问题。SPA带来的第二个冲击是数据流变复杂。多个页面共享同一份用户状态、购物车状态跨页面通信从URL参数全局变量升级成了需要一套全局状态管理机制。Redux这类的单向数据流模型脱颖而出它的核心约束是所有数据变更必须通过显式的action触发经由reducer计算新的状态。这套约束看起来很绕但它在业务复杂度上去之后最大的价值是可以回溯任何一个状态变更都能找到是谁触发的、为什么触发。2. 模块化设计的演进路线从全局函数到ES Modules我到底该怎么选模块化这条线跟前端架构演进是并行且深度交织的。理解模块化的历史不只是考古学兴趣它直接决定了你今天写代码时怎么组织依赖、怎么拆分文件、怎么处理循环引用。2.1 命名空间时代与IIFE大家在用约定规避问题在标准模块系统出现之前前端社区靠的是两样东西命名空间约定和立即执行函数表达式IIFE。命名空间解决的是变量污染问题IIFE解决的是变量私有化问题。典型的写法是这样的(function(global) { var privateVar secret; function internalHelper() { return privateVar.toUpperCase(); } global.MyModule { getValue: internalHelper }; })(window);这种模式的好处是不依赖任何工具浏览器直接就能跑。坏处是依赖关系完全靠加载顺序保证A模块依赖B模块你就必须在引入A之前先引入B。一旦模块数量超过几十个script标签的顺序就成了一个隐形炸弹。我记得有一回线上环境出了一个偶发bug某个用户在某些页面下进入功能模块时控制台报Cannot read property xxx of undefined。排查了大半天发现是两个模块之间有一个隐藏的依赖关系但在某个入口页面里依赖模块的加载顺序被另一次需求调整给打乱了。2.2 CommonJS与AMD服务端思维与浏览器现实的碰撞CommonJS来自服务端JavaScript生态它的设计初衷是为了Node.js的模块化开发。require同步加载模块、module.exports导出接口简洁直接。问题在于浏览器环境下同步加载意味着阻塞渲染这在网络延迟面前是致命的。于是AMD异步模块定义被提了出来也就是requirejs那套方案用define和require把模块定义和异步加载结合起来。AMD解决了浏览器的异步加载问题但它带来了一个开发者体验上的痛点代码嵌套层级深、写法繁琐每个模块都要包一层define。我当时在一个大型项目中用requirejs管理了几百个模块配置文件长得吓人而且因为路径解析规则灵活经常出现找不到模块的报错。后来CommonJS思想借助打包工具的兴起webpack的CommonJS兼容模式逆袭了浏览器端开发者用CommonJS写代码打包工具把模块依赖图构建出来最终产出一个或多个静态bundle。这种方式兼顾了开发体验和运行时性能让模块化真正在工程层面代替了运行时层面。2.3 ES Modules标准化浏览器原生的模块系统最终形态ES ModulesESM是ECMAScript标准层面的模块方案它的核心优势有三点静态结构化、天然支持异步加载、原生运行在浏览器中。静态结构化意味着import和export在代码解析阶段就能确定依赖关系这让打包器可以做tree shaking我做过一个项目引入ESM标准写法之后打包体积整整小了18%因为很多从未被调用的导出函数被自动删除了。ESM的原生浏览器支持还带来了一个新的架构思路利用浏览器原生模块加载配合HTTP/2多路复用直接在生产环境用裸ESM省掉打包这一步。这种方式常被称作Bundleless但真正落地时有个现实问题模块请求数量可能达到数百上千个开发环境没问题生产环境对服务器并发能力和HTTP缓存策略要求很高。所以现在的主流实践是开发期用Vite的ESM方案享受秒级热更新生产期仍然经过Rollup/Rspack精细打包。ES Modules的意义不只是语法层面的标准化它把模块化的底层逻辑从约定和工具变成了语言标准整个前端生态的协作成本因此降了一个量级。3. 组件化架构落地从组件分层到可维护的设计系统这里全是实操模块化解决了代码怎么拆组件化解决了界面怎么拆。组件化架构设计得好不好直接决定了新功能迭代时是加一个配置项还是复制粘贴再改改。3.1 组件分层的三个层次基础组件、业务组件、页面组件我见过无数团队在组件化初期走弯路根源在于没有清晰的层级边界。我的建议是把组件分成三个层级基础组件不包含任何业务语义纯粹是UI原子。按钮、输入框、弹窗、下拉菜单、日期选择器。它们只接收通用props输出标准化交互。业务组件绑定特定业务领域的组件比如订单卡片、用户头像带等级标识、商品价格展示。它们可以组合多个基础组件并包含一定的业务逻辑。页面组件对应路由级别负责获取数据、组合业务组件、布局排版。在这套分层里最核心的规则是依赖方向必须是单向的页面组件可以依赖业务组件和基础组件业务组件只能依赖基础组件基础组件不依赖任何业务代码。我在某中后台项目里强制执行这个规则之后最明显的变化是当产品要求全局替换按钮样式时只需要改基础组件的按钮一处所有业务组件和页面组件自动生效不用担心某个业务组件里塞了一套自己写的按钮样式。判断一个组件该放哪一层我的经验是看它的复用半径如果项目内多个页面都会用且只涉及通用UI能力放基础组件层如果只有某个业务域内复用且包含业务语义放业务组件层如果只在某个页面内使用可以先放在页面组件内部的子组件里等出现第二个使用方时再提升到业务组件层。很多团队把组件过早地抽到公共库里结果公共库演变成垃圾桶谁的东西都往里丢层级边界直接糊掉。3.2 原子设计方法论在真实业务中的调整空间原子设计Atomic Design把界面拆成原子、分子、组织、模板、页面五个层次。这套方法论在to B中后台系统里落地效果很好但在to C运营活动页里生搬硬套往往会发现化学键根本连不上。我的做法是保留它的内核——从最小元素开始自底向上组合——但把层次简化为三个视觉原子、区块组件、页面模块。视觉原子对应颜色、字体、间距、阴影这些设计变量它们在代码里的落地形式是design tokens设计令牌而不是组件。例如主干色、警示色、圆角尺寸、栅格间距都可以定义成变量。设计令牌的好处是当品牌改版需要调整主色调时只需要改一处配置整个应用的颜色随之变化。我维护过一个使用了六年的大型系统经历了两次品牌换色都是通过修改tokens文件一次性完成的没有手写过一条覆盖样式的代码。区块组件则对应可组合的业务区块比如用户信息栏、数据统计卡片、筛选表单面板。这些区块可能在不同页面里出现配置参数和展示形态略有差异。在这个层面我强烈建议在组件props设计上遵循默认值优先原则所有非必填props都有合理的默认值新页面接入时零配置即可渲染按需覆盖再调整。这个原则初期会增加基础组件的搭建成本但后续维护时收益巨大。3.3 组件库与设计系统的灰色地带规范之外的那些事很多团队把组件库等同于设计系统这是一个常见的误解。组件库是代码层面的实现设计系统则包含设计价值观、使用原则、内容规范、无障碍标准等多个维度。组件库只解决有哪些组件可用设计系统解决什么时候该用哪个组件、怎么用才对。在实际项目中我见过最典型的灰色地带是组件库里存在三个长得差不多的表单输入框分别由三个不同时期的成员维护。新成员根本不知道该用哪个最后的结果就是全公司每个页面都长得不一样。解决这个问题不能只靠文档喊话靠的是在组件库层面做唯一入口约束每个组件只有一个标准实现其他变体通过props控制不另开新组件。组件库发布前必须经过视觉走查确认和设计规范一致。使用方提交PR前强制跑一遍组件规范检查违反的自动构建失败。这套机制跑起来之后组件库的熵值增长速度明显放缓设计走查单从每月二十多张降到了零星几张。组件化架构到了这个程度才算真正闭环有层级、有规范、有约束、有反馈。4. 状态管理方案选型不同应用体量下的最佳实践我直接给结论状态管理是前端架构里争议最多的领域。Redux、MobX、Zustand、Pinia、Jotai方案层出不穷团队内经常吵得不可开交。我梳理的结论是状态管理方案没有绝对的最好只有和项目体量以及团队习惯的匹配度。方案核心思想适合场景需要注意的问题Redux单向数据流单一store纯函数reducer大型中后台、多人协作、复杂业务状态样板代码多Action/Reducer体系学习和维护成本偏高MobX响应式代理多store细粒度更新中小型项目、表单密集型应用过度自由导致状态变更路径难以追踪Zustand轻量hook式store无需Provider包裹React应用中的局部到中等全局状态大型应用时缺乏强制约束依赖约定PiniaVue 3官方推荐天然集成DevToolsVue生态项目基本无硬伤但生态相对Vue专属Jotai原子化状态模块化粒度极细高频更新场景、复杂派生状态原子粒度过小时心智负担较重4.1 从Flux到Redux单向数据流的约束到底解决了什么问题Flux是Facebook提出来的一套前端状态管理思想它针对的是当时MVC框架在复杂交互下数据流混乱的问题。核心改变是数据不再是双向绑定的自由流动而是强制走一条单向链路——View发出ActionDispatcher分发ActionStore响应Action并更新状态View订阅Store变化重新渲染。Redux把Flux进一步简化用一个全局store替代了多store用纯函数reducer保证状态变更可预测。这套模型真正的价值在于可追踪性因为数据流是单向的任何状态变化都可以通过action日志完整重放。我在一个复杂的权限配置系统里用过Redux用户操作路径极长跨多个页面的状态联动非常多。线上反馈配置保存后刷新丢失我们就通过Redux DevTools回放了用户从打开页面到点击保存的完整操作序列一帧一帧看状态变化最后定位到是一个异步回调在保存成功后覆盖了本地提交数据。这种排查效率在双向绑定的架构下是不可能达到的。Redux被人诟病的样板代码问题也真实存在。我的建议是不要裸用Redux搭配官方推荐的RTK以后action和reducer的模板代码大幅缩减Slice模式把每个业务域的state、action、reducer定义在同一个文件里维护起来比传统Redux顺手太多。4.2 服务端状态与客户端状态必须分开管理一个非常容易犯的错误是把服务端数据和UI状态混在同一个store里。服务端状态指的是从接口拉来的数据比如用户列表、订单详情客户端状态指的是用户当前选中了哪一行、弹窗开了没有、表单填到哪一步。如果混在一起会出现一类经典问题两个页面缓存了同一份服务端数据但其中一个页面修改了数据之后没有同步通知另一个页面结果界面显示的是旧数据。解决思路是分离管理服务端状态交给专门的数据请求库比如React生态里的React Query、Vue生态里的Vue Query它们负责缓存、失效、重试、后台重新拉取客户端状态才放到全局store或组件状态里。我接手过一个系统原本所有接口数据都放在全局store里每次接口返回都要手写reducer更新逻辑。页面一多缓存同步的代码越堆越密还常出现A页面改了数据B页面还显示缓存的问题。迁移到数据请求库之后所有服务端数据都有统一的缓存key和失效策略A页面的数据变更后只需要invalidate对应的keyB页面自动感知并重新拉取开发心智负担降了一大截。这是架构演进里性价比最高的一次改造。4.3 现代状态管理方案的对比别再纠结了按这个思路选如果你现在在选型阶段我的建议流程是先判断项目形态。如果是纯展示型网站几乎没有交互状态那根本不需要全局状态管理组件内部状态就够了。如果有跨页面共享的交互状态比如多步骤表单、全局购物车、用户偏好设置再引入状态管理库。团队偏好函数式还是响应式决定了选Redux系还是MobX系。关键看团队对约束和自由度的偏好。如果主要用React且不想折腾直接选Zustand它没有Provider包裹的负担也没有Action/Reducer的心智负担。如果是Vue 3直接用Pinia官方加上TypeScript支持开发体验很丝滑。我现在的个人偏好是服务端状态用数据请求库客户端状态用轻量的store方案完全不搞重型全局store。这套组合在多个项目里实践下来状态管理部分的bug率显著下降新成员上手速度也快了很多。5. 工程化架构与性能优化的联动构建工具、按需加载与性能预算前端架构落到最实际的层面就是构建产物长什么样、首屏加载快不快、更新一个功能要等多久。这些都属于工程化架构的范畴。我见过不少团队花大量时间抠组件代码性能却忽略了构建配置层面的架构性问题结果收效甚微。5.1 构建工具选型背后的架构考量Webpack曾是事实标准它的优势是生态丰富、插件体系成熟但开发服务器的冷启动速度和热更新效率在大项目里会比较吃力。Vite的出现改变了这个局面它基于浏览器原生ESM支持将开发环境的模块加载变成按需编译启动速度能做到亚秒级。我换到Vite之后最直接的体感是之前Webpack冷启动一个大型中后台项目要40多秒换成Vite只需要3秒左右热更新从秒级降到了毫秒级。开发体验的提升是实打实的。生产构建方面Vite默认使用Rollup配合tree shaking和代码分割产物体积通常优于Webpack的默认配置。Rspack作为新起的Rust重实现打包器也在兼容Webpack生态的前提下实现了数倍的构建速度提升适合对Webpack配置依赖很深的存量项目做平滑迁移。选构建工具时我建议从三个维度评估开发体验启动和热更新速度、生产构建质量产物体积和分割逻辑、生态迁移成本现有插件和配置的兼容程度。5.2 按需加载与代码分割的实际操作代码分割是工程化架构里最实用的性能优化手段之一。具体做法是让打包器把代码切成多个chunk浏览器只在需要时加载对应的chunk。两种最常见的分割场景路由级分割每个路由页面一个chunk用户访问某个页面时才加载它的代码。组件级分割对体积大、非首屏渲染的组件使用动态导入例如弹窗组件、复杂图表组件、富文本编辑器。代码分割的配置并没有想象的复杂。在Vue Router里使用动态导入就是改一行路由配置const routes [ { path: /dashboard, component: () import(/views/Dashboard.vue) } ];但如果只做路由级分割往往还会有一个问题同一个业务组件被多个路由引用时打包器可能把公共依赖重复打包进多个chunk里导致总体积反而膨胀。这时候需要小心配置splitChunks把公共依赖提取出来。我实践过的策略是把体积大且稳定的第三方库单独拆成vendor chunk把多个路由共用的业务代码拆成shared chunk设一个合理的chunk大小阈值比如30KB低于阈值的模块不单独拆分避免请求碎片化。曾经有一个图表项目一开始把echarts全量引入路由级分割后每个图表页面的chunk都打了一遍echarts总体积直接膨胀。配置splitChunks把echarts提取出来后虽然首屏体积没减但页面切换时图表页的加载速度明显提升因为echarts已经被浏览器缓存过了。5.3 架构层面的性能预算如何量化并守住底线性能优化如果没有量化指标很容易变成每次发布前跑一跑LightHouse红了就改改绿了就上线的被动模式。更有效的做法是在架构层面设立性能预算Performance Budget用硬性指标卡住产物质量。一个可落地的性能预算方案包括三个维度体积预算首屏所需最小JavaScript体积比如核心JS chunk不超过170KBgzip后超出则构建失败。时间预算LCP最大内容绘制不超过2.5秒TBT总阻塞时间不超过200ms。依赖预算第三方依赖总量变化超过一定比例时CI阶段给出警告并需要人工确认。我在团队里落地过一个简单的体积预算脚本读取构建产物里的chunk大小超过阈值时让CI直接红掉。第一次执行时产品经理提了一个加一个富文本批量导入功能的需求开发顺手引入了一个10MB的xlsx解析库构建直接在CI上失败了。经过协调换成流式解析方案体积降为原来的1/12用户体验反而更好。这就是架构约束的价值它把性能问题从事后救火变成了事前拦截。6. 架构演进中的典型问题与排查老项目改造、循环依赖、状态不同步架构演进不是一条铺满鲜花的道路实操中你会遇到大量具体而棘手的问题。这一节我把印象最深的几个问题记录下来每个都附上排查思路和解决方式希望能帮你少走一些弯路。6.1 老项目改造怎么在不推倒重来的前提下完成模块化迁移老项目改造是前端架构里风险最高的事情之一。直接推倒重写风险极大业务逻辑复杂的老系统重写周期里需求还在不断叠加很容易陷入永远在重构、永远上不了线的泥潭。我的建议始终是用渐进式迁移把换架构变成加架构。第一步先梳理应用的模块边界。根据业务域把代码划分成相对独立的模块比如用户模块、订单模块、商品模块。第二步为每个模块建立显式的接口层。老代码不可能立刻改造成被import的ES Modules但可以用一个门面对象做适配。例如原来是全局函数window.submitOrder现在用一个模块导出包装一下// 新代码入口 import { submitOrder } from /legacy/order;/legacy/order内部引用旧文件旧文件保持不动。这样可以逐步把新代码切到新模块体系上而旧逻辑不需要一次性改完。第三步在通信层引入事件总线或全局store做新旧代码之间的状态同步。旧代码改变数据后发布一个事件新代码订阅事件更新UI反之亦然。这个中间层会带来一定的性能开销但在过渡期是值得的。我在一个金融后台系统上做过类似的迁移从jQueryrequirejs迁到Vue 3前后花了大概五个月采用的是新页面全量用新框架老页面逐步改造的策略。每周稳定迁移两三个页面老代码和新代码之间通过事件总线通信。五个月后老代码占比降到10%以下在最后一个月才彻底切掉requirejs。整个过程中的线上事故数是零这就是渐进式迁移的价值。6.2 循环依赖与构建异常的排查记录ES Modules虽然是从语言标准层面解决了模块化问题但它也带来一个常见的新坑循环依赖。A模块引用了B模块B模块又间接引用了A模块。在构建阶段这不一定报错但在运行时可能表现为A模块里获取到的某个导出值是undefined。我遇到过一次典型的循环依赖问题A模块定义了一个工具函数B模块在模块顶层调用了这个函数而A模块在顶层又调用了B模块的某个常量。由于ESM的导入提升机制B模块顶层代码执行时A模块的某些定义还没初始化读取到的值自然就是undefined。排查方法很直接在构建工具里开启循环依赖检测插件或者在报错处加console.log看模块初始化的顺序是否符合预期。解决的常规手段有两种一是把顶层调用改成延迟调用放到函数内部、事件回调里让它等到所有模块初始化完成之后再执行二是重新设计模块划分把公共依赖下沉到独立的底层模块避免上层之间互相依赖。我在代码评审时都会专门问一句这个模块被其他模块引用了吗它又引用了谁形成习惯之后循环依赖这类问题在评审阶段就能被提前发现。6.3 架构演进中调试与质量保障的实用方案架构演进期间代码流动性很大质量保障如果不跟上线上事故很难避免。我自己的实践经验是在架构调整期间把构建时的类型检查、代码规范检查和单元测试全部接入CI流水线。任何一次改动如果让已有的测试用例挂了不允许合并。这是底线不是锦上添花。另外建议在演进期间建立一份架构迁移看板记录每个模块的迁移状态哪些已经切到新架构、哪些还是老代码、哪些存在临时适配层。这份看板比任何文档都直观团队开周会时扫一眼就知道进度和风险在哪里。等到所有模块迁移完毕再去删那些临时适配层代码不要一边迁移一边留债到最后债多得还不过来。还有一个容易被忽视的细节老代码在迁移过程中可能会产生一些临时性的野代码比如曾为了兼容同时出现在window和模块导出里的同一个函数。这类野代码要在切完模块之后专门做一次清理否则它们会成为新一轮架构债务的种子。我吃过这个亏某次迁移完成后忘了清理一个挂在全局的临时函数结果半年后有人从全局环境里取到了它以为是系统内置能力直接写进新业务里等到全局函数被删除时线上炸了一片。所以迁移完成的标志不是新代码能跑了而是旧入口全断了。最后再分享一个小技巧做架构演进这件事心态上要有一点考古学家的感觉。很多历史遗留代码看起来奇怪但当初大概率是为了解决某个真实问题才那么写的。直接推翻它之前先搞清楚它到底在解决什么问题。我见过最惨烈的重构失败案例不是因为代码水平不行而是因为根本不知道旧代码的存在意义一铲子挖掉之后隐藏的业务规则跟着一起消失了然后花了一个月才从线上反馈里把规则一条条找回来。所以每动一块代码之前我建议先用git blame看一下它为什么存在再去决定是改造还是重写。这个习惯帮我在无数次架构调整里避开了大坑。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。