Node.js 支持 ES6 模块的进展

简介: 本文讲的是Node.js 支持 ES6 模块的进展,如果你没有读过我之前的文章,在继续阅读之前,建议你看一下,里面描述了两种模块系统架构一些重大差异。简单来说:CommonJS 和 ES6 模块的根本差异在于模块结构解析完全并能够在其他代码里使用的时机。
本文讲的是Node.js 支持 ES6 模块的进展,

几个月前我写了篇文章阐述 Node.js 现有的 CommonJS 模块系统与 ES6 模块系统的一些区别,以及由此产生的在 Node.js 中实现 ES6 模块系统的挑战。本文将跟进相关进展。

何时知晓

如果你没有读过我之前的文章,在继续阅读之前,建议你看一下,里面描述了两种模块系统架构一些重大差异。简单来说:CommonJS 和 ES6 模块的根本差异在于模块结构解析完全并能够在其他代码里使用的时机。

例如,有如下简单的 CommonJS 模块,姑且称为 foobar 模块。

function foo() {
  return 'bar';
}
function bar() {
  return 'foo';
}
module.exports.foo = foo;
module.exports.bar = bar;

现在我们用一个 app.js 文件引用此模块:

const {foo, bar} = require('foobar');
console.log(foo(), bar());

在命令行里执行 node app.js 时,Node.js 程序读取并开始解析 app.js 文件的内容,执行代码。执行期间 require() 函数被调用,同步地读取 foobar.js 内容载入内存,同步解析和编译 JavaScript 代码,而后同步执行代码,将 module.exports的返回值 app.js 中 require('foobar') 的值。app.js 中的 require() 函数执行完后,foobar 模块的结构已知并能被调用。以上所有的一切全发生在 Node.js 事件循环的一次执行中。

理解 CommonJS 和 ES6 差异的重要一点,在于 CommonJS 模块的结构(模块的 API)在模块代码执行完之前是未知的,即便在执行完后,其结构也随时能被其他代码更改。

以下是在 ES6 语法下的“等效”模块:

export function foo() {
  return 'bar';
}

export function bar() {
  return 'foo';
}

调用的代码如下:

import {foo, bar} from 'foobar';
console.log(foo());
console.log(bar());

根据 ECMAScript 标准,ES6 模块与 CommonJS 实现步骤有很大差异。第一步,从硬盘读取文件内容的步骤大体相同,不过有可能是异步的。得到内容后进行解析时,决定�模块结构的 export 声明,优先 于代码的执行。在结构定义完了以后,才执行代码。很重要的一点是,所有的 import 和 export 声明指向的目标在代码执行前就都确定了。还有一点,ES6 标准允许分解的步骤异步进行。对 Node.js 来说,意味着读取脚本内容、解析模块引用关系、执行模块代码这些步骤可以在事件循环中轮番进行。

时机决定一切

我们实现 ES6 模块标准时的一个主要目标,是尽可能地无缝切换。我们希望能够同时兼容两种标准,且对使用者隐藏两种标准细节的差别,例如 require('es6-module') 和 import from 'commonjs-module' 都能正常工作。

不幸的是,事情没那么简单。

由于 ES6 模块的读取和解析都是异步的,这就不可能 require() 一个 ES6 模块,因为 require() 是个同步的函数。若通过改变 require() 函数的语义去支持异步加载,会让整个社区闹得鸡犬不宁。因此我们考虑过写一个 require.import() 作为 ES6 提议的 import() 函数实现。该函数返回一个 Promise 对象, 其于 ES6 模块载入完成时解决 (resolve) 。虽说这不是最理想的方案,但起码能在现有的 CommonJS 风格 Node.js 代码中使用 ES6 模块。

一个小小好消息是,在 ES6 模块中通过 import 声明使用 CommonJS 模块应该变得很容易。因为并不总强制异步加载。为更好支持此点,ECMAScript 语言标准将会有一些更改,不过当一切稳定下来后,肯定是能正常使用的。

但是可能有个巨大的坑……

哎妈呀,可怜的具名引入

具名引入 (named import) 是 ES6 模块系统的重要功能,如下例:

import {foo, bar} from 'foobar';

从 foobar 模块中引入 foo 和 bar 变量,这些发生在模块解析阶段 - 在任何实际代码执行之前。因为在 ES6 中模块结构是预先定义的。

然而在 CommonJS 中,模块结构在代码执行完之前是未定的。意味着若不大改 ECMAScript 语言标准,不可能具名引入一个 CommonJS 模块的内容。无奈之下,开发者不得不使用 ES6 模块中的 'default' 暴露声明。例如,使用本文开头的 CommonJS 模块样例代码,引入的代码要这样写:

import foobar from 'foobar';
console.log(foobar.foo(), foobar.bar());

与之前有微小但及其重要的差别。使用 import 声明来引入 CommonJS 模块的时候,没法使用如下写法来将 foo 和 bar 指向 CommonJS 模块暴露的 foo() 和 bar() 函数。

import {foo, bar} from 'foobar';

但在 Babel 中还是可以用的

正在使用像 Babel 之类的转译工具的人,使用 ES6 模块语法时多半熟悉其具名引入特性。Babel 将 ES6 代码转换为能在 Node.js 中使用的 CommonJS 风格代码。语法和 ES6 似乎一样,但具体实现却不是。理解这点很重要:Babel 处理 ES6 具名引入的方式和完整遵循 ES6 标准要求的实现根本不是一回事。

