做者:Tobias Nießen翻译:疯狂的技术宅javascript
原文:https://jaxenter.com/es-modul...html
未经容许严禁转载前端
几乎每种编程语言都能将组成程序的代码拆分为多个文件。 在 C 和 C++ 中#include
指令就用于这个目的,而 Java 和 Python 有import
关键字。 JavaScript 是迄今为止为数很少的例外之一,但新的 JavaScript 标准(ECMAScript 6)经过引入所谓的 ECMAScript 模块来改变这一点。全部主流浏览器都支持这个新标准 —— 只有 Node.js 彷佛落后了。这是为何?
新的 ECMAScript(ES)模块与之前的语言版本不彻底兼容,所以使用的 JavaScript 引擎须要知道每个文件是“旧” JavaScript 代码仍是“新”模块。java
例如在 ECMAScript 5 中引入的许多程序员首选的严格模式曾经是可选的,必须明确启用才行,同时它在 ES 模块中始终处于活动状态。所以,如下代码段在语法上能够解释为传统的 JavaScript 代码和 ES 模块:node
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网络协议加载。npm
CommonJS 容许经过 require
函数加载模块,该函数返回加载的模块。例如,CommonJS 模块可能以下所示:编程
const { readFile } = require('fs'); const myModule = require('./my-module');
这不是 ECMAScript 6 中的一个选项,由于在 require()
调用期间,模块在 HTTP 上加载时可能会长时间阻止整个程序的执行。相反,ES 模块提供了两种加载其余模块的方法。在大多数状况下,使用 import
是有意义的:json
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 都支持它。
区分 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
的新属性。能够选择将其更改成 commonjs
或 module
以肯定默认状况下应加载的包中所包含的 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 安装的软件包的加载。 fs
和 crypto
等内置模块能够经过两种方式加载。
除了异步加载依赖项的问题以外,Node.js 中的旧模块和新模块之间还存在进一步的差别。特别是 ES 模块中再也不提供 Node.js 的特定功能,如变量 __dirname
,__filename
,export
和 module
。 __dirname
和 __filename
能够根据须要重新的 import.meta
对象重建:
import { fileURLToPath } from 'url'; import { dirname} from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);
变量 module
和 exports
已被删除而无需替换;这一样适用于 module.filename
,module.id
和 module.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 做为编程语言的开发,标准化模块的引入是重要的一步,为将来的改进铺平了道路。