专栏首页Nodejs技术栈ECMAScript Modules 在 Node.js 中的支持与使用

ECMAScript Modules 在 Node.js 中的支持与使用

2019 年的 4 月份,Node.js 官方团队在发布 Node.js 12 时,也给我们带来了最新的 ECMAScript Modules 支持。

首先我们需要明确的是,ECMAScript Modules 在现在已经不是什么新鲜事了。 早在 ES6 规范推出时,我们通过 Babel/TypeScript 等工具便已能在项目中使用该 Feature,那为什么我们还需要关注该 Feature 在 Node.js 上的实现与具体使用呢?

答案是明确的,因为 ECMAScript Modules 在 Node.js 规范中的实现与使用,实际上与现今 Babel/TypeScript 的使用是有较大的区别的。

关于这一点,我想从 Babel/TypeScript 当时的设计思路上去分析。

Babel/TypeScript 的设计思路

首先我们看一下 Babel/TypeScript 的 Slogan:

  • Babel:Babel is a JavaScript compiler:Use next generation JavaScript, today.
  • TypeScript:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

从两个产品的 Slogan 上不难看出,Babel 专注于通过编译,在现在的 JS 引擎中使用最新的 JS Feature。而 TS 则是通过编译,实现静态类型的校验等。

而这两者的最终产物都受限于当前 JS 引擎的能力,也就是说 Babel 和 TypeScript 并不能凭空模拟出之前 JS 引擎尚未支持的 Feature。

这一点非常重要,因为在 Babel 与 TypeScript 对 ECMAScript Modules 时,实际上是编译成 Node.js 所支持的 CommonJS 规范,从而使得最终产物可以在 Node.js 上运行。

也就是说,当时我们的使用方式,其实是遵循了 CommonJS 的规范的,只是写法上是 ECMAScript Modules 而已。且由于底层缺失对于 ECMAScript Modules 的强约束(因为还不存在),所以大家的写法上也都是五花八门的,只能最终编译成 CommonJS 时能运行即可。

而 Node.js 12 的这个 Feature,则对 ECMAScript Modules 的开发与使用做了强约束,所以在正式开始使用该规范前,我们还是需要对其有一定的了解的。

启用 Feature

通过 --experimental-modules ,我们可以在 Node.js 中启用该 Feature。

当设置该选项时,Node.js 便会以 ECMAScript Modules 的方式去解析 JS 并运行,在这儿值得注意的是,在新模式下,文件的后缀与解析规则也发生了变更。

在该 Feature 下,文件分为了以下几种后缀:

  • .mjs:ECMAScript Modules 模式,使用 import/export
  • .cjs:CommonJS 模式,也就是原有的 Node.js 模块解析方式
  • .js:在 ECMAScript Modules 模式下,如果 package.json 中 的 type 字段为 module 时(后文会提及),则该文件会被认为是符合 ES Module 规范的文件。

通过 package.json 区分模块类型

ECMAScript Modules 由于具体实现上与之前的 CommonJS 有较大区别,因此在使用时是需要对两种情况进行区分的。而官方则提供了一种简单有效的方式,那就是通过 package.json 中的 type 字段。

该字段主要影响该 package 下 .js 后缀的解析,而新增的 .cjs/.mjs 后缀则从文件名上已经做了类型区分,Node 会根据后缀切换不同的解析方式。

在规范中,package.json 的 type 字段一共有两种值,"module" 与 "commonjs",而当 type 字段不存在时则默认使用 "commonjs" 来适应原有规范。

// package.json
{
  "type": "module" | "commonjs"
}

而在具体使用时,当导入项目中的 js 时,根据 type 的值,会有以下两种情况:

  • module:以 ECMAScript Modules 模式解析
  • commonjs:以 commonjs 的方式解析

通过这种设计,我们可以非常方便的实现对原有代码的兼容,且 CommonJS 与 ES Modules 之间也能互相引用,Node.js 会处理好运行时的一切。

至于解析的例子,大家可以看下面的代码:

// package.json 中 "type" 为 "module".

// 以 ECMAScript Modules 解析与加载
import './startup/init.js';

// 以 CommonJS 加载,因为 ./node_modules/commonjs-package/package.json
// 缺乏 "type" 字段或者 "type" 为 "commonjs".
import 'commonjs-package';

// 以 CommonJS 加载,因为 ./node_modules/commonjs-package/package.json
// 缺乏 "type" 字段或者 "type" 为 "commonjs".
import './node_modules/commonjs-package/index.js';

// 以 CommonJS 加载,因为 .cjs 后缀即代表该模块遵循 CommonJS 规范.
import './legacy-file.cjs';

// 以 ECMAScript Modules 加载,因为 .mjs 后缀即代表该模块遵循 ECMAScript Modules 规范.
import 'commonjs-package/src/index.mjs';

ES Modules 与 CommonJS 的区别

在 Node.js 的实现中,ES Modules 实际上与 CommonJS 的规范在部分细节上已有了较大的区别。这部分差异直接影响到我们书写代码的方式,因此我会具体贴出部分重要改动。

导入模块时需要提供文件拓展名

在 CommonJS 时代,我们在导入模块时无需书写文件后缀,而是由 Node.js 自行通过 extensions 来加载指定文件。如 import 'index' 在 Node.js 中实际上会加载 index.js,Node 会帮忙自动尝试加载该文江。 而在 ES Modules 规范下,导入一个模块时,我们需要提供确切的文件拓展名。

