Node.js 12 支持最新 ECMAScript 模块
Node.js 12(发布于 2019/04/23)带来 ECMAScript 模块的改进支持。这一支持在通常的 flag 后面添加 --experimental-modules
开启。
继续阅读了解 ECMAScript 模块新的支持是如何工作的。
简单说明:文件名扩展 .mjs
更方便,但是 ES 模块也允许 .js
。
这篇博客文章中使用的术语和简称
- CommonJS 模块(CJS): 最早适用于 Node.js 模块的标准。
- ECMAScript 模块(ES 模块, ESM): ECMAScript 规范提供的模块标准。
package.prop
意思是package.json
的属性prop
。
模块说明符
模块说明符是标识模块的字符串。它们在 CommonJS 模块和 ES 模块中的工作方式略有不同。在我们查看差异之前,我们需要了解模块说明符的不同类别。
模块说明符的分类
在 ES 模块中,我们把说明符分为下面的类别。这些分类起源于 CommonJS 模块。
- 相对路径:以小数点开始。举例:
'./some/other/module.mjs' '../../lib/counter.mjs'
- 绝对路径:以斜杠开始。举例:
'/home/jane/file-tools.mjs'
- URL:包含协议(从技术上说,路径也是 URLs)。举例:
'https://example.com/some-module.mjs' 'file:///home/john/tmp/main.mjs'
- 裸路径:不是以小数点、斜杠或协议开始,并且由不包含扩展名的单文件名组成。举例:
'lodash' 'the-package'
- 深入导入路径:以裸路径开始,且至少有一个斜杠。举例:
'the-package/dist/the-module.mjs'
CommonJS 模块说明符
CommonJS 怎么处理模块说明符:
- CommonJS 不提供 URLs 作为说明符。
- 相对路径和绝对路径按照预期处理。
- 可以加载一个目录
foo
作为模块:- 如果目录中存在文件
foo/index.js
- 如果目录存在文件
foo/package.json
,且它的属性 “main
” 指向一个模块文件。
- 如果目录中存在文件
- 裸路径和深导入路径的解析最近的目录
node_modules
:- 与导入模块相同的目录
- 在目录的父目录中
- 其它
- 如果说明符
X
没有指向一个文件,系统将尝试寻找X.js
,X.json
和X.node
。
另外,CommonJS 模块拥有两个可访问的全局模块变量:
__filename
: 包含当前模块的路径。__firname
: 包含当前模块的符目录。
本节的来源:Node.js 文档的模块页面。
Node.js 中的 ES 模块说明符
- 所有的说明符,除裸路径外,必须引用实际的地址。与 CommonJS 相比, ESM 不会添加缺少的文件扩展名。
file
是说明符唯一支持的协议。- 当前绝对路径不支持。一个方案是,你可以使用以
file:///
开头的 URL 路径。 - 相对路径在浏览器中解析 - 相对于当前模块的路径。
- 裸路径相对于与目录
node_modules
来解析。该模块引用裸路径,通过package.main
(类似于 CJS) 指定。 - 深导入路径也是相对于与目录
node_modules
来解析。 - 不支持导入目录。换句话说,既不支持
package.main
也不支持index.*
。
所有内置的 Node.js 模块允许通过裸路径和拥有名称的 ESM 导出。举例来说:
import * as path from 'node:path'
console.log(path.join('a/b/c', '../d') === 'a/b/d')
// output: true
文件扩展名
Node.js 支持下面的默认文件扩展名:
- ES 模块的
.mjs
- CommonJS 模块的
.cjs
文件扩展名 .js
在 ESM 或者 CommonJS 两者之中任一都有效。具体取决于 package.type
属性,有两种设置:
- CommonJS(默认情况):扩展名
.js
或者没有扩展名的文件被解析为 CommonJS。{ "type": "commonjs" }
- 模块:扩展名
.js
或者没有扩展名的文件被解析为 ESM。{ "type": "module" }
寻找 package.json
指定的文件,Node.js 搜索相同的目录作为文件,父目录,等等。
解析非文件源代码作为 CommonJS 或 ESM
不是所有被 Node.js 执行的源代码都来自文件。你也可以通过 --eval
和 --print
发送代码。命令行选项 --input-type
让你定义怎么样解析代码:
- 作为 CommonJS(默认情况):
--input-type=commonjs
- 作为 ESM:
--input-type=module
互操作性
在 ESM 中导入 CommonJS
在此时,你有两个选项从 ES 模块中导入 CommonJS 模块:
考虑下面的 CommonJS 模块:
// common.cjs
module.exports = {
foo: 123,
}
第一个选项是默认导入(未来可能添加命名导入):
// es1.mjs
import common from './common.cjs' // default import
console.log(common.foo) // output: 123
第二个选项是使用 createRequire():
// es2.mjs
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const common = require('./common.cjs')
console.log(common.foo) // output: 123
在 CommonJS 中导入 ESM
如果你想要在 CommonJS 模块中导入 ES 模块,你可以使用 import()
操作符。
使用下面的 ES 模块作为例子:
// es.mjs
export const bar = 'abc'
这里我们在 CommonJS 模块中导入它:
// common.cjs
async function main() {
const es = await import('./es.mjs')
console.log(es.bar) // output: 'abc'
}
main()
其它功能
import.meta.url
特定的 __filename
和 __dirname
在 ES 模块中不被允许,我们需要使用可替换的 import.meta.url
替换。它包含一个绝对路径的 URL 文件。举例来说:
'file:///Users/rauschma/my-module.mjs'
重要的是:使用 url.fileURLToPath()
获取路径 - new URL().pathname
属性不总是工作:
import { fileURLToPath } from 'node:url'
// Unix
const urlStr1 = 'file:///tmp/with%20space.txt'
console.log(new URL(urlStr1).pathname)
// output: '/tmp/with%20space.txt'
console.log(fileURLToPath(urlStr1))
// output: '/tmp/with space.txt'
const urlStr2 = 'file:///home/thor/Mj%C3%B6lnir.txt'
console.log(new URL(urlStr2).pathname)
// output: '/home/thor/Mj%C3%B6lnir.txt'
console.log(fileURLToPath(urlStr2))
// output: '/home/thor/Mjölnir.txt'
// Windows
const urlStr3 = 'file:///C:/dir/'
console.log(new URL(urlStr3).pathname)
// output: '/C:/dir/'
console.log(fileURLToPath(urlStr3))
// output: 'C:\\dir\\'
下一节示范使用 import.meta.url
和 url.fileURLToPath()
。
与 url.fileURLToPath()
相反的是 url.pathToFileURL()
:它转换路径为 URL 文件路径。
fs.promises
fs.promises
包含 fs
API 的 promise
版本,且按预期运行:
import { promises as fs } from 'node:fs'
import { fileURLToPath } from 'node:url'
async function main() {
// The path of the current module
const pathname = fileURLToPath(import.meta.url)
const str = await fs.readFile(pathname, { encoding: 'UTF-8' })
console.log(str)
}
main()
--experimental-json-modules
与 flag --experimental-json-modules
,Node.js 加载 .json
文件作为 JSON。
拿 JSON 文件 data.json
来举例:
{
"first": "Jane",
"last": "Doe"
}
我们能如同下面一样导入 ES 模块(如果我们使用该 flag 在 ESM 和 JSON 模块中):
import data from './data.json'
console.log(data)
// output: { first: "Jane", last: "Doe" }
npm 上的 ES 模块
在此刻,使用裸路径 ‘mylib’,你要在两者之间做决定:
- require(‘mylib’)
- import from ‘mylib’
你不能两者兼顾(深导入路径是合理的解决方法)。正在努力改变这一状况。它可能会通过 package.main
更强大的功能来完成。
在该功能准备就绪之前,处理该功能的人员有以下要求:
“在解决之前,请不要发布任何供 Node.js 使用的 ES 模块包。”
在 Node.js 中使用 ES 模块
在使用 Node.js 12 之前,你在 Node.js 中有以下选项来使用 ES 模块:
为了支持 ESM 的该 flag 可能在 2019 年十月被移除,当 Node.js 12 到达 LTS 状态的时候。
扩展阅读
- 三篇官方文档是重要的来源对于这篇博客文章:
- 更多关于脚本, CommonJS 模块 和 ES 模块的信息,查阅“JavaScript for impatient programmers”的章节 “Modules”
文章由 吳文俊 翻译,原文地址 The new ECMAScript module support in Node.js 12,转载请注明来源。