Skip to content

JavaScript 中的模块化、ES Module 与运行时边界

当项目从几百行脚本变成几千、几万行代码后,真正会先出问题的,通常不是语法,而是组织方式。

模块化这条主线,本质上就是在解决:代码怎么拆、依赖怎么管、运行时怎么加载、不同环境下边界怎么划分。

1. 为什么需要模块化

在没有模块化的时候,前端代码常见写法是:

  1. 多个 script 直接挂到页面上
  2. 大量全局变量互相污染
  3. 依赖顺序靠人工维护
  4. 文件一多,维护成本急剧上升

模块化要解决的核心问题,主要就是:

  1. 代码隔离
  2. 依赖声明
  3. 复用组织
  4. 按需加载与工程构建

2. 模块到底是什么

模块可以看成 一个有自己作用域、可以暴露部分能力给外部使用的代码单元

它通常会做两件事:

  1. 对外导出自己想暴露的内容
  2. 对内隐藏实现细节

3. 常见模块化方案有哪些

3.1 CommonJS

最典型的是 Node.js 早期生态里的:

  1. require
  2. module.exports

它的特点先抓这几条:

  1. 更偏服务端和构建工具生态
  2. 更像运行时按需加载
  3. 历史上在 Node.js 里非常常见

3.2 ES Module

现代前端最核心的模块化方案。

典型写法:

js
import { add } from './math.js'
export const name = 'demo'

它的关键价值在于:

  1. 成为语言层面的官方标准
  2. 静态结构更清晰
  3. 更利于打包器做依赖分析、Tree Shaking 等优化

4. importrequire 到底有什么区别

可以抓最核心的边界:

  1. import/export 更偏标准模块体系
  2. require/module.exports 更偏 CommonJS 历史生态

更细一点说:

维度CommonJSES Module
典型语法requiremodule.exportsimportexport
主要使用场景Node.js 历史生态现代前端与现代 Node
依赖分析更偏运行时更偏静态分析
工程优化友好度一般更好

5. 为什么说 ES Module 更利于工程化

因为打包器在构建阶段更容易知道:

  1. 你依赖了谁
  2. 谁导出了什么
  3. 哪些代码根本没被用到

这也是为什么 Tree Shaking 大多会围绕 ES Module 展开。

6. 模块化和作用域是什么关系

模块化某种意义上就是:把原来容易泄漏到全局的变量,收进更稳定的文件级边界里。

也就是说,模块不仅是“导入导出语法”,更是:一种更可靠的作用域隔离机制。

7. 浏览器里的模块是怎么跑起来的

在浏览器里,ES Module 常见入口形式是:

html
<script type="module" src="./main.js"></script>

它和普通脚本相比,常见差异包括:

  1. 按模块语义加载
  2. 默认有自己的模块作用域
  3. 支持 import/export
  4. 加载与执行顺序更受模块依赖关系控制

8. 模块化和打包器是什么关系

这里很容易混。

把关系拆开看:

  1. 模块化解决“代码怎么组织”
  2. 打包器解决“这些模块如何被分析、转换、合并、优化并交付浏览器”

所以:模块化是组织方式,打包器是工程工具。

9. 运行时边界为什么重要

很多人写前端时,最容易踩的坑之一就是:把浏览器环境、Node.js 环境、构建阶段环境混在一起。

例如:

  1. 在浏览器代码里直接用 Node.js 专属 API
  2. 在服务端渲染场景里直接访问 window
  3. 在构建配置里和业务运行时代码混写

所以“运行时边界”真正要回答的是:这段代码到底打算在哪个环境执行。

10. 浏览器运行时 和 Node.js 运行时有什么差别

10.1 浏览器更擅长什么

  1. DOM 操作
  2. 事件系统
  3. 页面渲染
  4. Web API

10.2 Node.js 更擅长什么

  1. 文件系统访问
  2. 服务端运行
  3. 包管理和构建工具生态
  4. 服务端脚本能力

这也是为什么:

  1. 同样是 JavaScript
  2. 但不同运行时里,可用的 API 和职责边界并不一样

11. 一张模块与运行时关系图

mermaid
flowchart TD
    A[JavaScript 源码] --> B[按模块拆分]
    B --> C[ES Module 或 CommonJS]
    C --> D[构建工具分析依赖]
    D --> E[浏览器运行时]
    D --> F[Node.js 运行时]

12. 工程上最容易踩的坑

12.1 误把模块化当成“只是 import/export”

它本质上还包含:

  1. 代码边界
  2. 依赖关系
  3. 运行时约束

12.2 不区分浏览器代码和 Node.js 代码

这样很容易在运行时直接报错。

12.3 模块拆分过细或过粗

  1. 太粗:文件越来越臃肿
  2. 太细:依赖链过深,理解成本增加

13. 小结

这条主线可以压缩成几句话:

  1. 模块化解决的是代码组织和依赖管理
  2. ES Module 是现代前端最核心的标准模块体系
  3. 打包器是模块化落地到工程交付过程中的关键工具
  4. 写代码时一定要清楚当前运行时到底是浏览器、Node.js,还是构建阶段

如果继续扩展 JavaScript 目录,比较自然的下一步是再补一篇“DOM、事件系统与浏览器 API”,把语言层和浏览器运行时层真正接起来。

基于 VitePress 构建的个人技术笔记。