Node.js中的ES模块现状

LogicByte
• 阅读 4957
作者:Tobias Nießen

翻译:疯狂的技术宅

原文:https://jaxenter.com/es-modul...

未经允许严禁转载

几乎每种编程语言都能将组成程序的代码拆分为多个文件。 在 C 和 C++ 中 #include 指令就用于这个目的,而 Java 和 Python 有 import 关键字。 JavaScript 是迄今为止为数不多的例外之一,但新的 JavaScript 标准(ECMAScript 6)通过引入所谓的 ECMAScript 模块来改变这一点。所有主流浏览器都支持这个新标准 —— 只有 Node.js 似乎落后了。这是为什么?

新的 ECMAScript(ES)模块与以前的语言版本不完全兼容,因此使用的 JavaScript 引擎需要知道每一个文件是“旧” JavaScript 代码还是“新”模块。

例如在 ECMAScript 5 中引入的许多程序员首选的严格模式曾经是可选的,必须明确启用才行,同时它在 ES 模块中始终处于活动状态。因此,以下代码段在语法上可以解释为传统的 JavaScript 代码和 ES 模块:

a = 5;

作为经典的 Node.js 模块,这相当于 global.a = 5,因为未声明变量 a 并且未明确激活严格模式,因此 a 被视为全局变量。如果你尝试加载与 ES 模块相同的文件,则会收到错误 “ReferenceError:a is not defined”,因为未声明的变量可能无法在严格模式下使用。

浏览器通过<script> 标记的扩展解决了区别问题:没有 type 属性或带有 type="text/javascript" 属性的脚本仍然在传统模式下运行,而当脚本使用 type ="module" 属性时则作为模块处理。由于这种简单的分离,现在所有流行的浏览器都支持新的模块。 Node.js 中的实现要困难得多:2009年发明的 JavaScript 应用程序框架使用 CommonJS 标准模块,该标准基于 require 函数。此函数可以随时根据其相对于当前运行模块的路径加载另一个模块。新的 ES 模块也是由它们的路径定义的,但是 Node.js 是如何知道正在加载的模块是遗留的 CommonJS 还是 ES 模块的呢?仅仅基于语法是不够的,因为即使不使用新关键字的 ES 模块也不兼容CommonJS模块。

此外,ECMAScript 6 还提供了可以从 URL 加载模块,而 CommonJS 仅限于文件的相对和绝对路径。这种创新不仅使加载更复杂,而且可能更慢,因为 URL 不需要指向本地文件。特别是在浏览器中,脚本和模块通常通过HTTP网络协议加载。

CommonJS 允许通过 require 函数加载模块,该函数返回加载的模块。例如,CommonJS 模块可能如下所示:

const { readFile } = require('fs');
const myModule = require('./my-module');

这不是 ECMAScript 6 中的一个选项,因为在 require() 调用期间,模块在 HTTP 上加载时可能会长时间阻止整个程序的执行。相反,ES 模块提供了两种加载其他模块的方法。在大多数情况下,使用 import 是有意义的:

import { readFile } from 'fs';
import myModule from './my-module';

但是,这会不可避免地延迟模块的执行,直到加载 fs./my-module,但它们不会阻止其他模块的执行。当模块必须动态加载时,会变得更加复杂。 CommonJS 模块中看起来微不足道的东西变得越来越难以异步:

if (condition) {
  myOtherModule = require('./my-other-module');
}

ECMAScript 希望通过功能性使用 import 关键字来解决这个问题,该关键字异步加载模块并在每次调用时返回 Promise 对象。但缺点是程序员现在也负责错误处理,因为错误不会像在同步情况下那样自动传给调用者。

if (condition) {
  import('./my-other-module.js')
  .then(myOtherModule => {
    // Module was loaded successfully and can
    // now be used here.
  })
  .catch(err => {
    // An error occurred that needs to be handled here.
    console.error(err);
  });
}

如果使用 async 关键字声明了要加载模块的函数,由于 ECMAScript 6 中引入了 await 函数,import() 的使用更加清晰,并且错误处理被传递给同步执行中的调用者:

if (condition) {
  myOtherModule = await import('./my-other-module');
}

import 作为一个函数使用,它不是 ECMAScript 6 的一个组件,而是一个所谓的 Stage 3 提案,有可能会在下一个 JavaScript 版本中标准化。此外 Firefox、Chrome 和 Safari 等许多浏览器以及 Node.js 都支持它。

在Node.js中使用

区分 CommonJS 和 ES 模块的难度导致在 Node.js 下为 ES 模块引入了新的文件扩展名:如果已设置了 -experimental-modules 选项, Node.js 可以把以 .mjs 结尾的文件作为 ES 模块进行加载。从 2017 年 9 月发布的 Node.js 8.5.0 开始,如果将以下代码保存为 testmodule.mjs,则可以用 node -experimental-modules testmodule.mjs 命令执行它:

export function helloWorld(name) {
  console.log(`Hallo, ${name}!`);
}
  
helloWorld('javascript-conference.com');

