《前端工程化:从零重构》课程第一章:混乱的起源 —— 当项目失去秩序

5 阅读5分钟

📖 课程第一章:混乱的起源 —— 当项目失去秩序

副标题:从手动管理 <script> 标签到理解模块化的必要性

1. 课程目标 (Learning Objectives)

完成本课后,你将能够:

  • 复现痛点:在没有构建工具的情况下,手动维护一个多文件依赖的项目,体验“依赖地狱”。
  • 理解本质:深刻理解浏览器原生脚本加载机制与模块化需求的矛盾。
  • 定义工程化:用一句话清晰定义前端工程化的核心价值(不仅仅是打包,更是依赖管理开发体验)。
  • 产出物:一个故意写得“很烂”的 Vanilla JS 项目代码库,作为后续课程的“重构靶子”。

2. 核心场景:一个“即将崩溃”的待办列表 (Todo List)

我们将创建一个简单的 Todo List,但故意采用最原始、最手工的方式来组织代码。

🛠️ 动手实践 (Hands-on Lab)

请创建一个文件夹 chapter-1-chaos,并建立以下文件结构:

chapter-1-chaos/
├── index.html
├── js/
│   ├── utils.js       // 工具函数
│   ├── store.js       // 数据存储
│   ├── renderer.js    // 页面渲染
│   └── app.js         // 入口逻辑
└── css/
    └── style.css
第一步:编写业务逻辑 (故意制造依赖耦合)

js/utils.js (基础工具)

// 注意:这里没有 export,我们假设它是全局变量
function generateId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

function saveToLocal(data) {
  localStorage.setItem('todos', JSON.stringify(data));
}

js/store.js (依赖 utils)

// 问题预埋:如果 utils.js 没加载,这里会报错 "generateId is not defined"
const Store = {
  todos: [],
  add(text) {
    const todo = { id: generateId(), text, completed: false }; // 依赖全局函数
    this.todos.push(todo);
    saveToLocal(this.todos); // 依赖全局函数
    return todo;
  },
  init() {
    const data = localStorage.getItem('todos');
    this.todos = data ? JSON.parse(data) : [];
  }
};

js/renderer.js (依赖 store)

// 问题预埋:依赖全局变量 Store
const Renderer = {
  render() {
    const listEl = document.getElementById('todo-list');
    listEl.innerHTML = '';
    Store.todos.forEach(todo => { // 依赖全局对象
      const li = document.createElement('li');
      li.textContent = todo.text;
      listEl.appendChild(li);
    });
  }
};

js/app.js (入口)

// 初始化
Store.init();
Renderer.render();

// 绑定事件 (为了简单直接写在这里)
document.getElementById('add-btn').addEventListener('click', () => {
  const input = document.getElementById('todo-input');
  if(input.value) {
    Store.add(input.value);
    Renderer.render();
    input.value = '';
  }
});
console.log("App Initialized");
第二步:在 HTML 中手动编排 (痛苦的根源)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>混沌的 Todo List</title>
  <!-- 样式先不管 -->
</head>
<body>
  <h1>我的待办事项 (原始版)</h1>
  <input type="text" id="todo-input" placeholder="输入任务...">
  <button id="add-btn">添加</button>
  <ul id="todo-list"></ul>

  <!-- 
    🔴 关键痛点时刻:
    你必须人工保证脚本的加载顺序!
    如果不小心把 app.js 放在 utils.js 前面,程序直接崩溃。
    如果新增一个 helper.js 被 store.js 依赖,你得记得把它插到中间。
  -->
  <script src="js/utils.js"></script>
  <script src="js/store.js"></script>
  <script src="js/renderer.js"></script>
  <script src="js/app.js"></script>
</body>
</html>

3. 深度剖析:我们遇到了什么问题?

现在,运行这个 HTML 文件(直接用浏览器打开或简单的 Live Server)。虽然它能跑,但请思考以下工程化危机

❌ 危机一:脆弱的依赖顺序 (Dependency Order)

  • 现象:如果你在 index.html 中不小心把 <script src="js/store.js"> 放到了 utils.js 前面。
  • 后果:控制台报错 Uncaught ReferenceError: generateId is not defined,整个应用白屏。
  • 工程化视角:在大型项目中,文件可能有上百个,人工维护拓扑排序(Topological Sort)是不可能的。构建工具的第一个作用:自动分析依赖图,解决加载顺序。

❌ 危机二:全局污染 (Global Namespace Pollution)

  • 现象generateId, Store, Renderer 全部挂载在全局 window 对象上(隐式或显式)。
  • 后果:如果引入第三方库(比如 jQuery 或某个 analytics 脚本),它们也定义了一个 Store 变量,你的代码瞬间被覆盖,Bug 难以追踪。
  • 工程化视角:我们需要作用域隔离。这就是模块化(CommonJS/ESM)诞生的原因。

❌ 危机三:难以维护与重构

  • 现象:如果你想把 generateId 移动到一个新的 helpers/id-generator.js 文件中。

  • 后果

    1. 新建文件。
    2. 剪切代码。
    3. 回到 index.html 修改 <script> 标签顺序
    4. 检查所有引用了 generateId 的文件,确保它们都在新文件之后加载。
  • 工程化视角:这种高耦合让重构变得极其危险。我们需要静态分析能力,让工具告诉我们在哪里引用了什么。

❌ 危机四:网络性能灾难

  • 现象:浏览器每遇到一个 <script> 标签,就要发起一次 HTTP 请求(HTTP/1.1 下尤其严重,即使 HTTP/2 也有开销)。
  • 后果:10 个 JS 文件 = 10 次请求 + 10 次解析阻塞。首屏加载极慢。
  • 工程化视角:我们需要打包 (Bundling) ,将多个小文件合并为一个大文件(或按需分割),减少请求次数。

4. 理论升华:什么是前端工程化?

通过上面的痛苦体验,我们可以给出一个深刻的定义:

前端工程化,就是利用工具链和规范化流程,将手工作坊式的代码编写模式(手动管理依赖、全局变量、原始文件),转化为工业化的生产模式(自动依赖分析、作用域隔离、资源优化),以解决复杂度协作性能问题。

它的核心不是 Webpack 或 Vite 这些工具,而是解决上述四个危机的思维模式


5. 课后作业 (Homework)

为了巩固记忆,请完成以下挑战:

  1. 制造崩溃:故意打乱 index.html 中的 script 顺序,截图报错信息,感受那种“不知道哪里错了”的无助感。

  2. 模拟扩展:尝试新增一个 validator.js 文件,里面有一个 validate(text) 函数,并在 store.js 中使用它。

    • 你需要修改几个文件?
    • 如果你忘了在 HTML 里加 validator.js,会发生什么?
    • 如果你加错了位置,会发生什么?
  3. 思考题:如果这个项目从 5 个文件变成 500 个文件,你觉得现在的开发模式还能维持吗?你会怎么解决?(写下你的直觉方案,不用管对错,这是下节课的伏笔)。


6. 下节课预告

“既然手动管理顺序这么痛苦,那如果我们让 JavaScript 自己告诉浏览器谁依赖谁呢?
下节课,我们将穿越回 2009-2015 年,深入模块化战争:从 CommonJS 到 AMD,再到 ESM 的最终胜利。我们将亲手实现一个简易的模块加载器。”


💡 给你的执行建议

  1. 创建仓库:现在就在 GitHub/Gitee 上创建一个名为 frontend-engineering-from-scratch 的仓库。
  2. 提交代码:把刚才那个“混乱版”的 Todo List 代码提交上去,Commit 信息写:feat(chapter-1): 初始化的混乱版本,体验无工程化的痛苦