您可能在现代 JavaScript 开发中使用过 ES 模块,但您知道它们演变背后的历史吗?了解从早期 JavaScript 实践到今天的模块系统的历程将帮助您了解我们已经走了多远以及为什么 ES 模块改变了游戏规则。
时间是 1995 年,第一个网页创建四年后。大多数网站都很简单——带有文本和最小交互性的静态页面。然而,开发人员很快就在寻找使网页更具动态性的方法。
在这种环境下,Netscape(当时占主导地位的网络浏览器)聘请了 Brendan Eich 来创建一种可以直接在浏览器中运行的脚本语言。这导致了 JavaScript 的诞生,这是一种简单易懂的语言,特别是对于像网页设计师这样的非程序员来说。他做到了,在 10 天内完成了第一个版本。
最初,JavaScript 的目的是向网页添加小的增强功能,例如表单验证,而不需要将数据往返到服务器。然而,随着网站的交互性变得越来越强,JavaScript 很快就超出了其最初的用途。
早期,所有 JavaScript 代码都位于全局范围内。随着越来越多的开发人员向同一页面添加代码,名称冲突的风险也随之增加。如果多个脚本使用相同的变量或函数名称,代码可能会以意想不到的方式中断。
为了解决这个问题,开发人员采用命名约定来防止冲突。如果一段代码只运行一次,开发人员通常会将其包装在 IIFE(立即调用函数表达式)中。这使得函数和变量的作用域保持在函数内,防止它们污染全局命名空间。
(function init() { function getData(){ // ... } })()
这在当时已经足够好了,因为大多数网站都是服务器端渲染的,客户端逻辑很少。
2008 年,Ryan Dahl 创建了 Node.js,这是一个用于构建服务器端应用程序的 JavaScript 运行时。这开辟了一个全新的可能性世界,但缺乏模块系统意味着开发人员难以管理大型代码库。
2009年,CommonJS被引入来解决服务器端的这个问题。 CommonJS 模块系统允许开发人员定义模块、公开功能以及导入其他模块。以下是其工作原理的示例:
const math = require("./math"); function subtract(a,b) { return math.add(a,-b); } module.exports = { subtract: subtract }
使用 CommonJS,每个文件都被视为自己的模块,并且使用 require 函数导入模块并使用 module.exports 导出。
CommonJS 的一些主要功能包括:
需要模块时,文件扩展名是可选的(例如 require('./math') 自动查找 math.js)。
模块加载是同步的,这意味着程序会等待模块加载后再继续执行。
在文章后面我们将了解为什么 Ryan Dahl 承认对这两个设计决定感到遗憾。
大约在同一时间,另一个名为AMD(异步模块定义)的模块系统被开发出来。 CommonJS 主要关注服务器端 JavaScript,而 AMD 旨在处理浏览器中的客户端 JavaScript。
AMD 的关键特性是它能够异步加载模块。这使得浏览器可以在任何给定时间仅加载页面所需的 JavaScript,从而通过减少初始页面加载时间来提高性能。它还解决了与依赖项解析相关的问题,确保模块仅在其依赖项完成加载后才会运行。
AMD 的优势包括:
随着 2010 年 npm(服务器端 JavaScript 的包管理器)的兴起,跨浏览器和服务器共享代码的需求变得显而易见。 Browserify 是一个工具,它允许开发人员通过转换代码以与浏览器环境兼容来在浏览器中使用 CommonJS 模块。
有 2 个竞争标准:CommonJS 和 AMD。需要一个可以在任何地方工作而无需构建步骤的单一模块系统。并于 2011 年引入了通用模块定义(UMD)。
UMD 结合了两全其美,允许开发人员编写可以在以下位置运行的模块:
UMD 在库作者中非常流行,Lodash、Underscore.js、Backbone.js 和 Moment.js 等著名库都采用了它。然而,UMD 有一些显着的缺点。它在解决兼容性问题的同时,也带来了管理两个系统的复杂性,并且继承了 AMD 和 CommonJS 的问题。
2015 年,ES 模块 (ESM) 作为 ECMAScript 标准的一部分被引入,最终为 JavaScript 提供了原生模块系统。到 2017 年,所有主流浏览器都支持 ES 模块,2020 年,Node.js 也添加了支持。
让我们看看为什么 ES 模块是最好的:
以下UMD代码:
(function init() { function getData(){ // ... } })()
现在可以简化为:
(function init() { function getData(){ // ... } })()
公平地说,没有人真正这样写 UMD。他们使用 umdify 等工具来生成该代码。但通过内置 ES 模块,我们可以跳过构建步骤并获得更小的包大小。
ES 模块是静态的,这意味着工具可以在编译时分析代码结构,以确定哪些代码正在使用,哪些没有。这允许进行树摇动,即从最终包中删除未使用的代码。
由于 CommonJS 和 AMD 模块是动态的(在运行时评估),因此这些系统的 Tree Shaking 效率要低得多,通常会产生更大的包。
使用 CommonJS 导入模块时,指定文件扩展名是可选的。
const math = require("./math"); function subtract(a,b) { return math.add(a,-b); } module.exports = { subtract: subtract }
但是数学到底指的是什么?它是一个 JavaScript 文件吗? JSON 文件?数学目录中的index.js 文件?
当使用 ES Lint、Typescript 或 Prettier 等静态分析工具时,每个需求都变成了猜谜游戏。
是 math.js 吗?
是 math.jsx 吗?
是 math.cjs 吗?
是 math.mjs 吗?
是 math.ts 吗?
是 math.tsx 吗?
是 math.mts 吗?
是 math.cts 吗?
是 math/index.js 吗?
是 math/index.jsx 吗?
你明白了。
读取文件的成本很高。它的性能比从内存中读取要低得多。导入 math/index.js 会导致 9 次 IO 操作,而不是 1 次!这种猜谜游戏正在减慢我们的工具速度并损害开发人员的体验。
在 ES 模块中,我们通过强制文件扩展名来避免这种混乱。
与 CommonJS 同步加载模块(阻塞整个进程直到模块加载)不同,ES 模块是异步的。这允许 JavaScript 在后台加载模块时继续执行,从而提高性能 - 特别是在 Node.js 这样的环境中。
尽管有明显的好处,但采用 ES 模块并不是一件简单的任务。这就是过渡花了这么长时间的原因:
从 CommonJS 切换到 ES 模块并不是一个简单的改变,尤其是对于大型项目。语法差异加上对工具支持的需求,使得迁移变得非常困难。
node.js花了5年时间才完全支持ES模块。在此期间,开发人员必须保持与 CommonJS(在服务器上)和 ES 模块(在浏览器上)的兼容性。这种双重支持在生态系统中造成了很多摩擦。
即使 Node.js 添加了对 ES 模块的支持,CommonJS 模块也无法加载 ES 模块。虽然 ES 模块可以加载 CommonJS 模块,但这两个系统不能完全互操作,这给必须支持这两个系统的包作者带来了额外的麻烦。
JavaScript 模块的未来是光明的,以下是一些关键的发展,将使 ES 模块成为未来的主导系统:
在 Node.js 23 中,我们终于能够从 CommonJS 加载 ES 模块。
有一个小警告:使用顶级await的ES模块不能导入到CommonJS中,因为await只能在异步函数中使用,而CommonJS是同步的。
一个与 npm 竞争的新 javascript 包注册表。它比 npm 有很多优点,我在这里不再赘述。但有趣的是你只能上传 ES 模块包。无需支持旧标准。
结论虽然迁移到 ES 模块具有挑战性,特别是在 Node.js 支持和生态系统兼容性方面,但好处是不可否认的。随着 Node.js 23 改进互操作性以及 JSR 等新工具促进统一模块系统,ES 模块将成为 JavaScript 的默认模块。
随着我们继续拥抱 ES 模块,我们可以期待更干净、更快、更可维护的代码,标志着 JavaScript 开发模块化的新时代。
以上是ES 模块简史的详细内容。更多信息请关注PHP中文网其他相关文章!