ChatGPT解决这个技术问题 Extra ChatGPT

使用 Node.js 需要与 ES6 导入/导出

在我正在合作的一个项目中,我们有两种选择可以使用的模块系统:

使用 require 导入模块,使用 module.exports 和 exports.foo 导出。使用 ES6 import 导入模块,使用 ES6 export 导出

使用其中一种是否有任何性能优势?如果我们要使用 ES6 模块而不是 Node 模块,还有什么我们应该知道的吗?

node --experimental-modules index.mjs 允许您在没有 Babel 的情况下使用 import,并且可以在 Node 8.5.0+ 中使用。您还可以(并且应该)publish your npm packages as native ESModule,向后兼容旧的 require 方式。
无需使用 .mjs 文件,只需在 package.json 中使用 type: "module" 并在仅为项目文件导入时添加扩展名,仅此而已

N
Nico Schlömer

更新

从 Node v12(2019 年 4 月)开始,默认启用对 ES 模块的支持,从 Node v15(2020 年 10 月)开始,它是稳定的(参见 here)。包含节点模块的文件必须以 .mjs 结尾,或者最近的 package.json 文件必须包含 "type": "module"Node documentation 包含更多信息,还包括 CommonJS 和 ES 模块之间的互操作。

在性能方面,总是有可能新功能没有像现有功能那样优化。但是,由于模块文件只评估一次,性能方面可能会被忽略。最后,无论如何,您都必须运行基准测试才能获得明确的答案。

ES 模块可以通过 import() 函数动态加载。与 require 不同,它返回一个承诺。

上一个答案

使用其中一种是否有任何性能优势?

请记住,目前还没有原生支持 ES6 模块的 JavaScript 引擎。你自己说你正在使用 Babel。 Babel 无论如何都会默认将 importexport 声明转换为 CommonJS (require/module.exports)。因此,即使您使用 ES6 模块语法,如果您在 Node.js 中运行代码,您将在后台使用 CommonJS。

CommonJS 和 ES6 模块之间存在技术差异,例如 CommonJS 允许您动态加载模块。 ES6 不允许这样做,but there is an API in development for that

由于 ES6 模块是标准的一部分,我会使用它们。


@Entei:似乎您想要默认导出,而不是命名导出。 module.exports = ...; 等同于 export default ...exports.foo = ... 等价于 export var foo = ...
值得注意的是,即使 Babel 最终将 import 转换为 Node 中的 CommonJS,与 Webpack 2 / Rollup(以及任何其他允许 ES6 树摇动的捆绑器)一起使用,最终还是有可能得到一个比同等文件小得多的文件代码节点通过使用 require 来处理,因为事实上 ES6 允许对导入/导出进行静态分析。虽然这不会对 Node 产生影响(目前),但如果代码最终将作为单个浏览器包结束,它肯定会产生影响。
ES6 模块在最新的 V8 中,并且也出现在标志后面的其他浏览器中。请参阅:medium.com/dev-channel/…
@stackjlei 嵌入网站时?使用 npm 实际上并不意味着代码在 node 中执行,这要归功于 webpack 等模块捆绑器。
@Akhila:添加了更新。如果您认为这就足够了,或者我是否应该添加更多内容,请告诉我。谢谢你就这件事告诉我。
A
Amit

您可能需要考虑几种用法/功能:

要求:

您可以在加载的模块名称未预定义/静态的情况下进行动态加载,或者仅在“真正需要”时有条件地加载模块(取决于某些代码流)。

加载是同步的。这意味着如果您有多个需求,它们会被一一加载和处理。

ES6 导入:

您可以使用命名导入来选择性地仅加载您需要的部分。这样可以节省内存。

导入可以是异步的(在当前的 ES6 模块加载器中,实际上是异步的)并且可以执行得更好一些。

此外,Require 模块系统不是基于标准的。既然存在 ES6 模块,它就不太可能成为标准。将来,将在各种实现中对 ES6 模块提供原生支持,这将在性能方面具有优势。


