Node.js 12 支持最新 ECMAScript 模块

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.jsonX.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 '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": "commonjs"

寻找 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 '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 '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.urlurl.fileURLToPath()

url.fileURLToPath() 相反的是 url.pathToFileURL():它转换路径为 URL 文件路径。

fs.promises

fs.promises 包含 fs API 的 promise 版本,且按预期运行:

import { fileURLToPath } from 'url';
import { promises as fs } from 'fs';

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 状态的时候。

扩展阅读

本文作者 Axel Rauschmayer,转载请注明来源链接:

原文链接:https://2ality.com/2019/04/nodejs-esm-impl.html

本文链接:https://tie.pub/2019/07/nodejs-esm-impl/