Node.js 12中新的ECMAScript模块支持(译)

3,463 阅读6分钟

本文于2019-04-23由Dr. Axel博士发表在个人网站2ality,全文对Node.js12中模块的新特性做了详细的分析解读。

Node.js 12(于2019-04-23发布)为ECMAScript模块带来了改进的支持。它实施了去年年底发布的计划的第二阶段。目前,这种支持可以在experimental-modules获得。

继续阅读以了解这种对ECMAScript模块的新支持是如何工作的。

简要说明:文件扩展名.mjs更方便,但.js也可以为ES模块启用。

1.本篇文章中使用的术语和缩写

  • CommonJS模块(CJS):指的是原始的Node.js模块标准。
  • ECMAScript模块(ES模块,ESM):指通过ECMAScript规范标准化的模块。
  • package.prop是指prop的package.json。

2.模块说明符

模块说明符是标识模块的字符串。它们在CommonJS模块和ES模块中的工作方式略有不同。在我们查看差异之前,我们需要了解模块说明符的不同类别。

2.1 模块说明符的类别

在ES模块中,我们区分以下类别的说明符。这些类别源自CommonJS模块。

  • 相对路径:以点开头。例:
./some/other/module.mjs
../../lib/counter.mjs
  • 绝对路径:以斜杠开头。例:
/home/jane/file-tools.mjs
  • URL:包括协议(从技术上讲,路径也是URL)。例:
'https://example.com/some-module.mjs'
'file:///home/john/tmp/main.mjs'
  • 裸路径:不以点,斜杠或协议开头,并且由没有扩展名的单个文件名组成。例子:
'lodash'
'the-package'
  • 深度导入路径:以裸路径开始,至少有一个斜杠。例:
'the-package/dist/the-module.mjs'

2.2 CommonJS模块说明符

这是CommonJS处理模块说明符的方式:

  • CommonJS不支持URL作为说明符。
  • 相对路径和绝对路径按预期处理。
  • 您可以将目录foo作为模块加载:
    • 如果有文件 foo/index.js
    • 如果存在foo/package.json其属性"main"指向模块文件的文件。
  • 根据node_modules找到的目录解析裸路径和深度导入路径:
    • 与导入模块位于同一目录中
    • 在该目录的父级中
    • 其它。
  • 如果说明符X未引用文件,则系统会尝试使用说明符X.jsX.jsonX.node

此外,CommonJS模块可以访问两个特殊的模块全局变量:

  • __filename:包含当前模块的路径。
  • __dirname:包含当前模块的父目录的路径。

本节的来源:Node.js文档的“模块”页面。

2.3 Node.js中的ES模块说明符

  • 除裸路径外,所有说明符都必须引用实际文件。与CommonJS相比,ESM不会添加缺少的文件扩展名。
  • 仅file:支持URL说明符的协议。
  • 目前不支持绝对路径。作为解决方法,您可以使用以file:///。开头的URL 。
  • 相对路径在Web浏览器中被解析 - 相对于当前模块的路径。
  • 裸路径相对于node_modules目录进行解析。裸路径引用的模块通过package.main(类似于CJS)指定。
  • 深度导入路径也相对于node_modules目录进行解析。
  • 不支持导入目录。换句话说,既不能使用package.main(只能用于packages)也不能通过index.*工作。

所有内置的Node.js模块都可以通过裸路径获得,并命名为ESM导出。例如:

import * as path from 'path';
import * as assert from 'assert';

assert.equal(
  path.join('a/b/c', '../d'), 'a/b/d');

2.4 文件扩展名

Node.js支持以下默认文件扩展名:

  • mjs 用于ES模块

  • cjs 用于CommonJS模块 文件扩展名.js代表ESM或CommonJS。它是哪一个,取决于package.type,它有两个设置:

  • commonjs(默认值):扩展名为.js或没有扩展名的文件被解析为CommonJS。

    "type": "commonjs"
    
  • module:具有扩展名.js或没有扩展名的文件被解析为ESM。

    "type": "module"
    

要查找package.json给定文件,Node.js将在与文件,父目录等相同的目录中进行搜索。

2.5将非文件源代码解释为CommonJS或ESM