是什么让您认为 ES6 导入是异步的?
@FelixKling - 各种观察的组合。使用 JSPM(ES6 模块加载器...)我注意到,当导入修改全局命名空间时,在其他导入中不会观察到效果(因为它们是异步发生的。这也可以在转译代码中看到)。此外,由于这是行为(1 导入不会影响其他行为),因此没有理由不这样做,因此它可能取决于实现
你提到了一些非常重要的东西:模块加载器。虽然 ES6 提供了导入和导出语法,但它并没有定义应该如何加载模块。重要的部分是声明是静态可分析的,因此可以在不执行代码的情况下确定依赖关系。这将允许模块加载器同步或异步加载模块。但是 ES6 模块本身并不是同步或异步的。
@FelixKling ES6 模块加载器在 OP 中被标记,所以我认为它与答案相关。我还说,根据观察,异步是当前的行为,以及未来的可能性(在任何实现中),因此这是一个需要考虑的相关点。你觉得有错吗?
我认为重要的是不要将模块系统/语法与模块加载器混为一谈。例如,如果您为 node 开发,那么您很可能将 ES6 模块编译为 require,因此您无论如何都在使用 Node 的模块系统和加载器。
H
Hasan Sefa Ozalp

截至目前 ES6 导入,导出是 always compiled to CommonJS,因此使用一个或另一个没有好处。虽然推荐使用 ES6,因为当浏览器的原生支持发布时它应该是有利的。原因是,您可以从一个文件中导入部分文件,而使用 CommonJS 您必须要求所有文件。

ES6 → import, export default, export

CommonJS → require, module.exports, exports.foo

以下是这些的常见用法。

ES6 导出默认

// hello.js
function hello() {
  return 'hello'
}
export default hello

// app.js
import hello from './hello'
hello() // returns hello

ES6 导出多个和导入多个

// hello.js
function hello1() {
  return 'hello1'
}
function hello2() {
  return 'hello2'
}
export { hello1, hello2 }

// app.js
import { hello1, hello2 } from './hello'
hello1()  // returns hello1
hello2()  // returns hello2

CommonJS 模块.exports

// hello.js
function hello() {
  return 'hello'
}
module.exports = hello

// app.js
const hello = require('./hello')
hello()   // returns hello

CommonJS module.exports 多个

// hello.js
function hello1() {
  return 'hello1'
}
function hello2() {
  return 'hello2'
}
module.exports = {
  hello1,
  hello2
}

// app.js
const hello = require('./hello')
hello.hello1()   // returns hello1
hello.hello2()   // returns hello2

在使用 CommonJS require 时,您实际上也可以使用 Object Destructuring。因此,您可以:const { hello1, hello2 } = require("./hello");,它会有点类似于使用导入/导出。
这是迄今为止最好的答案,因为它不仅提供了描述,还提供了实际的代码片段。
s
snozza

主要优点是语法:

更多声明性/紧凑的语法

ES6 模块基本上会使 UMD(通用模块定义)过时 - 从本质上消除 CommonJS 和 AMD(服务器与浏览器)之间的分裂。

您不太可能看到 ES6 模块的任何性能优势。即使浏览器完全支持 ES6 功能,您仍然需要一个额外的库来捆绑模块。


你能解释一下为什么即使浏览器有完整的 ES6 模块支持也需要打包器吗?
道歉,编辑更有意义。我的意思是导入/导出模块功能并未在任何浏览器本机中实现。仍然需要转译器。
这对我来说似乎有点矛盾。如果有完全支持,那么捆绑器的目的是什么? ES6 规范中是否缺少某些内容?在完全受支持的环境中,捆绑程序实际上会做什么?
正如@snozza 所说......“导入/导出模块功能没有在任何浏览器中天真地实现。仍然需要转译器”
您不再需要任何额外的库。从 v8.5.0(一年多前发布)开始,node --experimemntal-modules index.mjs 允许您在没有 Babel 的情况下使用 import。您也可以(并且应该)publish your npm packages as native ESModule, with backwards compatibility 使用旧的 require 方式。许多浏览器本身也支持 dynamic imports
p
prosti

使用其中一种是否有任何性能优势?

当前的答案是否定的,因为当前的浏览器引擎都没有实现 ES6 标准中的 import/export

一些比较图表 http://kangax.github.io/compat-table/es6/ 没有考虑到这一点,因此当您看到 Chrome 的几乎所有绿色时,请小心。 ES6 中的 import 关键字未被考虑在内。

换句话说,包括 V8 在内的当前浏览器引擎无法通过任何 JavaScript 指令从主 JavaScript 文件中导入新的 JavaScript 文件。

(在 V8 根据 ES6 规范实现它之前,我们可能还需要 a few bugs away 或几年的时间。)

这个document是我们需要的,这个document是我们必须遵守的。

