在现代软件开发中,模块将软件代码组织成独立的块,这些块共同构成了更大、更复杂的应用程序。
在浏览器 JavaScript 生态系统中,JavaScript 模块的使用依赖于import
和export
语句;这些语句分别加载和导出 EMCAScript 模块(或 ES 模块)。
ES 模块格式是打包 JavaScript 代码以供重用的官方标准格式,大多数现代 Web 浏览器都原生支持这些模块。
然而,Node.js 默认支持 CommonJS 模块格式。 CommonJS 模块使用require()
加载,变量和函数使用module.exports
从 CommonJS 模块导出。
随着 JavaScript 模块系统的标准化,ES 模块格式在 Node.js v8.5.0 中被引入。作为一个实验模块,—experimental-modules
标志是在 Node.js 环境中成功运行 ES 模块所必需的。
但是,从 13.2.0 版本开始,Node.js 已经稳定支持 ES 模块。
本文不会过多地介绍这两种模块格式的用法,而是介绍 CommonJS 与 ES 模块的比较以及为什么你可能想要使用其中一种。
比较 CommonJS 模块和 ES 模块语法
默认情况下,Node.js 将 JavaScript 代码视为 CommonJS 模块。正因为如此,CommonJS 模块的特点是模块导入的require()
语句和模块导出的module.exports
语句。
例如,这是一个导出两个函数的 CommonJS 模块:
// util.js
module.exports.add = function(a, b) {
return a + b;
}
module.exports.subtract = function(a, b) {
return a - b;
}
我们还可以使用require()
将公共函数导入到另一个 Node.js 脚本中,就像我们在这里所做的那样:
const { add, subtract } = require('./util');
console.log(add(5, 5)); // 10
console.log(subtract(10, 5)); // 5
如果你正在寻找关于 CommonJS 模块的更深入的教程,请阅读其他相关文章。
另一方面,库作者也可以通过将文件扩展名从.js
更改为.mjs
来简单地在 Node.js 包中启用 ES 模块。
例如,这是一个简单的 ES 模块(带有 .mjs 扩展名)导出两个函数供公共使用:
// util.mjs
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
然后我们可以使用import
语句导入这两个函数:
// app.mjs
import { add, subtract } from './util.mjs';
console.log(add(5, 5)); // 10
console.log(subtract(10, 5)); // 5
在项目中启用 ES 模块的另一种方法是在最近的package.json
文件(与你正在制作的包相同的文件夹)中添加一个"type": "module"
字段:
{
"name": "my-library",
"version": "1.0.0",
"type": "module",
// ...
}
有了这个包含,Node.js 将该包内的所有文件都视为 ES 模块,你不必将文件更改为.mjs
扩展名。
或者,你可以安装并设置一个像 Babel 这样的转译器来将你的 ES 模块语法编译成 CommonJS 语法。 React 和 Vue 等项目支持 ES 模块,因为它们在后台使用 Babel 来编译代码。
在 Node.js 中使用 ES 模块和 CommonJS 模块的优缺点
ES 模块是 JavaScript 的标准,而 CommonJS 是 Node.js 中的默认
ES 模块格式的创建是为了标准化 JavaScript 模块系统。它已成为封装 JavaScript 代码以供重用的标准格式。
另一方面,CommonJS 模块系统内置于 Node.js 中。在 Node.js 中引入 ES 模块之前,CommonJS 是 Node.js 模块的标准。因此,有大量使用 CommonJS 编写的 Node.js 库和模块。
对于浏览器支持,所有主流浏览器都支持 ES 模块语法,你可以在 React 和 Vue.js 等框架中使用import
/export
。这些框架使用像 Babel 这样的转译器将import
/export
语法编译为require()
,旧的 Node.js 版本原生支持。
除了作为 JavaScript 模块的标准之外,ES 模块语法与require()
相比也更具可读性。由于语法相同,主要在客户端编写 JavaScript 的 Web 开发人员在使用 Node.js 模块时不会有任何问题。
Node.js 对 ES 模块的支持
⚠️ 提醒:旧的 Node.js 版本不支持 ES 模块
虽然 ES 模块已经成为 JavaScript 中的标准模块格式,但开发人员应该考虑到旧版本的 Node.js 缺乏支持(特别是 Node.js v9 及以下版本)。
换句话说,使用 ES 模块会使应用程序与仅支持 CommonJS 模块(即require()
语法)的早期版本的 Node.js 不兼容。
但是有了新的条件导出,我们可以构建双模式库。这些库由较新的 ES 模块组成,但它们也向后兼容旧 Node.js 版本支持的 CommonJS 模块格式。
换句话说,我们可以构建一个同时支持import
和require()
的库,从而解决不兼容的问题。
考虑以下 Node.js 项目:
my-node-library
├── lib/
│ ├── browser-lib.js (iife 格式)
│ ├── module-a.js (commonjs 格式)
│ ├── module-a.mjs (es6 module 格式)
│ └── private/
│ ├── module-b.js
│ └── module-b.mjs
├── package.json
└── …
在package.json
中,我们可以使用exports
字段以两种不同的模块格式导出公共模块(module-a
),同时限制对私有模块(module-b
)的访问:
// package.json
{
"name": "my-library",
"exports": {
".": {
"browser": {
"default": "./lib/browser-module.js"
}
},
"module-a": {
"import": "./lib/module-a.mjs"
"require": "./lib/module-a.js"
}
}
}
通过提供有关我们的my-library
包的以下信息,我们现在可以在任何支持它的地方使用它,如下所示:
// CommonJS
const moduleA = require('my-library/module-a')
// ES6 Module
import moduleA from 'my-library/module-a'
// 这里不会生效
const moduleA = require('my-library/lib/module-a')
import moduleA from 'my-awesome-lib/lib/public-module-a'
const moduleB = require('my-library/private/module-b')
import moduleB from 'my-library/private/module-b'
由于exports
中的路径,我们可以在不指定绝对路径的情况下import
(和require()
)我们的公共模块。
通过包含.js
和.mjs
的路径,我们可以解决不兼容的问题;我们可以为浏览器和 Node.js 等不同环境映射包模块,同时限制对私有模块的访问。
较新的 Node.js 版本完全支持 ES 模块
在大多数较低的 Node.js 版本中,ES 模块被标记为实验性的。这意味着该模块缺少一些功能并且落后于--experimental-modules
标志。较新版本的 Node.js 确实对 ES 模块提供了稳定的支持。
然而,重要的是要记住,对于 Node.js 将模块视为 ES 模块,必须发生以下情况之一:模块的文件扩展名必须从 .js
(对于 CommonJS)转换为.mjs
(对于 ES 模块)或者我们必须在最近的package.json
文件中设置一个{"type": "module"}
字段。
在这种情况下,该包中的所有代码都将被视为 ES 模块,并且应该使用import
/export
语句而不是require()
。
CommonJS 提供模块导入的灵活性
在 ES 模块中,只能在文件开头调用import
语句。在其他任何地方调用它会自动将表达式移动到文件开头,甚至可能引发错误。
另一方面,将require()
作为函数,在运行时进行解析。因此,可以在代码的任何位置调用require(
。你可以使用它从if
语句、条件循环和函数中有条件地或动态地加载模块。
例如,你可以在条件语句中调require()
,如下所示:
if(user.length > 0){
const userDetails = require('./userDetails.js');
// Do something ..
}
在这里,我们仅在用户存在时才加载名为 userDetails
的模块。
CommonJS 加载模块是同步的,ES 模块是异步的
使用require()
的限制之一是它会同步加载模块。这意味着模块被一个接一个地加载和处理。
正如你可能已经猜到的那样,这可能会给包含数百个模块的大型应用程序带来一些性能问题。在这种情况下, import
的性能可能优于require()
基于其异步行为。
然而,对于使用几个模块的小规模应用程序来说,require()
的同步特性可能不是什么大问题。
结语:CommonJS 还是 ES 模块?
对于仍在使用旧版本 Node.js 的开发人员来说,使用新的 ES 模块是不切实际的。
由于粗略的支持,将现有项目转换为 ES 模块会使应用程序与仅支持 CommonJS 模块(即require()
语法)的早期版本的 Node.js 不兼容。
因此,将你的项目迁移到使用 ES 模块可能不是特别有益。
作为初学者,了解 ES 模块可能是有益和方便的,因为它们正在成为用 JavaScript 为客户端(浏览器)和服务器端(Node.js)定义模块的标准格式。
对于新的 Node.js 项目,ES 模块提供了 CommonJS 的替代方案。 ES 模块格式确实提供了一种编写同构 JavaScript 的更简单途径,它可以在浏览器或服务器上运行。
总之,EMCAScript 模块是 JavaScript 的未来 🎉。