第一章:单文件时代

什么是单文件应用

说白了,单文件应用就是「一个 HTML 文件搞定一切」。

我刚入行那会儿,前端项目就是这么干的。一个 index.html,里面塞着 CSS 样式、JavaScript 逻辑,再加上 HTML 结构。所有代码都在一个文件里,打开就能跑,简单粗暴。

你想想看,那时候没有 Node.js,没有 Webpack,甚至连 jQuery 都还没普及。写个页面,就是新建一个 .html 文件,然后往里写东西。浏览器直接打开,效果立竿见影。

这种模式,我称之为「单文件时代」。它有几个明显特征:

  • 一个文件 = 一个页面:所有资源都在一个 HTML 里
  • 零构建:不需要打包、编译,保存即刷新
  • 直接运行:双击文件或用浏览器打开即可
  • 适合小项目:几十行代码的页面,用这种方式最省事
小提示:现在很多原型设计、快速演示、在线工具,依然在用单文件的方式。比如 CodePen、JSFiddle 上的 demo,本质上就是单文件应用。

HTML/CSS/JS 混写的痛点

单文件应用虽然简单,但一旦项目变大,问题就来了。

我记得有一次,我接手一个同事留下的单文件项目。那个 HTML 文件有 2000 多行,CSS 和 JS 混在一起,找一段逻辑要翻半天。我当时就想:这玩意儿怎么维护?

具体来说,混写模式有这几个硬伤:

  1. 可读性差:HTML 结构、样式定义、业务逻辑全搅在一起。你想改个按钮颜色,得先找到对应的 CSS 在哪;想修个点击事件,又得在 JS 堆里翻。
  2. 复用困难:同样的导航栏、同样的弹窗组件,如果多个页面都需要,只能复制粘贴。改一处,所有地方都得跟着改。
  3. 协作冲突:两个人同时改一个文件,Git 合并时全是冲突。我经历过一次,两个人改了同一个 HTML 的不同部分,结果合并后页面直接崩了。
  4. 加载性能差:所有代码一次性加载,哪怕用户只看了首页,也得把整个页面的 JS 和 CSS 都下载下来。
  5. 调试体验糟糕:浏览器开发者工具里,所有代码都在一个文件里,断点调试时找函数位置都费劲。
避坑指南:我曾经在一个小项目中坚持用单文件模式,结果项目从 3 个页面膨胀到 20 多个页面。最后不得不花两天时间重构,把所有代码拆成独立文件。那两天我改代码改到凌晨两点,教训深刻。

一个简单的单页面计算器案例

光说不练假把式。咱们写一个单文件计算器,感受一下这种模式。

这个计算器很简单:两个输入框,一个下拉选择运算符,一个按钮,一个结果显示区域。所有代码都在一个 HTML 文件里。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>单文件计算器</title>
    <style>
        /* 所有样式写在这里 */
        body {
            font-family: Arial, sans-serif;
            max-width: 400px;
            margin: 50px auto;
            padding: 20px;
            background: #f0f4ff;
        }
        .calculator {
            background: white;
            padding: 24px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        h2 {
            text-align: center;
            color: #2c3e50;
            margin-top: 0;
        }
        input, select, button {
            width: 100%;
            padding: 10px;
            margin: 8px 0;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            font-size: 16px;
            box-sizing: border-box;
        }
        button {
            background: #4a90d9;
            color: white;
            border: none;
            cursor: pointer;
            font-weight: bold;
        }
        button:hover {
            background: #357abd;
        }
        .result {
            margin-top: 16px;
            padding: 12px;
            background: #e8f4fd;
            border-radius: 6px;
            text-align: center;
            font-size: 20px;
            color: #2c3e50;
        }
    </style>
</head>
<body>
    <div class="calculator">
        <h2>简易计算器</h2>
        <input type="number" id="num1" placeholder="输入第一个数字">
        <input type="number" id="num2" placeholder="输入第二个数字">
        <select id="operator">
            <option value="+">加 (+)</option>
            <option value="-">减 (-)</option>
            <option value="*">乘 (*)</option>
            <option value="/">除 (/)</option>
        </select>
        <button onclick="calculate()">计算结果</button>
        <div class="result" id="result">等待计算...</div>
    </div>

    <script>
        // 所有 JavaScript 逻辑写在这里
        function calculate() {
            const num1 = parseFloat(document.getElementById('num1').value);
            const num2 = parseFloat(document.getElementById('num2').value);
            const operator = document.getElementById('operator').value;
            let result;

            // 检查输入是否合法
            if (isNaN(num1) || isNaN(num2)) {
                document.getElementById('result').textContent = '请输入有效数字';
                return;
            }

            // 执行运算
            switch (operator) {
                case '+': result = num1 + num2; break;
                case '-': result = num1 - num2; break;
                case '*': result = num1 * num2; break;
                case '/': 
                    if (num2 === 0) {
                        document.getElementById('result').textContent = '除数不能为0';
                        return;
                    }
                    result = num1 / num2; 
                    break;
                default: result = '未知运算符';
            }

            // 显示结果
            document.getElementById('result').textContent = `结果:${num1} ${operator} ${num2} = ${result}`;
        }
    </script>
</body>
</html>

你看,这个文件里 HTML、CSS、JS 全都有。保存成 calculator.html,双击就能用。

但问题也很明显:

  • 如果我想给这个计算器加个「历史记录」功能,代码会越来越长
  • 如果我想在另一个页面也用这个计算器,只能复制整个代码块
  • 如果团队里两个人同时改这个文件,合并时大概率会冲突
核心认知:单文件模式适合「一次性使用」或「快速原型」。但一旦项目需要维护、扩展、多人协作,就必须走向组件化和工程化。

从单文件到工程化的思考

写这个计算器的时候,我就在想:如果能把 HTML 结构、CSS 样式、JS 逻辑拆成三个独立文件,是不是更好维护?

如果能把计算器封装成一个「组件」,在其他页面直接引用,是不是更省事?

如果能把代码压缩、合并、按需加载,是不是性能更好?

嗯,这些想法,就是前端工程化的起点。后面的章节,我们会一步步把这些想法变成现实。

下一章,咱们聊聊「如何把单文件拆成多文件,以及这中间会遇到哪些坑」。

个人建议:初学者可以先从单文件入手,感受一下「原始」的开发方式。等你真正体会到痛了,再学工程化工具,理解会更深刻。我当年就是这么过来的。