ES6 标准规定,在我们读取模块之前,模块依赖项应该存在,就像在编程语言 C 中一样,我们有(头文件).h 文件。

这是一个很好且经过充分测试的结构,我相信创建 ES6 标准的专家已经考虑到了这一点。

这使得 Webpack 或其他包捆绑器能够在某些特殊情况下优化捆绑包,并减少捆绑包中一些不需要的依赖项。但是在我们有完美依赖的情况下,这永远不会发生。

import/export 原生支持上线还需要一些时间,而且 require 关键字在很长一段时间内都不会出现在任何地方。

require 是什么?

这是加载模块的 node.js 方式。 (https://github.com/nodejs/node)

Node 使用系统级别的方法来读取文件。使用 require 时,您基本上依赖它。 require 将以 uv_fs_open 之类的系统调用结束(取决于最终系统、Linux、Mac、Windows)以加载 JavaScript 文件/模块。

要检查这是否属实,请尝试使用 Babel.js,您会看到 import 关键字将转换为 require

https://i.stack.imgur.com/5WgFJ.png


实际上,有一个方面可以提高性能——包大小。在 Webpack 2 / Rollup 构建过程中使用 import 可能会通过“摇树”未使用的模块/代码来减少生成的文件大小,否则可能会在最终包中结束。更小的文件大小 = 更快的下载 = 在客户端上更快地初始化/执行。
原因是地球上当前的浏览器没有原生允许 import 关键字。或者这意味着您不能从一个 JavaScript 文件导入另一个 JavaScript 文件。这就是为什么您无法比较这两者的性能优势的原因。当然,像 Webpack1/2 或 Browserify 这样的工具可以处理压缩。他们并驾齐驱:gist.github.com/substack/68f8d502be42d5cd4942
你忽略了“摇树”。您的要点链接中没有任何地方讨论过摇树。使用 ES6 模块可以启用它,因为 importexport 是导入特定代码路径的静态声明,而 require 可以是动态的,因此捆绑在未使用的代码中。性能优势是间接的——Webpack 2 和/或 Rollup 可以潜在地导致更小的包大小,下载速度更快,因此对(浏览器的)最终用户来说看起来更快捷。这仅适用于所有代码都写在 ES6 模块中的情况,因此可以静态分析导入。
我更新了@LeeBenson 的答案,我认为如果我们考虑浏览器引擎的本机支持,我们还无法比较。使用 Webpack 的便捷三抖动选项甚至可以在我们设置 CommonJS 模块之前实现,因为对于大多数实际应用程序,我们知道应该使用哪些模块。
您的回答完全有效,但我认为我们正在比较两个不同的特征。 All import/export 转换为 require,授予。但是这一步之前发生的事情可以被认为是“性能”增强。示例:如果 lodash 是用 ES6 编写的,而您是 import { omit } from lodash,则最终捆绑包将仅包含“省略”而不包含其他实用程序,而简单的 require('lodash') 将导入所有内容。这会增加捆绑包的大小,需要更长的下载时间,因此会降低性能。当然,这仅在浏览器上下文中有效。
L
Lee Benson

使用 ES6 模块对于“摇树”很有用;即启用 Webpack 2、Rollup(或其他捆绑程序)来识别未使用/未导入的代码路径,因此不会将其放入生成的捆绑包中。这可以通过消除您永远不需要的代码来显着减少其文件大小,但是默认情况下与 CommonJS 捆绑在一起,因为 Webpack 等人无法知道是否需要它。

这是使用代码路径的静态分析来完成的。

例如,使用:

import { somePart } 'of/a/package';

... 提示捆绑程序不需要 package.anotherPart(如果未导入,则无法使用 - 对吗?),因此它不会打扰捆绑它。

要为 Webpack 2 启用此功能,您需要确保您的转译器不会吐出 CommonJS 模块。如果您将 es2015 插件与 babel 一起使用,则可以在 .babelrc 中禁用它,如下所示:

{
  "presets": [
    ["es2015", { modules: false }],
  ]
}

Rollup 和其他人的工作方式可能不同 - 如果您有兴趣,请查看文档。


那么 require 会发生什么?
J
Joel

当涉及到异步或延迟加载时,import () 更强大。看看我们何时以异步方式需要组件,然后我们以某种异步方式使用 import,就像在使用 awaitconst 变量中一样。

const module = await import('./module.js');

或者,如果您想使用 require()

const converter = require('./converter');

事情是 import() 实际上是异步的。正如 neehar venugopal 在 ReactConf 中提到的,您可以使用它为客户端架构动态加载反应组件。

在路由方面也更好。这是当用户连接到特定网站到其特定组件时,使网络日志下载必要部分的一件特别的事情。例如,仪表板之前的登录页面不会下载仪表板的所有组件。因为当前需要的是登录组件,所以只会下载。

export 也是如此:ES6 export 与 CommonJS module.exports 完全相同。

注意 - 如果您正在开发 node.js 项目,那么您必须严格使用 require(),因为如果您使用 import,节点将抛出异常错误为 invalid token 'import'。所以node不支持import语句。

更新 - 正如 Dan Dascalescu 所建议的:从 v8.5.0(2017 年 9 月发布)开始,node --experimental-modules index.mjs 允许您在没有 Babel 的情况下使用 import。您也可以(并且应该)使用旧的 require 方式publish your npm packages as native ESModule, with backwards compatibility

有关在何处使用异步导入的更多信息,请参阅此内容 - https://www.youtube.com/watch?v=bb6RCrDaxhw


那么需要同步并等待吗?
可以如实说!
i
isysd

最重要的是要知道 ES6 模块确实是官方标准,而 CommonJS (Node.js) 模块不是。

2019 年,84% 的浏览器支持 ES6 模块。虽然 Node.js 将它们放在 --experimental-modules 标志后面,但还有一个名为 esm 的便捷节点包,它使集成顺利进行。

您可能在这些模块系统之间遇到的另一个问题是代码位置。 Node.js 假定源代码保存在 node_modules 目录中,而大多数 ES6 模块部署在平面目录结构中。这些不容易协调,但可以通过使用安装前和安装后脚本破解您的 package.json 文件来完成。下面是一个示例 isomorphic module 和一个 article,解释了它的工作原理。


c
chandoo

我个人使用 import 是因为,我们可以通过 import 来导入所需的方法、成员。

import {foo, bar} from "dep";

文件名:dep.js

export foo function(){};
export const bar = 22

归功于 Paul Shan。 More info


选的好!您是否也publishing your npm packages as native ESModule, with backwards compatibility 使用旧的 require 方式?
你可以用 require 做同样的事情!
const {a,b} = require('module.js'); 也可以...如果您导出 ab
module.exports = { a: ()={}, b: 22 } - @BananaAcid 的第二部分响应
l
l00k

不知道为什么(可能是优化 - 延迟加载?)它是这样工作的,但我注意到如果不使用导入的模块,import 可能无法解析代码。
在某些情况下这可能不是预期的行为。

将讨厌的 Foo 类作为我们的示例依赖项。

脚.ts

export default class Foo {}
console.log('Foo loaded');

例如:

索引.ts

import Foo from './foo'
// prints nothing

索引.ts

const Foo = require('./foo').default;
// prints "Foo loaded"

索引.ts

(async () => {
    const FooPack = await import('./foo');
    // prints "Foo loaded"
})();

另一方面:

索引.ts

import Foo from './foo'
typeof Foo; // any use case
// prints "Foo loaded"

Y
Yilmaz

ES 模块是静态的,这意味着在每个模块的顶层和任何控制流语句之外都描述了导入。这不起作用: if (condition) { import module1 from 'module1' }

但在 commonjs 中,它是允许的:

if (condition) {
    module = require('module1')
}

ES 模块以严格模式隐式运行。这意味着我们不必在每个文件的开头显式添加“use strict”语句。严格模式不能禁用;因此,我们不能使用未声明的变量或 with 语句或具有仅在非严格模式下可用的其他功能。严格模式是一种更安全的执行模式。

在 ESM 中,一些重要的 CommonJS 引用没有定义。其中包括 require、exports、module.exports、__filename 和 __dirname。

我们可以使用标准的导入语法从 ESM 中导入 CommonJS 模块。但只有默认导出有效: import packageName from 'commonjs-package' // 有效 import { moduleName } from 'commonjs-package' // 错误

但是,无法从 CommonJS 模块中导入 ES 模块。

ESM 不能直接将 JSON 文件作为模块导入,这是 CommonJS 中经常使用的功能。这就是为什么在 reactjs 中使用 fetch api 的原因。从'./data.json'导入数据//失败


可以使用 dynamic import 有条件地导入模块。
您现在可以直接导入 JSON 文件。