Michael Jackson Script

CommonJS 和 ES6 模块的另一个根本区别在于,ECMAScript 编译器在载入模块之前需要知道它属于哪种模块系统。因为 ES6 模块中的 export 和 import 声明需要在运行代码之前解析。

这就要求 Node.js 要有预先探知文件模块类型的机制。在多种方案中挣扎后,我们觉得闹心程度最低的是引入一个新的 *.mjs扩展名(过去曾被我们深情地称为 'Michael Jackson Script')来显式标记该 JavaScript 文件使用 ES6 模块标准处理。

换句话说,对待两个文件 foo.js 和 bar.mjs,使用 import * from 'foo' 会将 foo.js 当作 CommonJS 模块引入,而使用 import * from 'bar' 会将 bar.mjs 当作 ES6 模块。

时间规划

目前,在 ES6 和虚拟机层面的标准和实现还需要许多更改,Node.js 才能开始尝试支持 ES6 模块。相关工作已经开始,但离完成还要等一段时间,我们估计至少要一年。





原文发布时间为:2017年3月03日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。
目录
相关文章
|
20天前
|
存储 JavaScript 索引
js开发:请解释什么是ES6的Map和Set,以及它们与普通对象和数组的区别。
ES6引入了Map和Set数据结构。Map的键可以是任意类型且有序,与对象的字符串或符号键不同;Set存储唯一值,无重复。两者皆可迭代,支持for...of循环。Map有get、set、has、delete等方法,Set有add、delete、has方法。示例展示了Map和Set的基本操作。
23 3
|
2月前
|
缓存 JavaScript 数据安全/隐私保护
js开发:请解释什么是ES6的Proxy,以及它的用途。
`ES6`的`Proxy`对象用于创建一个代理,能拦截并自定义目标对象的访问和操作,应用于数据绑定、访问控制、函数调用的拦截与修改以及异步操作处理。
26 3
|
2月前
|
JavaScript
js开发:请解释什么是ES6的类(class),并说明它与传统构造函数的区别。
ES6的类提供了一种更简洁的面向对象编程方式,对比传统的构造函数,具有更好的可读性和可维护性。类使用`class`定义,`constructor`定义构造方法,`extends`实现继承,并可直接定义静态方法。示例展示了如何创建`Person`类、`Student`子类以及它们的方法调用。
23 2
|
4天前
|
JavaScript 前端开发 测试技术
编写JavaScript模块化代码主要涉及将代码分割成不同的文件或模块,每个模块负责处理特定的功能或任务
【5月更文挑战第10天】编写JavaScript模块化代码最佳实践:使用ES6模块或CommonJS(Node.js),组织逻辑相关模块,避免全局变量,封装细节。利用命名空间和目录结构,借助Webpack处理浏览器环境的模块。编写文档和注释,编写单元测试以确保代码质量。通过这些方法提升代码的可读性和可维护性。
9 3
|
14天前
|
消息中间件 监控 JavaScript
Node.js中的进程管理:child_process模块与进程管理
【4月更文挑战第30天】Node.js的`child_process`模块用于创建子进程,支持执行系统命令、运行脚本和进程间通信。主要方法包括:`exec`(执行命令,适合简单任务)、`execFile`(安全执行文件)、`spawn`(实时通信,处理大量数据)和`fork`(创建Node.js子进程,支持IPC)。有效的进程管理策略涉及限制并发进程、处理错误和退出事件、使用流通信、谨慎使用IPC以及监控和日志记录,以确保应用的稳定性和性能。
|
15天前
|
缓存 JavaScript 前端开发
Node.js的模块系统:CommonJS模块系统的使用
【4月更文挑战第29天】Node.js采用CommonJS作为模块系统,每个文件视为独立模块,通过`module.exports`导出和`require`引入实现依赖。模块有独立作用域,保证封装性,防止命名冲突。引入的模块会被缓存,提高加载效率并确保一致性。利用CommonJS,开发者能编写更模块化、可维护的代码。
|
20天前
|
JavaScript 前端开发
js开发:请解释什么是ES6的Generator函数,以及它的用途。
ES6的Generator函数是暂停/恢复功能的特殊函数,利用yield返回多个值,适用于异步编程和流处理,解决了回调地狱问题。例如,一个简单的Generator函数可以这样表示: ```javascript function* generator() { yield 'Hello'; yield 'World'; } ``` 创建实例后,通过`.next()`逐次输出"Hello"和"World",展示其暂停和恢复的特性。
20 0
|
29天前
|
JavaScript API
node.js之模块系统
node.js之模块系统
|
1月前
|
JavaScript 前端开发
JavaScript高级主题:什么是 ES6 的解构赋值?
【4月更文挑战第13天】ES6的解构赋值语法简化了从数组和对象中提取值的过程,提高代码可读性。例如,可以从数组`[1, 2, 3]`中分别赋值给`a`, `b`, `c`,或者从对象`{x: 1, y: 2, z: 3}`中提取属性值给同名变量。
18 6
|
1月前
|
Web App开发 JavaScript 前端开发
【Node系列】node核心模块util
Node.js的核心模块util为开发者提供了一些常用的实用工具函数。这些函数能够很方便地进行对象的继承、类型判断以及其他工具函数的实现。
22 2