Node.js 12 扩展了对 ES 模块的支持。重要的是,现在可以用 package.json 文件,它包含了诸如包的唯一名称之类的信息。现在使用的 JSON 格式扩展了一个名为 type 的新属性。可以选择将其更改为 commonjsmodule 以确定默认情况下应加载的包中所包含的 JavaScript 文件的模式。以下配置指定了一个包 example-package,它至少必须包含 ES 模块 index.js

{
  "name": "example-package",
  "type": "module",
  "main": "index.js"
}

像往常一样,“main” 字段指定哪个文件应该作为入口点。例如 index.js 模块可能如下所示:

import { userInfo } from 'os';
  
export function greet() {
  return `Hello ${userInfo().username}!`;
}

现在可以从其他文件加载此模块。包通常位于 node_modules 目录中各自的文件夹中。要加载刚创建的包,我们可以用以下目录结构和一个名为 main.js 的新文件:

- main.js
+ node_modules
  + example-package
    - package.json
    - index.js

main.js 文件可以引用传统的 CommonJS 或新的 ECMAScript 模块。在这两种情况下,example-package 都不能使用通常的 require() 调用加载,因为 ECMAScript 模块必须始终异步加载。因此 CommonJS 模块必须使用 import 加载 ES 模块:

import('example-package')
.then(package => {
  console.log(package.greet());
})
.catch(err => {
  console.error(err);
});

这样做的缺点是 CommonJS 模块不能像往常那样在开始时访问其他模块或软件包,但只能在事实和异步之后才能访问。执行如上所述脚本:node -experimental-modules main.js,如果入口点本身也是 ES 模块,则更容易。如果将 main.js 重命名为 main.mjs,则可以用 import

import { greet } from 'example-package';
console.log(greet());

因此,可以在一个应用程序中同时使用 CommonJS 和 ECMAScript 模块,但它有可能会引发混乱。因为 CommonJS 模块需要知道正在加载的模块是 CommonJS 还是 ES 模块,并且只能异步加载 ES 模块。这也适用于通过 npm 安装的软件包的加载。 fscrypto 等内置模块可以通过两种方式加载。

Node.js 中的差异

除了异步加载依赖项的问题之外,Node.js 中的旧模块和新模块之间还存在进一步的差异。特别是 ES 模块中不再提供 Node.js 的特定功能,如变量 __dirname__filenameexportmodule__dirname__filename 可以根据需要从新的 import.meta 对象重建:

import { fileURLToPath } from 'url';
import { dirname} from 'path';
  
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

变量 moduleexports 已被删除而无需替换;这同样适用于 module.filenamemodule.idmodule.parent 等属性。同样 require()require.main 不再可用。

虽然 CommonJS 中的循环依赖关系已经通过缓存各个模块的 module.exports 对象来解决,但 ECMAScript 6 用了所谓的绑定。简而言之,ES 模块不会导出和导入值,只是对值的引用。导入此类引用的模块可以访问该值,但无法修改它。已导出引用的模块可以为引用分配新值,该值将由从该点导入引用的其他模块使用。与之前的概念相比,这有着本质的区别,后者允许在任何时间点将属性分配给 CommonJS 模块的 module.exports 对象,从而使这些更改仅部分反映在其他模块中。

根据 ECMAScript 规范,import 默认情况下不会用文件扩展名完成文件路径,因为 Node.js 之前已经为 CommonJS 模块完成了,因此必须明确说明。同样当指定的路径是目录时,行为会发生变化:import'./directory' 不会在指定的文件夹中查找 index.js 文件,而是抛出一个错误,这是 Node.js 中的标准情况。两者都可以通过传递实验选项 -es-module-specifier-resolution = node 来改变。

结论

在最近发布的 Node.js 12.1.0 中,仍然需要通过 -experimental-modules 选项显式激活 ECMAScript 模块的使用,因为它是一个实验性功能。但是,开发人员的目标是在 Node.js 12 成为新的长期支持版本之前,在没有明确激活的情况下完成此功能并支持 ES 模块,预计将会在2019年10月完成。

现有的各种 CommonJS 模块使从 CommonJS 到 ECMAScript 模块的转换变得复杂。单个程序包无法切换到 ES 模块,从而不会发生与使用 require() 加载相应程序包的现有程序和程序包不兼容的情况。像 Babel 这样的工具可以将较新的语法转换为与旧环境兼容的代码,这使转换更容易。像 Deno 这样的新框架背弃了近年来多样化的模块化系统,完全依赖于 ECMAScript 模块,这对于把 JavaScript 作为编程语言的开发,标准化模块的引入是重要的一步,为未来的改进铺平了道路。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

Node.js中的ES模块现状

欢迎继续阅读本专栏其它高赞文章:


点赞
收藏
评论区
推荐文章
blmius blmius
3年前
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
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
3年前
AssemblyScript 入门指南[每日前端夜话0xEB]
每日前端夜话0xEB每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2459 字预计阅读时间:10分钟作者:DannyGuo翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/b880277c594152a503
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
Node.js 12中的ES模块[每日前端夜话0x9E]
每日前端夜话0x9E每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2552字预计阅读时间:10 分钟作者:BrianDeSousa翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/2ccaf94cecd3
Wesley13 Wesley13
3年前
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
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(