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

Stella981
• 阅读 803

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

往期精彩回顾

Node.js 包管理器 NPM 讲解

“3N 兄弟” 助您完成 Node.js 环境搭建

I/O 模型如何演进及 I/O 多路复用是什么?

Node.js 版本知多少?又该如何选择?

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

我就知道你在看!

本文分享自微信公众号 - Nodejs技术栈(NodejsRoadmap)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这