《前端工程化:从零重构》前端工程化实战课程 · 第二章

4 阅读6分钟

📘 前端工程化实战课程 · 第二章

浏览器原生模块化:ESM 的机制与边界

导读:告别全局变量污染,拥抱浏览器原生的 ES Modules (ESM)。本章不依赖任何构建工具,直接在浏览器中运行模块化代码。我们将深入探究浏览器的静态分析机制加载时序以及多入口架构,并厘清“浏览器能力”与“工程化需求”的边界。


1. 核心概念:什么是浏览器原生 ESM?

ESM (ECMAScript Modules) 是 JavaScript 官方的模块化标准。现代浏览器(Chrome 61+, Firefox 60+, Safari 10.1+)已完全原生支持,无需任何转译或打包即可直接运行。

1.1 两个关键语法

  • 导出 (export) :暴露变量、函数或类。

    // utils.js
    export const PI = 3.14;          // 命名导出
    export function add(a, b) { ... } 
    export default class User { ... } // 默认导出
    
  • 导入 (import) :引入其他模块的功能。

    // app.js
    import { PI, add } from './utils.js'; // 对应命名导出
    import User from './user.js';         // 对应默认导出
    

1.2 激活方式

在 HTML 中,必须显式声明 type="module"

<!-- ❌ 错误:会被当作普通脚本,报错 "Cannot use import statement outside a module" -->
<script src="app.js"></script>

<!-- ✅ 正确:浏览器启动模块加载器 -->
<script type="module" src="app.js"></script>

2. 深度原理:浏览器是如何加载模块的?

这是理解 ESM 性能特征的关键。浏览器的行为与普通 <script> 标签有本质区别。

2.1 静态分析 (Static Analysis)

核心规则import 语句必须是静态的

  • import './a.js' (字符串字面量,路径固定)
  • import './' + fileName (动态拼接,禁止)
  • if (condition) { import './a.js' } (条件导入,禁止在顶层)

为什么重要?
因为路径是固定的,浏览器可以在不执行任何代码的情况下,仅通过读取文本就能知道这个文件依赖了谁。

2.2 加载时序:预加载扫描 (Preload Scanner)

当浏览器遇到 <script type="module"> 时,发生以下过程:

  1. 下载入口文件:请求 app.js

  2. 暂停执行绝对不执行 app.js 中的任何代码(包括 console.log)。

  3. 递归扫描与预加载

    • 解析 app.js 源码,提取所有 import 路径。
    • 发现依赖 store.jsutils.js立即并行发起请求
    • 拿到 store.js 后,继续扫描它的 import,发现依赖 api.js,继续请求。
    • 此过程递归进行,直到构建出完整的依赖树
  4. 统一执行:只有当依赖树中所有文件都下载成功后,浏览器才按照依赖顺序(先底层,后上层)开始执行代码。

💡 结论 1:ESM 是“全有或全无” (All-or-Nothing)。如果深层依赖中有一个文件 404 或加载失败,整个模块树都不会执行
💡 结论 2:这也意味着,如果依赖树很深或文件很大,首屏等待时间会变长,因为必须等所有资源就位。


3. 实战特性:作用域与多入口

3.1 严格的作用域隔离

每个模块都有自己独立的作用域。

  • app.js 中定义的 const x = 1,在 store.js 中完全不可见。
  • 它们也不会污染全局 window 对象。
  • 优势:彻底解决了第一章中的“变量命名冲突”问题。

3.2 多入口支持 (Multiple Entry Points)

一个 HTML 页面可以包含多个独立的模块入口:

<body>
  <div id="header"></div>
  <div id="footer"></div>

  <!-- 入口 A:负责头部逻辑 -->
  <script type="module" src="./header.js"></script>
  
  <!-- 入口 B:负责底部逻辑 -->
  <script type="module" src="./footer.js"></script>
