本文回顾了Node.js模块系统从CommonJS(cjs)到ECMAScript模块(esm)的演变,介绍了全局作用域的问题、Node.js通过cjs引入模块概念、npm包和打包工具的兴起、ESM模块的诞生及其特点,以及Node.js中cjs和esm的互操作性。文章还讨论了这一演变对开发者、行业和未来趋势的影响。
从全局作用域的问题引发,Node.js通过引入cjs解决了全局命名冲突,随后出现了npm包和打包工具。随着ESM规范的推广,出现了更灵活的模块系统。
cjs使用module.exports和require()导入模块,而esm使用export和import关键字进行成员的导入和导出,并引入了默认导出等新概念。esm还具有动态import()和顶层await等特性。
Node.js在12.x版本中添加了对esm的支持,并通过一些规则确保向后兼容cjs。Node.js允许使用不同的文件扩展名来区分cjs和esm模块,同时esm模块可以导入具有cjs和esm入口点的npm包,但反之存在一些限制。
ESM的广泛采用推动了JavaScript模块系统的标准化,促进了前端开发的进一步发展。随着esm的普及,未来可能会看到cjs逐渐被淘汰,而bundlers的作用将变得更加重要。
作者:@Marco Gonzalez
原文:https://dev.to/marcogrcr/nodejs-a-brief-history-of-cjs-bundlers-and-esm-2nlb
背景
Node.js 作为现代 Web 开发的核心技术之一,其模块系统经历了从 CommonJS(cjs)到 ECMAScript 模块(esm)的演变。本文通过回顾 Node.js 模块系统的历史,详细介绍了 cjs、esm 以及 bundlers 的发展过程,帮助开发者更好地理解这些概念及其在实际应用中的使用。
全局作用域的局限性
早期 JavaScript 仅有全局作用域,所有成员都在其中声明。这导致代码共享时容易出现冲突,因为不同文件可能对同一个成员使用相同的名称。
【早阅】Node.js 性能hooks和度量 API
例如,以下代码展示了全局作用域下的命名冲突:
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>冲突示例title>
head>
<body>
<script src="greet-1.js">script>
<script src="greet-2.js">script>
<script>
// TypeError: "greet" is not a function
greet();
script>
body>
html>
CommonJS 模块的出现
Node.js 通过 CommonJS (cjs) 正式引入了 JavaScript 模块的概念,解决了全局作用域的冲突问题。开发者可以通过 module.exports
决定导出哪些成员,并使用 require()
导入其他模块。
以下代码展示了 cjs 模块的使用:
// src/greet.js
// 此变量保持“私有”
const GREETING_PREFIX = " Hello ";
// 此函数将被导出
function greet(name) {
return `${GREETING_PREFIX} ${name}!`;
}
// `exports` 是 `module.exports` 的简写
exports.greet = greet;
// src/main.js
// 注意这裡没有 `.js` 后缀
const { greet } = require("./greet");
// 输出:Hello Alice!
console.log(greet("Alice"));
npm 包和打包工具的兴起
npm 包的出现,让开发者可以发布和使用可重用的 JavaScript 代码,极大地提高了开发效率。npm 包默认安装在 node_modules 文件夹中,package.json 文件通过 "main" 属性指定入口文件。
然而,cjs 不兼容 web 浏览器。为了解决这个问题,出现了打包工具,例如 browserify、webpack、parcel、rollup、esbuild 和 vite 等。打包器可以将入口文件及其所有依赖打包成一个兼容 web 浏览器的.js
文件。
【第3405期】了解npm audit以及修复漏洞
ECMAScript 模块的诞生
随著 Node.js 和 cjs 模块的普及,ECMAScript 规范维护者决定将模块概念纳入规范。因此,原生 JavaScript 模块也被称为 ESModules 或 esm。
esm 使用新的关键字和语法进行成员的导入和导出,并引入了默认导出等新概念。随着时间推移,esm 模块获得了动态 import()
和顶层 await 等新功能。
以下代码展示了 esm 模块的使用:
// src/greet.js
// 此变量保持“私有”
const GREETING_PREFIX = " Hello ";
// 此函数将被导出
export function greet(name) {
return `${GREETING_PREFIX} ${name}!`;
}
// src/part.js
// 默认导出:新概念
export default function part(name) {
return `Goodbye ${name}!`;
}
// src/main.js
// 注意这裡需要 `.js` 后缀
import part from "./part.js";
// 动态导入:新功能
// 顶层 await:新功能
const { greet } = await import("./greet.js");
// 输出:Hello Alice!
console.log(greet("Alice"));
// 输出:Bye Bob!
console.log(part("Bob"));
Node.js 中 cjs 和 esm 的互操作性
为满足需求,Node.js 在 12.x 版本中正式添加了对 esm 的支持。为确保向后兼容 cjs,Node.js 采用用以下规则:
除非 package.json 中 "type" 属性设置为 "module",否则 Node.js 将 .js 文件解释为 cjs 模块。
Node.js 将 .cjs 文件解释为 cjs 模块。
Node.js 将 .mjs 文件解释为 esm 模块。
esm 模块可以导入具有 cjs 和 esm 入口点的 npm 包。然而,反过来则存在一些限制。cjs 模块无法直接使用 require()
导入 esm 模块,因为 esm 模块允许顶层 await,而 require()
函数是同步的。可以使用动态 import()
导入 esm 模块,但它返回一个 Promise,需要使用异步函数处理。
为解决此兼容性问题,一些 npm 包通过利用 package.json 中的 "exports" 属性,提供 cjs 和 mjs 两种入口点。例如:
// node_modules/esm/package.json
{
"name": "esm",
"type": "module",
"main": "./entry.cjs",
"exports": {
"import": "./entry.js",
"require": "./entry.cjs"
}
}
"main" 属性指向 cjs 版本,以确保与不支持 "exports" 属性的 Node.js 版本兼容。
影响
对开发者的影响:随着 esm 的普及,开发者可以更灵活地使用模块系统,尤其是在现代前端框架和工具链中。esm 的动态导入和顶层 await 特性使得异步操作更加简洁和直观。
对行业的影响:esm 的广泛采用标志着 JavaScript 模块系统的标准化,推动了前端开发的进一步发展。bundlers 的不断进化也使得前端构建工具更加强大和高效。
未来趋势:随着 esm 的普及,cjs 可能会逐渐被淘汰,尤其是在新的项目中。未来,bundlers 可能会进一步优化 esm 的打包效率,减少打包后的文件体积。
结论
Node.js 的模块系统从 cjs 到 esm 的演变,反映了 JavaScript 生态的不断进步。esm 的引入为开发者提供了更灵活、更强大的模块系统,而 bundlers 的出现则解决了 cjs 模块在浏览器中的兼容性问题。未来,随着 esm 的进一步普及,cjs 可能会逐渐退出历史舞台,而 bundlers 将继续在前端开发中发挥重要作用。
😀 每天只需花五分钟即可阅读到的技术资讯,加入【早阅】共学,可联系 vx:zhgb_f2er
5 分钟新知:了解技术资讯的一种方式。
🚀可直接通过阅读原文了解详细内容。