在我正在合作的一个项目中,我们有两种选择可以使用的模块系统:
使用 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
方式。
更新
从 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 无论如何都会默认将 import
和 export
声明转换为 CommonJS (require
/module.exports
)。因此,即使您使用 ES6 模块语法,如果您在 Node.js 中运行代码,您将在后台使用 CommonJS。
CommonJS 和 ES6 模块之间存在技术差异,例如 CommonJS 允许您动态加载模块。 ES6 不允许这样做,but there is an API in development for that。
由于 ES6 模块是标准的一部分,我会使用它们。
您可能需要考虑几种用法/功能:
要求:
您可以在加载的模块名称未预定义/静态的情况下进行动态加载,或者仅在“真正需要”时有条件地加载模块(取决于某些代码流)。
加载是同步的。这意味着如果您有多个需求,它们会被一一加载和处理。
ES6 导入:
您可以使用命名导入来选择性地仅加载您需要的部分。这样可以节省内存。
导入可以是异步的(在当前的 ES6 模块加载器中,实际上是异步的)并且可以执行得更好一些。
此外,Require 模块系统不是基于标准的。既然存在 ES6 模块,它就不太可能成为标准。将来,将在各种实现中对 ES6 模块提供原生支持,这将在性能方面具有优势。
require
,因此您无论如何都在使用 Node 的模块系统和加载器。
截至目前 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
Object Destructuring
。因此,您可以:const { hello1, hello2 } = require("./hello");
,它会有点类似于使用导入/导出。
主要优点是语法:
更多声明性/紧凑的语法
ES6 模块基本上会使 UMD(通用模块定义)过时 - 从本质上消除 CommonJS 和 AMD(服务器与浏览器)之间的分裂。
您不太可能看到 ES6 模块的任何性能优势。即使浏览器完全支持 ES6 功能,您仍然需要一个额外的库来捆绑模块。
node --experimemntal-modules index.mjs
允许您在没有 Babel 的情况下使用 import
。您也可以(并且应该)publish your npm packages as native ESModule, with backwards compatibility 使用旧的 require
方式。许多浏览器本身也支持 dynamic imports。
使用其中一种是否有任何性能优势?
当前的答案是否定的,因为当前的浏览器引擎都没有实现 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
import
可能会通过“摇树”未使用的模块/代码来减少生成的文件大小,否则可能会在最终包中结束。更小的文件大小 = 更快的下载 = 在客户端上更快地初始化/执行。
import
关键字。或者这意味着您不能从一个 JavaScript 文件导入另一个 JavaScript 文件。这就是为什么您无法比较这两者的性能优势的原因。当然,像 Webpack1/2 或 Browserify 这样的工具可以处理压缩。他们并驾齐驱:gist.github.com/substack/68f8d502be42d5cd4942
import
和 export
是导入特定代码路径的静态声明,而 require
可以是动态的,因此捆绑在未使用的代码中。性能优势是间接的——Webpack 2 和/或 Rollup 可以潜在地导致更小的包大小,下载速度更快,因此对(浏览器的)最终用户来说看起来更快捷。这仅适用于所有代码都写在 ES6 模块中的情况,因此可以静态分析导入。
import/export
转换为 require
,授予。但是在这一步之前发生的事情可以被认为是“性能”增强。示例:如果 lodash
是用 ES6 编写的,而您是 import { omit } from lodash
,则最终捆绑包将仅包含“省略”而不包含其他实用程序,而简单的 require('lodash')
将导入所有内容。这会增加捆绑包的大小,需要更长的下载时间,因此会降低性能。当然,这仅在浏览器上下文中有效。
使用 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
会发生什么?
当涉及到异步或延迟加载时,import ()
更强大。看看我们何时以异步方式需要组件,然后我们以某种异步方式使用 import
,就像在使用 await
的 const
变量中一样。
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
最重要的是要知道 ES6 模块确实是官方标准,而 CommonJS (Node.js) 模块不是。
2019 年,84% 的浏览器支持 ES6 模块。虽然 Node.js 将它们放在 --experimental-modules 标志后面,但还有一个名为 esm 的便捷节点包,它使集成顺利进行。
您可能在这些模块系统之间遇到的另一个问题是代码位置。 Node.js 假定源代码保存在 node_modules
目录中,而大多数 ES6 模块部署在平面目录结构中。这些不容易协调,但可以通过使用安装前和安装后脚本破解您的 package.json
文件来完成。下面是一个示例 isomorphic module 和一个 article,解释了它的工作原理。
我个人使用 import 是因为,我们可以通过 import 来导入所需的方法、成员。
import {foo, bar} from "dep";
文件名:dep.js
export foo function(){};
export const bar = 22
归功于 Paul Shan。 More info。
require
方式?
const {a,b} = require('module.js');
也可以...如果您导出 a
和 b
module.exports = { a: ()={}, b: 22 }
- @BananaAcid 的第二部分响应
不知道为什么(可能是优化 - 延迟加载?)它是这样工作的,但我注意到如果不使用导入的模块,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"
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'导入数据//失败
不定期副业成功案例分享
module.exports = ...;
等同于export default ...
。exports.foo = ...
等价于export var foo = ...
;import
转换为 Node 中的 CommonJS,与 Webpack 2 / Rollup(以及任何其他允许 ES6 树摇动的捆绑器)一起使用,最终还是有可能得到一个比同等文件小得多的文件代码节点通过使用require
来处理,因为事实上 ES6 允许对导入/导出进行静态分析。虽然这不会对 Node 产生影响(目前),但如果代码最终将作为单个浏览器包结束,它肯定会产生影响。