</body>
  • 独立性header.jsfooter.js 互不干扰,拥有各自的作用域。
  • 共享缓存:如果两者都 import './utils.js',浏览器只会下载一次 utils.js
  • 单例特性:在现代浏览器中,同一 URL 的模块通常只会被求值 (Evaluate) 一次。这意味着如果 utils.js 导出了一个对象实例,两个入口引用的是同一个实例。

4. 最佳实践:命名导出 vs 默认导出

在纯浏览器环境下,选择哪种导出方式主要基于语义未来优化潜力

场景推荐方式理由
工具函数库命名导出 (export const fn)语义明确:调用者清楚知道引入了什么。 Tree Shaking 基础:虽然浏览器不做 Tree Shaking,但这种写法为将来使用构建工具优化打下了基础(允许移除未使用的函数)。
单一组件/类默认导出 (export default)简洁直观:一个文件主要就导出一个东西(如 Button.vue, User.class),导入时无需纠结名字,可随意重命名。
混合场景混合使用框架常用模式:默认导出主组件,命名导出辅助 Hooks 或常量。
// ✅ 推荐:工具库
export function formatDate() {}
export function parseJSON() {}

// ✅ 推荐:组件
export default function Button() {}

5. 边界探讨:浏览器 ESM 的局限性

既然浏览器原生支持这么好,为什么我们还需要 Webpack/Vite?
注意:本章不深入讲解工具原理,仅列出浏览器做不到的事,作为下一章的引子。

🚫 限制 1:无法解析裸模块 (Bare Specifiers)

  • 浏览器import React from 'react' ❌ 报错。浏览器不知道 'react' 对应哪个 URL,它只认识相对路径 (./, /, https://)。
  • 现状:你必须写 import React from './node_modules/react/index.js' (极其繁琐且不可维护)。

🚫 限制 2:不支持非 JS 资源

  • 浏览器import './style.css' ❌ 报错。JS 引擎无法解析 CSS 语法。
  • 现状:CSS 必须通过 <link> 标签引入,无法像 JS 一样模块化导入。

🚫 限制 3:网络请求开销

  • 浏览器:如果有 100 个模块文件,就会发起 100 次 HTTP 请求。虽然 HTTP/2 支持并发,但在弱网环境下,大量的握手和延迟依然会影响性能。
  • 现状:没有合并文件的能力。

🚫 限制 4:不支持新语法 (如 TypeScript/JSX)

  • 浏览器:只能运行标准的 JavaScript。.ts.jsx 文件直接加载会报错。

🔜 预告:正是因为上述限制,工程化工具 (Bundler) 应运而生。下一章,我们将引入构建工具,看看它们如何填补浏览器能力的空白,实现 npm install、TypeScript 支持和自动打包。


6. 本章实验任务

目标:在不使用任何构建工具(无 Webpack/Vite,无 npm 包)的情况下,重构 Todo List。

  1. 环境准备

    • 必须使用本地服务器 (Live Server / python -m http.server)。注意:由于 CORS 策略,ESM 模块不能通过 file:// 协议直接打开,必须通过 http:// 访问。
  2. 代码拆分

    • 将逻辑拆分为 store.js, utils.js, ui.js, app.js
    • 使用 export / import 连接它们。
    • 确保 index.html 中只引入 app.js
  3. 观察现象

    • 打开 Chrome DevTools -> Network 面板。
    • 观察 Waterfall (瀑布流) :验证是否所有 JS 文件都在执行前被下载了?
    • 尝试故意写错一个依赖路径,观察控制台报错,验证“全有或全无”机制。
  4. 多入口测试

    • 在 HTML 中增加一个 <script type="module" src="./analytics.js">,模拟独立的统计模块,验证它与主程序互不干扰。

7. 总结

  1. ESM 是静态的:浏览器在执行前通过静态分析构建依赖树。
  2. 加载是阻塞的:所有依赖必须下载完成后,代码才开始执行。
  3. 作用域是隔离的:模块间变量不可见,天然防冲突。
  4. 能力是有边界的:浏览器不懂 node_modules,不懂 CSS 导入,不懂 TS。这些是工程化要解决的问题。