这一点虽然对比现在的方案缺失了灵活性,但却使得整体模块的依赖关系可以在编译时就确定,而不需要等到运行时。这是符合 ES Modules 的设计初衷的。

require, exports, module.exports, __filename, __dirname

Node 在实现 CommonJS 规范时,实际上给每一个文件都做了包裹,传入了以上的这些变量,从而使得在代码中可以使用 require/exports 等方式实现模块化。

在 ES Modules 规范下,这些都将不复存在。这一点对于原有的代码而言,是一个非常大的变更。 这也就是为什么在 Babel/TypeScript 等工具体系下,明明可以使用 ES Modules 进行开发了,还需要关注 Node 具体实现的原因,因为之前的代码强依赖于这些变量,在新规范下必须进行修改才能继续使用。

然而这些都是 Node 运行的基础,总不能一下子就没有了吧?答案是确定的,这些变量在 ES Modules 规范下的使用方式,Node 官方也给出了具体的方案:

比如 require,可以通过 module.createRequire() 方法使用。

又比如 __filename 与 __dirname:

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

import.meta

这儿其实有一个小知识,那就是关于 import.meta 的。 在 MDN 的文档中,解释如下:

import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。

console.log(import.meta);

// 输出
{ url: "file:///home/user/my-module.mjs" }

没有 require.extensions 与 require.cache

在 ES Modules 规范中,require.extensions 与 require.cache 将不再被使用。

基于 URL 的文件路径

在文件路径上,ES Modules 的解析与缓存是基于 URL 规范的。 这也就意味着,模块实际上是可以携带查询参数的,且当查询参数不同时,Node 会重新加载该模块。

import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"

总结

在此需要特别提及的是,目前 Node.js 所提供的 ECMAScript Modules 规范并非是最终解,其具体实现与诸多技术细节未来也可能进行一定的调整。 比如关于 CommonJS 与 ES Modules 的互相调用,实际上是还没有完全确定下来的(因此我这儿也没有特别去阐述如何使用)。也因此在 Node.js 的文档中,ECMAScript Modules 规范的稳定性等级还是 1,属于 Experimental 。

而个人对于 ECMAScript Modules 规范态度,总体是看好的。强有力的约束有利于 Node.js 去做更多的优化,统一的模块规范则避免了浏览器与 Node.js 生态的进一步割裂。虽然过程是曲折,但前途却充满了光明。

参考文档

  • ECMAScript Modules - Node.js 官方文档
  • Plan for New Modules Implementation - Node.js 规划
  • The new ECMAScript module support in Node.js 12 - 2ality
  • import.meta - MDN

本文分享自微信公众号 - Nodejs技术栈(NodejsDeveloper)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 干货 | 携程机票Node.js开发实践

    付文平,携程机票研发部前端开发总监。2011年加入携程,主要负责携程机票PC、H5、Hybrid业务方面的开发工作。先后负责机票PC前后端分离,H5 Swift...

    五月君
  • 你需要了解的有关 Node.js 的所有信息

    Node.js 是当前用来构建可扩展的、高效的 REST API's 的最流行的技术之一。它还可以用来构建混合移动应用、桌面应用甚至用于物联网领域。

    五月君
  • Java 能抵挡住 JavaScript 的进攻吗?

    德高望重的IO大臣颤悠悠地走上前来:“启禀陛下,昨日收到战报,有个叫做Node.js的番邦又一次向我国进攻,我边防将士死伤惨重。”

    五月君
  • 密度散点图-colorbar

    在做精度对比的时候,密度散点图作用很大,特别的数据量大、精度高、相关系数高等情况出现的时候,很容易产生密集散点在聚集的热点,这个热点内的点数无法通过肉眼直观的了...

    一个有趣的灵魂W
  • 如何在Kerberos环境下使用Haproxy实现HiveServer2负载均衡

    前面Fayson介绍了《如何使用HAProxy实现HiveServer2负载均衡》,本文主要介绍如何使用HAProxy实现Kerberos环境下HiveServ...

    Fayson
  • 在spring中使用自定义注解注册监听器

    接口回调 监听器本质上就是利用回调机制,在某个动作发生前或后,执行我们自己的一些代码。在Java语言中,可以使用接口来实现。 实现一个监听器案例 为了方便,直接...

    yawn
  • 论文阅读: Light-head R-CNN

    “ Head ” 在文中指连接在basemodel后面的网络结构,包括以下两部分:

    JNingWei
  • 海淘党必备!这 4 款小程序,告诉你去哪花钱最划算

    以前每次要查汇率的时候,都很头大。只知道 1 美元大概换 6.xx 人民币,却不知道具体数额,只得老老实实用百度查。

    知晓君
  • flask 通过flask-script生成指令(flask 36)

    from flask import Flask from flask_bootstrap import Bootstrap from flask_momen...

    用户5760343
  • 组策略限制3389登录的绕过方式

    要登录到这台远程计算机,您必须被授予允许通过终端服务登录的权限。默认地,“远程桌面用户”组的成员拥有该权限,如果您不是“远程桌面用户”组或其他拥有该权限的组的成...

    潇湘信安

扫码关注云+社区

领取腾讯云代金券