给"单文件胖前端"做瘦身手术 ------ 从零到一讲清前端模块化
故事背景:你写了一个全栈项目,前端一开始全塞在一个
index.html里,跑起来了。现在想整理一下,又不想引入一堆新工具。怎么干?
一、问题:为什么会"胖"?正常吗?
正常。
很多项目都这么走:MVP → 先跑起来 → 再重构。MVP 阶段,所有东西塞一个文件,能快速验证想法,这叫「快速原型」,完全合理。问题出在「跑通之后不收拾」。
我们来看看一个"胖单文件"的典型结构:
xml
index.html (~3000 行)
├── <style> (~1000 行) → 样式
├── <body> 模板(~1500 行) → 页面结构 + Vue 指令
└── <script>(~500 行) → 业务逻辑 + 函数
就像你收拾书包------刚放学回来说先放下书包,然后就去玩了;周末得花十分钟把课本、零食、水杯分开,把脏衣服掏出来。项目跑通了,不代表就不用整理了。
二、重构第一刀:把 CSS 从模板里抽出来
操作:
xml
- index.html: 删除整个 <style> 块
+ static/css/main.css: 把样式粘进去
+ index.html: 加一行 <link rel="stylesheet" href="/static/css/main.css">
为什么这么做?------关注点分离
「关注点分离」是软件工程最最基础的原则:
- CSS 管"长什么样"------颜色、字体、间距
- HTML 管"结构是什么"------有几个按钮、几个输入框
- JS 管"点了做什么"------发请求、改状态
如果你把这三类东西揉在一起,改样式得去几千行里翻 JS,改逻辑得在 CSS 块旁边找,就像你把课本和脏衣服放一块,找起来能不累吗?
把 CSS 抽出来,一个文件只干一件事 ------这就是最基础的可维护性:每个东西有固定位置,找东西不用翻遍整个屋子。
三、重构第二刀:给 HTTP 请求做个"总管"
这一步是给 AI 应用做地基,也是本次重构最重要的一步------因为 AI 应用的接口天然比普通后端更不稳定。
问题:为什么要"总管"?
原来你每个地方发请求,都要写一遍:
javascript
async function xxx() {
const resp = await fetch(url, ...)
if (!resp.ok) {
// 解析错误信息... 各种 if-else
throw new Error(...)
}
return resp.json()
}
有 10 个页面,你就得写 10 遍。要是想改"错误怎么解析",就得改 10 个地方。而且:
- AI 大模型调用容易超时
- 网络抖一下请求就失败了,得重试
- 多个请求同时飞,Loading 状态怎么统一?
这些问题每一个页面各写一遍,代码重复量爆炸,改起来累死人。
解法:做一个统一请求层,所有请求都走这里
新建 static/js/api.js,把「发请求、错了怎么办、超时、重试、Loading」这些事全部收在一处:
javascript
// api.js 里只干这一件事,但干得很彻底
export async function api(url, opts = {}, cfg = {}) {
// 1. 错误规范化:后端不管返回什么乱格式,都统一变成一句人话
// 2. 全局 Loading 计数:多个请求并发,Loading 显示/隐藏不闪烁
// 3. 超时控制:超时自动掐断,不卡死页面
// 4. 幂等重试:网络抖动 5xx,自动退避重试几次
}
收益:
- 一次做好,处处复用 ------以后所有页面都
import { api } from '/static/js/api.js',直接用就行,不用再写一遍错误处理。 - AI 应用刚需------大模型输出不稳定、超时是家常便饭,统一重试/超时/错误处理就是你的第一道防线。
- 可测试------请求层独立出来,以后 mock 数据测试不用碰页面逻辑。
四、重构第三刀:搭原生模块化骨架,不引入构建工具
抽完 CSS 和请求层,现在把"纯数据"和"纯工具"也抽出来------用浏览器原生支持的 ES 模块 ,零工具就能拆文件。
为什么不引入 Vite/Webpack?------这是主动选择,要讲清楚原因
❓ 现在行业不都是 Vite + React 或者Vue 吗?为什么不用?
✅ 因为我的后端是 FastAPI,它本来就是"直接把 HTML 发给浏览器"。一旦引入 Vite,我就得加一套「开发构建 + 打包产物部署」流程------从"一键 run 就能跑"变成"先开发构建,再部署产物",部署成本直接翻倍。
取舍要主动说出来,不能闷头堆代码:
- 我的项目是个人全栈 MVP,目标是"快速迭代、低成本运行"------不需要工程化的"复杂度升级"。
- 浏览器早就原生支持
<script type="module">+import/export了------不用任何工具,就能拆成多个文件,为什么非要引入一堆新依赖? - 用造火箭的标准去造自行车当然很好,何不食肉糜?
最终骨架(无构建):
bash
static/
css/main.css # 样式(关注点分离)
js/
api.js # HTTP 请求总管(AI 应用地基)
constants.js # 纯数据:比如选项数组、配置、枚举(不依赖状态)
utils.js # 纯函数:工具方法(输入→输出,不依赖页面状态)
templates/index.html # 只剩模板 + 组件状态逻辑 + 引入口
五、重构第四刀:什么该抽?什么不该抽?------区分纯函数和有状态函数
抽完小的零碎,你会遇到一个问题:一堆函数摆在面前,是不是全抽出去?别瞎抽,先分类:
🔹 第一类:纯函数 / 纯数据 ------ 放心抽出去
纯函数 就像家里的万年历 ------你放客厅、放书房、放门口,它都一样能用:给它一个输入,就给你一个输出,不依赖任何别的东西,也不改变外面的世界。
举例子:
| 函数/数据 | 为什么是纯的? | 抽去哪里 |
|---|---|---|
formatDate('2026-08-13') → "2026/08/13" |
只跟输入日期有关,跟页面状态没关系 | utils.js |
highlightWords(sentence, words) → 带高亮的 HTML |
只跟输入的句子和单词有关 | utils.js |
TTS_MODELS = [{name, label, group}, ...] |
写死的配置,不会变 | constants.js |
✅ 结论:万年历放公共抽屉,哪都能用,抽出去!
🔹 第二类:有状态函数 ------ 别瞎抽,留在原地
有状态函数 就像你书桌上的台灯------它得插你桌上的插座才能亮,你不能把它硬搬到客厅去用。它要读页面上的响应式状态,抽出去就得把状态当参数一层层传进来。
举例子:
javascript
// 这个函数要读 `imageModels.value`(这是页面上的响应式状态,后端加载后才有的)
function getImageModelNote(model) {
const m = imageModels.value.find(...)
return ...
}
要是硬抽去 utils.js,调用的时候就得这么写:
scss
// 调用者每次都得把 imageModels 当参数传进来,啰嗦死了!这就是"传参地狱"
getImageModelNote(imageModels.value, model)
什么是「传参地狱」? 就是为了满足"抽出去"这个形式,硬生生把一堆参数从调用者传到被调用者,每调用一次就得背一遍参数,又容易漏,又啰嗦。为了模块化而模块化,反而更难维护了。
✅ 结论:只抽纯的,不抽依赖状态的,既练了手,又不制造新麻烦。
六、重构完成:我们得到了什么?
| 原先 | 现在 |
|---|---|
| 一个文件 3600 行 | 分成 5 个文件,最大的 index.html 瘦身约 1200 行 |
| 样式、结构、逻辑揉在一起 | 每个文件只干一类事,改样式不用翻 JS |
| 到处是重复的错误处理代码 | 统一处理,改一次全项目生效 |
| 想加新功能不知道在哪加 | 新增功能有固定位置放 |
关键的工程感悟(给即将实习的你)
- 先跑通,再重构 ------ MVP 先堆出来能跑,再整理,别一开始就追求"完美架构"。
- 取舍要透明 ------ 选"简单还是规范",选"不用工具还是用工具",自己心里要有数,并且要能说出来为什么。闷头堆代码才是最糟的。
- 抽纯不抽杂 ------ 先把纯函数/纯数据抽出去,有状态的留在原地,避免"传参地狱"。边界感是工程能力的核心。
- AI 应用要先做请求层 ------ LLM 输出不可信、接口不稳定,统一把错误/超时/重试做了,这就是地基。
七、上线体检:重构完还能用吗?
重构完必须实测:
- ✅ 控制台零 JS 报错
- ✅ 所有页面功能正常(列表加载、日期显示、下拉选项、高亮都正常)
- ✅ 所有新模块 HTTP 200 正常加载
- ✅ 不改变原有交互,用户完全感知不到重构------这叫「不改变行为的重构」,是正确姿势。
重构完成。胖文件变成清晰骨架,功能一点没少,以后改起来快多了。