并非所有Node.js执行的源代码都来自文件。你也可以通过stdin --eval和它发送代码--print。命令行选项--input-type允许您指定如何解释此类代码:

  • 作为CommonJS(默认): --input-type=commonjs
  • 作为ESM: --input-type=module

3. 互操作能力

3.1 从ESM导入CommonJS

目前,有两种从ES模块导入CommonJS模块的选项。

考虑以下CommonJS模块。

// common.cjs
module.exports = {
  foo: 123,
};

第一个选项是默认导入它(将来可能会添加对命名导入的支持):

// es1.mjs
import * as assert from 'assert';

import common from './common.cjs'; // default import
assert.equal(common.foo, 123);

第二种选择是使用createRequire():

// es2.mjs
import * as assert from 'assert';

import {createRequire} from 'module';
const require = createRequire(import.meta.url);

const common = require('./common.cjs');
assert.equal(common.foo, 123);

3.2 从CommonJS导入ESM

如果要从CommonJS模块导入ES模块,可以使用该import()运算符。

例如,采用以下ES模块:

// es.mjs
export const bar = 'abc';

这里我们从CommonJS模块导入它:

// common.cjs
const assert = require('assert');

async function main() {
  const es = await import('./es.mjs');
  assert.equal(es.bar, 'abc');
}
main();

4. 各种其他功能

4.1 import.meta.url

鉴于 __filename和 __dirname在ES模块不可用,我们需要一个替代方案。import.meta.url是另一种选择。它包含file:具有绝对路径的URL。例如:

'file:///Users/rauschma/my-module.mjs'

注意:url.fileURLToPath()用于提取路径 - new URL().pathname并不是总可以正常运行:

import * as assert from 'assert';
import {fileURLToPath} from 'url';

//::::: Unix :::::

const urlStr1 = 'file:///tmp/with%20space.txt';
assert.equal(
  new URL(urlStr1).pathname, '/tmp/with%20space.txt');
assert.equal(
  fileURLToPath(urlStr1), '/tmp/with space.txt');

const urlStr2 = 'file:///home/thor/Mj%C3%B6lnir.txt';
assert.equal(
  new URL(urlStr2).pathname, '/home/thor/Mj%C3%B6lnir.txt');
assert.equal(
  fileURLToPath(urlStr2), '/home/thor/Mjölnir.txt');

//::::: Windows :::::

const urlStr3 = 'file:///C:/dir/';
assert.equal(
  new URL(urlStr3).pathname, '/C:/dir/');
assert.equal(
  fileURLToPath(urlStr3), 'C:\\dir\\');

下一节演示使用import.meta.url和url.fileURLToPath()。

反过来想url.fileURLToPath()是url.pathToFileURL():它将路径转换为文件URL。

4.2 fs.promises

fs.promises包含fsAPI 的promisified版本并按预期工作

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();

4.3 --experimental-json-modules

使用该标志--experimental-json-modules,Node.js将.json文件加载为JSON。

以JSON文件为例data.json:

{
  "first": "Jane",
  "last": "Doe"
}

可以从ES模块导入它,如下所示(如果同时使用ESM和JSON模块的标志):

import * as assert from 'assert';
import data from './data.json';

assert.deepEqual(
  data,
  {first: "Jane", last: "Doe"});

5.npm上的ES模块

目前,npm上的ES模块引用有以下两种方式可以选择:

  • require('mylib')
  • import from 'mylib

你不能两者兼顾(深度导入路径是合理的解决方法)。我们正在努力改变这一状况。它可能会通过package.main更强大的功能来完成。

在该功能准备就绪之前,对处理该功能的人员有以下要求:

“在解决之前,请不要发布任何供Node.js使用的ES模块包。”

6.在Node.js上使用ES模块

从Node.js 12开始,Node.js上使用ES模块有以下选项:

  • library:ESM由John-David Dalton维护。esm还支持旧版本的Node.js.

  • Flag--experimental-modules

当Node.js 12达到LTS状态时,可能会在2019年10月删除ESM支持的标志。

最后欢迎大家关注公众号前端小苑,我会定期在这里发表原创文章。