首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
前端架构演进与模块化设计:从全局变量到业务域拆分
📅 2026/10/21 19:12:14
✍️ 爱科研究院
👁 阅读 3,247
如果你在前端圈待过几年大概率会经历这样一种感觉代码从能跑就行变成跑着跑着就跑不动了。我最早维护一个后台管理系统时光是暴露在window上的全局变量就有几十个后来每次新功能上线最怕的不是逻辑写错而是不知道改了哪个文件会引爆另一个页面。那段经历让我意识到前端的问题往往不是写得不够好而是从一开始就没有把架构和模块化设计当回事。这篇内容我想用自己过去几年的实践把前端架构演进与模块化设计这条路从头捋一遍聊聊我是怎么从全局变量满天飞走到模块边界清晰、依赖方向可控的也会把踩过的坑和判断标准一起分享出来。无论你是刚接手一个中型前端项目还是正在折腾团队的基础设施这篇都值得参考。1. 一条不太远的路从全局函数到模块化落地1.1 早期前端能跑起来但全靠潜意识维护十多年前写前端基本就是几个script标签按顺序堆在 HTML 里。当时没有人跟你讲什么模块化设计更多是能用就行。但问题很快就来了你在a.js里定义一个init()b.js里也定义一个init()后加载的把你覆盖了。你依赖jQuery的插件但插件的加载顺序错了整个页面白屏。想复用一段逻辑最稳妥的方式就是复制粘贴因为根本找不准这段逻辑到底挂在哪个全局对象下。现在回头看这种模式的本质问题是所有文件共享同一个全局作用域文件之间没有显式的依赖关系。代码看起来能跑实际是靠加载顺序和命名默契硬撑起来的。一旦项目超过几百个文件这种默契就会崩溃。我当时就遇到过某天早上发现首页弹窗有问题排查了半天发现是一个公共工具函数被另一个新加的全局变量覆盖了。那时候我还没有架构概念只觉得前端这摊事太脆弱了。1.2 模块化标准之争CommonJS、AMD 与 ES Module必须有模块机制这件事前端行业很早就意识到了。但真正落地经历了一个漫长的标准纠缠期。先是 CommonJS它主要在 Node.js 环境里用require同步加载模块写法简单直接。问题在于浏览器环境没有module和exports这些对象也没法同步地去加载一个远程文件。于是社区又搞出了 AMD 和它的代表实现通过define和require把模块定义和异步加载结合起来适合浏览器。后来又出现了 UMD本质上是一段万能包装代码同时兼容 CommonJS 和 AMD但可读性惨不忍睹。这个阶段的混乱本质上不是谁比谁高级而是模块化机制和运行环境、加载策略强绑定。直到 ES ModuleESM成为标准import/export成为语言层面的能力浏览器原生开始支持前端模块化才有了真正的标准答案。ESM 是静态声明的打包器可以在编译期分析出完整的依赖图这让按需加载、Tree Shaking、作用域隔离都有了基础。它们之间的核心区别我用一张表总结过模块机制加载时机语法适用环境特点CommonJS同步、运行时require/module.exportsNode.js、早期打包器简单直接但浏览器不支持也不适合AMD异步、运行时define/require浏览器早期方案解决异步加载但配置繁琐UMD运行时全局包装函数跨环境兼容生态过渡产物可读性差ES Module静态、编译期import/export现代浏览器、Node.js依赖可分析支持 Tree Shaking1.3 模块化不只是一套语法更是一整套构建体系回头看很多人以为用了import就是模块化这是天大的误解。ESM 语法让你有了一张模块清单但没有一个构建系统浏览器里照样没办法高效地加载成千上万个文件。真正让模块化落地的是配套的打包器与构建链路。早期的打包器把 CommonJS 模块转成浏览器可执行的格式解决了模块怎么变成可运行代码。后来的打包器开始把模块切分和按需加载变成一等公民同一个应用的代码可以拆成初始包和若干异步块路由跳转时再加载对应代码。这背后的逻辑已经不是语法有没有而是模块化设计如何配合资源加载策略影响首屏性能、缓存命中率、迭代效率。我自己的体会是模块化的演进实际上是前端从页面脚本走向应用系统的必然结果。它不是某个工具或语法的功劳而是语言标准、运行环境、构建工具三层相互作用的结果。理解了这条链路后面做模块化设计时你思考的问题就会完全不一样——你不是在凑代码结构而是在设计一套所有模块如何协作、如何被加载、如何演进的底层秩序。2. 为什么很多团队卡在装了框架但没装上架构2.1 目录分层不等于架构设计我看了很多前后端分离的项目代码目录长得很标准components、pages、services、store、utils看起来井井有条。但只要一追代码引用关系就会发现components里的组件直接调用services里的接口又再绕去store取状态。pages里的业务逻辑散落在各处没有一个明确的归属。utils越滚越大最后成了一个什么都能往里塞的杂物间。这种项目的问题不是目录命名不好而是没有定义模块之间的依赖规则。目录只是告诉我们你大概放了什么东西完全没有约束谁可以依赖谁哪一层不能反向引用。架构的本质是规则不是文件夹的摆设。没有规则约束的目录时间越长越混乱最后只能靠开发者自觉而自觉这种东西在迭代压力面前通常撑不过三个月。2.2 组件化不等于模块化很多团队说我们已经模块化了实际做的是组件化——把页面拆成按钮、表单、卡片这些可复用的 UI 片段。组件化解决的是视图复用和交互封装但模块化解决的是业务能力、状态逻辑和资源归属的组织。两者相辅相成但绝对不是一回事。比如一个订单模块如果它只是被拆成了OrderList.vue、OrderDetail.vue几个组件那它仍然是散装的——订单的 API 请求写在services/order.js订单的状态放在全局store订单特有的类型定义放在types目录。这些文件因为业务上都和订单有关但物理上却散落在不同目录最后没人能说清楚一个订单功能到底包含哪些代码。真正的模块化应该是把订单模块作为一个整体来看待它内部的组件、状态、API、工具函数、类型定义都归属在一个清晰的边界内外部只能通过明确定义的入口去使用它。这样才能做到改订单时你不用关心登录模块实现团队协作上的解耦。2.3 一个典型失控现场依赖方向的反向污染我之前接手过一个项目规模不算大但每次改代码都是牵一发动全身。我梳理了一下问题核心是依赖方向完全失控页面 A 为了图省事直接 import 了页面 B 的 store 里的某个 getter。公共组件内部偷偷调用了一个只在某个业务页面里使用的工具函数。两个模块之间互相调用形成 A → B → A 的循环依赖运行时不报错但初始化时某个状态始终是undefined排查起来极度痛苦。为什么会这样本质原因是团队一直在做增量式开发——每次业务来了就找一个最近的、现成的代码去复用不考虑它在哪个模块、属于什么边界。三个月后整个系统的依赖图变成了一张没有中心的蜘蛛网。解决这个问题的第一步不是重构而是在白板上画出当前的模块依赖图然后一行一行地标出哪些箭头是反向的、哪些是跨层跳转的。通常画完这一步团队自己就看出问题了。接下来才是制定依赖规则只允许上层依赖下层、业务模块依赖公共基础模块、模块之间不直接互相引用需要协作就走事件或接口层。规则不需要多两三条就够关键是能持续执行。3. 落地模块化时我用过的三种拆分策略3.1 按技术层拆分项目最短路径上的兜底方案如果你刚把一个项目从script 标签堆积改成工程化方式最容易上手的拆分维度就是技术层src/ api/ # 所有接口请求 store/ # 全局状态 components/ # 通用组件 utils/ # 工具函数 pages/ # 页面级入口这种拆法最大的优点是好理解团队不用培训就能上手。它也是很多脚手架默认给的工程结构。但它的缺陷在项目增长到一定规模后会非常明显每个页面或功能的代码被按技术特征拆散到多个层里导致做一个完整的业务改动时你要横跨 API、store、components、pages 四个目录。我的判断标准是如果你的项目只是几十个页面、人数不超过五个、业务还不复杂按技术层拆分完全够用没必要为了架构感搞复杂。但你要清醒地知道这只是过渡态不是终点。你要做的不是抱着它不放而是持续观察它在哪个业务场景下开始变痛。3.2 按业务域拆分真正让模块化发挥价值的主力方式当一个项目开始出现一个需求要跨多个技术层改代码的痛感时就该考虑按业务域拆分。我实践下来效果很直接src/ modules/ user/ # 用户模块组件、状态、API、类型 order/ # 订单模块组件、状态、API、类型 product/ # 商品模块组件、状态、API、类型 shared/ # 跨模块复用的基础能力每个业务模块内是自治的自己管自己的视图、状态、接口和业务规则。模块之间不直接操作对方的内部文件只通过对外暴露的入口文件比如index.ts协作。这时候模块不再是一个抽象概念而是物理上看得见、摸得着的目录边界和依赖边界。这种拆分方式的核心逻辑是让业务需求的变更尽可能落在同一个模块内部。用户改资料你只需要进user模块订单状态流转你只需要进order模块。模块之间除非有明确的业务协作关系否则不产生直接代码依赖。这样改代码时的爆炸半径被控制住了团队成员之间的冲突也少了。我建议的模块内部结构很简单就四块组件层只负责渲染和交互不直接发请求。状态层管理本模块的状态状态不要无条件扔进全局 store。API 层封装模块涉及的后端接口。类型与工具模块内部特有的类型定义和纯函数工具。3.3 横切关注点权限、埋点、主题这类不属于任何一个模块的东西按业务域拆分后很快会遇到一个新问题有些代码不属于任何一个业务模块却又会被所有模块用到比如权限控制、埋点统计、主题切换、用户登录态。这些横切关注点cross-cutting concerns如果处理不好就会慢慢演变成一个伪模块——什么模块都引它它又什么都依赖。我实践下来比较可靠的做法是权限控制做成一个独立的基础能力包通过高阶组件、装饰器或者路由守卫来接入不要在业务模块内部散落大量if (hasRole)判断。埋点与统计封装成统一的埋点 API业务模块只管声明我在这个时机发生了什么事具体上报逻辑全部收敛在基础层。主题与样式变量放到shared的样式变量体系里通过设计令牌design token的方式使用而不是每个模块自定义一套颜色值。这类横切代码正是模块化设计里最常见的边界模糊地带。我的经验是宁可把它们做成纯基础设施也不要让它们长成万能的中间层。如果一个工具包被超过一半的模块使用你就要警惕它的膨胀趋势定期清理。为了让你更直观地判断哪种拆分策略适合你我给个参考拆分策略适用场景优点主要风险按技术层小团队、业务简单、快速上线简单直接上手零成本业务散落后期维护成本上升按业务域业务复杂、多模块并行迭代变更集中边界清晰模块间通信设计要求高分层分域混合项目规模大、团队分工明确兼顾稳定与解耦规则复杂需要纪律约束4. 模块之间的通信与依赖治理最容易翻车的地方4.1 通信方式的取舍别上来就上全局事件总线模块拆分之后最直接的问题就是两个模块要协作怎么通信。我见过太多项目为了解决模块间不直接引用的问题直接上了全局事件总线各种emit、on满天飞。结果是写代码时很爽——不 import 了嘛问题解耦了。但调试时就哭了一个事件被触发你根本不知道是谁发的、谁在监听、监听顺序是什么。正确的做法是有梯度的通信策略场景优先使用的通信方式父子模块或同模块内组件props / 回调函数兄弟模块共同完成一个流程提升到上级模块由上级协调跨多个模块、多个页面通过统一状态容器或服务层接口真正的临时通知、与 UI 无关事件总线慎用做好事件字典我的原则是先看能不能把协作提升到共同的上级去处理再看能不能通过接口层去调用最后才考虑事件机制。因为事件本质上是一种隐式依赖它绕过了代码静态检查靠字符串匹配滥用起来失控速度非常快。4.2 循环依赖你以为不会踩但它就在那里循环依赖是我在模块化项目里踩过最多、也最隐蔽的坑。它通常不是一次写出来的而是经过多轮增量修改慢慢长出来的// order-module/core.js import { getCurrentUser } from user-module/auth; export function initOrder() { const user getCurrentUser(); // ... }// user-module/auth.js import { resolveUserLevel } from order-module/core; export function getCurrentUser() { const level resolveUserLevel(); // ... }如果两个模块在编译期就形成这种互相 import 的关系ESM 虽然不会直接崩溃但在某些初始化路径上会拿到undefined而不是模块的实际导出。我遇到过最隐蔽的情况是本地开发环境正常生产构建后偶发白屏最后定位到就是循环依赖导致的初始化顺序错乱。根治的办法不是把 import 改小一点而是先画依赖图找到成环的那几个节点然后决定要么把环上被两头依赖的那段逻辑抽到shared基础层要么把其中一个文件的加载时机延后为动态import()。如果是架构层面的相互依赖A 在业务上就必须知道 B那应该反思模块边界是不是切错了。4.3 依赖治理工具与团队纪律让架构规则被机器执行我在几个项目里实践下来最有效的方法不是靠代码评审时多看一眼而是让机器去检查。依赖图可视化工具可以在构建或 CI 阶段输出一张模块依赖关系图还有一类工具可以做依赖规则检查在 lint 或测试阶段拦截禁止跨业务模块引用这类违规。实现方式不复杂本质就是自定义规则// 伪代码自定义依赖规则检查 module.exports { rules: { no-cross-module-import: { create(ctx) { return { ImportDeclaration(node) { const from ctx.getFilename(); const to node.source.value; if (isBusinessModule(from) isBusinessModule(to) getModuleName(from) ! getModuleName(to)) { ctx.report(node, 禁止直接跨业务模块引用请走对外接口); } } }; } } } };这类规则一旦进 CI效果是立竿见影的——它把架构规范从靠自觉变成了强约束。我的建议是最少配上三条硬性规则业务模块之间禁止直接互相引用。业务模块禁止反向依赖基础层或者基础层的敏感文件禁止被业务模块依赖。禁止出现循环依赖。每个业务模块只能通过约定的入口文件对外暴露能力。5. 让架构在真实项目里活下来的工程化配套5.1 Monorepo 与多包管理用物理边界固化模块边界当你把模块边界设计好了下一步就是思考这些模块在代码仓库层面怎么组织。我试过三种方式一个大仓库里按目录分模块、每个模块单独一个仓库、Monorepo 多包管理。现在更倾向后者原因很朴素模块之间需要独立的版本和构建边界但代码放在一起又方便统一维护和统一 CI。Monorepo 工具链比如 pnpm workspaces / npm workspaces可以帮你做到每个业务模块作为一个独立包拥有自己的package.json。模块之间的引用必须通过包引用而不是相对路径直接 import这天然地强化了边界。可以单独构建、单独测试、单独发布版本。公共的基础层升级时所有依赖它的模块都能被自动识别出来。这个改造不复杂尤其是已经有了清晰的业务域拆分后基本就是把目录平移到 workspaces 下再补一下包的导入路径。收益却很大因为模块的边界从代码风格升级成了物理约束。5.2 微前端另一种演进答案但别急着套前端架构演进聊到最后一定会有人问微前端。我自己的态度是微前端解决的是多团队、多技术栈、独立发布的组织问题不是代码太乱的技术问题。如果你只有一个前端团队迭代节奏统一那用微前端往往只会增加运行时隔离和加载调度的复杂度收益很小。我更建议按这个顺序思考先用模块化设计把代码边界划清楚。再用 Monorepo 把物理边界固化好。如果这时候依然有长期共存多套技术栈且无法统一步调发布的硬需求再考虑微前端方案。微前端的核心不是拆应用而是运行时的模块集成与隔离。它把构建期决定一切的模式改成了运行期按容器约定加载子应用。如果你团队里没有足够的人力去维护这套运行时机制就不要为了架构先进而硬上。5.3 架构评审与增量演进没有一锤子重构架构这件事最忌讳的是等放假了来一次彻底重构。我参与过的项目里凡是这种心态的重构基本都以失败收场——因为业务不会停你也不可能一次性地把所有模块清空重写。更实际的节奏是增量演进从一个最痛的点开始比如先解决循环依赖再把最混乱的模块边界切出来接着把依赖规则检查放进 CI最后才考虑仓库结构要不要改成 Monorepo。每一步都小到不阻塞日常迭代但又都在往同一个方向走。我通常会在每个季度留一个架构治理周只做三件事看依赖图有没有恶化、清理违规的引用、补几条能机器执行的规则。这个节奏不重但能保证架构不会悄悄腐烂。6. 回看演进我的一些体会我越来越觉得前端架构演进和模块化设计本质不是要一上来就设计出一个完美的分层模型而是要在业务速度和技术秩序之间找到一个可持续的平衡点。最开始可能只是从全局变量走到import/export然后从技术层拆分走到业务域拆分再通过工程化手段把边界固化下来。每一步都不是高深理论而是被真实问题逼出来的选择。如果一定要给后来者一句建议我会说先别急着复制某个标准架构而要先记录你项目里最痛的三个问题然后让架构演进为解决这三个问题服务。把规则工具化、把依赖可视化、把关键约束交给 CI 去执行这比任何一次轰轰烈烈的重构都更能让架构活下来。前端架构没有终点它更像是在一次次迭代中慢慢长成了一棵既能支撑业务、又不至于把人绊倒的树。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/21 19:12:14
翻斗式雨量计与压电式雨量传感器对比:原理、性能与选型指南
2026/10/21 19:12:14
REA建模框架:用事件驱动彻底解决业务对账难题
2026/10/21 19:12:14
微信支付商家券发券签名错误排查:证书私钥请求体全解析
2026/10/21 22:30:43
AI工具全景图:100+工具分类选型与工作流搭建指南
2026/10/21 22:30:43
Flink CDC 写入 Kafka 后字段为 null 的排查与解决
2026/10/21 22:30:43
lingbot-map:基于Canvas与GeoJSON的轻量级自研地图模块构建实践
2026/10/21 22:30:43
条件码寄存器详解:从原理到实战的跳转逻辑
2026/10/21 22:30:43
纯CSS实现贪吃蛇:状态机与radio hack的极致玩法
2026/10/21 22:25:42
知识管理插件化实战:从采集到输出打通效率链路
2026/10/21 0:01:44
ClickHouse实战:从列式存储到亿级数据秒级查询
2026/10/21 0:01:44
高效筛选arXiv机器学习论文:四步法实战指南
2026/10/21 0:01:44
Python爬取百度迁徙数据:接口签名、反爬与SQLite存储实战
2026/10/21 19:05:16
OpenResearch实战:Claude Code、Codex、OpenCode、Cursor 工作流搭建与故障排查
2026/10/21 14:47:26
知识管理核弹:用Obsidian构建第二大脑的工程化方案(TaoToken 统一 Key 接入版)
2026/10/21 19:05:04
金融AI落地指南:从调研报告到技术选型与工程实践
2026/10/20 2:24:03
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/20 2:23:57
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/21 19:01:08
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)