node_modules/
中import.meta
– 当前模块的元数据 [ES2020]import.meta.url
import.meta.url
和类 URL
import.meta.url
import()
动态加载模块 [ES2020](高级)import
语句的局限性import()
运算符进行动态导入import()
的用例await
[ES2022](高级)await
的用例await
在底层是如何工作的?await
的优缺点// Named exports
export function f() {}
export const one = 1;
export {foo, b as bar};
// Default exports
export default function f() {} // declaration with optional name
// Replacement for `const` (there must be exactly one value)
export default 123;
// Re-exporting from another module
export {foo, b as bar} from './some-module.mjs';
export * from './some-module.mjs';
export * as ns from './some-module.mjs'; // ES2020
// Named imports
import {foo, bar as b} from './some-module.mjs';
// Namespace import
import * as someModule from './some-module.mjs';
// Default import
import someModule from './some-module.mjs';
// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, {foo, bar as b} from './some-module.mjs';
// Empty import (for modules with side effects)
import './some-module.mjs';
当前 JavaScript 模块的格局相当多样化:ES6 带来了内置模块,但在此之前出现的源代码格式仍然存在。了解后者有助于理解前者,所以让我们来研究一下。接下来的部分将描述以下几种交付 JavaScript 源代码的方式
表 18 概述了这些代码格式。请注意,对于 CommonJS 模块和 ECMAScript 模块,通常使用两种文件名扩展名。哪一种合适取决于我们想如何使用文件。本章稍后将详细介绍。
运行于 | 加载方式 | 文件名扩展名 | |
---|---|---|---|
脚本 | 浏览器 | 异步 | .js |
CommonJS 模块 | 服务器 | 同步 | .js .cjs |
AMD 模块 | 浏览器 | 异步 | .js |
ECMAScript 模块 | 浏览器和服务器 | 异步 | .js .mjs |
在我们接触到内置模块(ES6 引入的)之前,我们将看到的所有代码都将用 ES5 编写。除此之外
const
和 let
,只有 var
。最初,浏览器只有脚本——在全局作用域中执行的代码片段。例如,考虑一个通过以下 HTML 加载脚本文件的 HTML 文件
<script src="other-module1.js"></script>
<script src="other-module2.js"></script>
<script src="my-module.js"></script>
主文件是 my-module.js
,我们在其中模拟一个模块
var myModule = (function () { // Open IIFE
// Imports (via global variables)
var importedFunc1 = otherModule1.importedFunc1;
var importedFunc2 = otherModule2.importedFunc2;
// Body
function internalFunc() {
// ···
}function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports (assigned to global variable `myModule`)
return {
exportedFunc: exportedFunc,
;
}; // Close IIFE })()
myModule
是一个全局变量,它被赋值为立即调用函数表达式的结果。函数表达式从第一行开始。它在最后一行被调用。
这种包装代码片段的方式称为立即调用函数表达式(IIFE,由 Ben Alman 提出)。我们从 IIFE 中获得了什么?var
不是块级作用域的(如 const
和 let
),它是函数级作用域的:为 var
声明的变量创建新作用域的唯一方法是通过函数或方法(使用 const
和 let
,我们可以使用函数、方法或块 {}
)。因此,示例中的 IIFE 隐藏了以下所有变量,使其不在全局作用域中,并最大限度地减少了名称冲突:importedFunc1
、importedFunc2
、internalFunc
、exportedFunc
。
请注意,我们以一种特殊的方式使用 IIFE:最后,我们选择要导出的内容,并通过对象字面量返回它。这被称为揭示模块模式(由 Christian Heilmann 提出)。
这种模拟模块的方式有几个问题
在 ECMAScript 6 之前,JavaScript 没有内置模块。因此,该语言灵活的语法被用来在语言内部实现自定义模块系统。两种流行的模块系统是
最初的 CommonJS 模块标准是为服务器和桌面平台创建的。它是最初的 Node.js 模块系统的基础,并在那里获得了巨大的流行。Node 的 npm 包管理器以及能够在客户端使用 Node 模块的工具(browserify、webpack 等)也促成了它的流行。
从现在开始,CommonJS 模块指的是该标准的 Node.js 版本(它有一些额外的功能)。这是一个 CommonJS 模块的例子
// Imports
var importedFunc1 = require('./other-module1.js').importedFunc1;
var importedFunc2 = require('./other-module2.js').importedFunc2;
// Body
function internalFunc() {
// ···
}function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports
.exports = {
moduleexportedFunc: exportedFunc,
; }
CommonJS 的特点如下
创建 AMD 模块格式是为了比 CommonJS 格式更易于在浏览器中使用。它最流行的实现是 RequireJS。下面是一个 AMD 模块的例子。
define(['./other-module1.js', './other-module2.js'],
function (otherModule1, otherModule2) {
var importedFunc1 = otherModule1.importedFunc1;
var importedFunc2 = otherModule2.importedFunc2;
function internalFunc() {
// ···
}function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
return {
exportedFunc: exportedFunc,
;
}; })
AMD 的特点如下
从好的方面来说,AMD 模块可以直接执行。相比之下,CommonJS 模块必须在部署之前进行编译,或者必须动态生成和评估自定义源代码 (想想 eval()
)。这在 Web 上并不总是允许的。
纵观 CommonJS 和 AMD,JavaScript 模块系统之间的相似之处就显现出来了
ECMAScript 模块(ES 模块或 ESM)是 ES6 引入的。它们延续了 JavaScript 模块的传统,并具有上述所有特点。此外
ES 模块还有新的优势
这是一个 ES 模块语法的例子
import {importedFunc1} from './other-module1.mjs';
import {importedFunc2} from './other-module2.mjs';
function internalFunc() {
···
}
export function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
从现在开始,“模块”指的是“ECMAScript 模块”。
ES 模块的完整标准包括以下部分
第 1 部分和第 2 部分是 ES6 引入的。第 3 部分的工作正在进行中。
每个模块可以有零个或多个命名导出。
例如,考虑以下两个文件
lib/my-math.mjs
main.mjs
模块 my-math.mjs
有两个命名导出:square
和 LIGHTSPEED
。
// Not exported, private to module
function times(a, b) {
return a * b;
}export function square(x) {
return times(x, x);
}export const LIGHTSPEED = 299792458;
要导出某些内容,我们在声明前面加上关键字 export
。未导出的实体对模块是私有的,不能从外部访问。
模块 main.mjs
有一个命名导入,square
import {square} from './lib/my-math.mjs';
.equal(square(3), 9); assert
它还可以重命名其导入
import {square as sq} from './lib/my-math.mjs';
.equal(sq(3), 9); assert
命名导入和解构看起来很相似
import {foo} from './bar.mjs'; // import
const {foo} = require('./bar.mjs'); // destructuring
但它们有很大不同
导入与其导出保持连接。
我们可以在解构模式中再次解构,但导入语句中的 {}
不能嵌套。
重命名的语法不同
import {foo as f} from './bar.mjs'; // importing
const {foo: f} = require('./bar.mjs'); // destructuring
基本原理:解构让人联想到对象字面量(包括嵌套),而导入则让人联想到重命名。
练习:命名导出
exercises/modules/export_named_test.mjs
命名空间导入是命名导入的替代方案。如果我们命名空间导入一个模块,它将成为一个对象,其属性是命名导出。如果我们使用命名空间导入,则 main.mjs
如下所示
import * as myMath from './lib/my-math.mjs';
.equal(myMath.square(3), 9);
assert
.deepEqual(
assertObject.keys(myMath), ['LIGHTSPEED', 'square']);
到目前为止,我们看到的命名导出样式是内联的:我们通过在实体前面加上关键字 export
来导出它们。
但我们也可以使用单独的导出子句。例如,以下是使用导出子句的 lib/my-math.mjs
function times(a, b) {
return a * b;
}function square(x) {
return times(x, x);
}const LIGHTSPEED = 299792458;
export { square, LIGHTSPEED }; // semicolon!
使用导出子句,我们可以在导出之前重命名并在内部使用不同的名称
function times(a, b) {
return a * b;
}function sq(x) {
return times(x, x);
}const LS = 299792458;
export {
as square,
sq as LIGHTSPEED, // trailing comma is optional
LS ; }
每个模块最多可以有一个默认导出。其理念是模块就是默认导出的值。
避免混合使用命名导出和默认导出
一个模块可以同时具有命名导出和默认导出,但通常最好每个模块坚持一种导出样式。
作为默认导出的示例,请考虑以下两个文件
my-func.mjs
main.mjs
模块 my-func.mjs
具有默认导出
const GREETING = 'Hello!';
export default function () {
return GREETING;
}
模块 main.mjs
默认导入导出的函数
import myFunc from './my-func.mjs';
.equal(myFunc(), 'Hello!'); assert
请注意语法差异:命名导入周围的花括号表示我们正在深入模块,而默认导入就是模块。
默认导出的用例是什么?
默认导出最常见的用例是包含单个函数或单个类的模块。
默认导出有两种样式。
首先,我们可以使用 export default
标记现有声明
export default function myFunc() {} // no semicolon!
export default class MyClass {} // no semicolon!
其次,我们可以直接默认导出值。这种 export default
样式很像声明。
export default myFunc; // defined elsewhere
export default MyClass; // defined previously
export default Math.sqrt(2); // result of invocation is default-exported
export default 'abc' + 'def';
export default { no: false, yes: true };
原因是 export default
不能用于标记 const
:const
可以定义多个值,但 export default
只需要一个值。请考虑以下假设代码
// Not legal JavaScript!
export default const foo = 1, bar = 2, baz = 3;
使用此代码,我们不知道三个值中的哪一个是默认导出。
练习:默认导出
exercises/modules/export_default_test.mjs
在内部,默认导出只是一个名为 default
的命名导出。例如,请考虑前面带有默认导出的模块 my-func.mjs
const GREETING = 'Hello!';
export default function () {
return GREETING;
}
以下模块 my-func2.mjs
等效于该模块
const GREETING = 'Hello!';
function greet() {
return GREETING;
}
export {
as default,
greet ; }
对于导入,我们可以使用普通的默认导入
import myFunc from './my-func2.mjs';
.equal(myFunc(), 'Hello!'); assert
或者我们可以使用命名导入
import {default as myFunc} from './my-func2.mjs';
.equal(myFunc(), 'Hello!'); assert
默认导出也可以通过命名空间导入的属性 .default
获得
import * as mf from './my-func2.mjs';
.equal(mf.default(), 'Hello!'); assert
default
作为变量名不是非法的吗?
default
不能是变量名,但它可以是导出名,也可以是属性名
const obj = {
default: 123,
;
}.equal(obj.default, 123); assert
到目前为止,我们已经直观地使用了导入和导出,并且一切似乎都按预期工作。但现在是时候仔细看看导入和导出是如何真正相关的了。
请考虑以下两个模块
counter.mjs
main.mjs
counter.mjs
导出一个(可变的!)变量和一个函数
export let counter = 3;
export function incCounter() {
++;
counter }
main.mjs
命名导入这两个导出。当我们使用 incCounter()
时,我们发现与 counter
的连接是实时的——我们始终可以访问该变量的实时状态
import { counter, incCounter } from './counter.mjs';
// The imported value `counter` is live
.equal(counter, 3);
assertincCounter();
.equal(counter, 4); assert
请注意,虽然连接是实时的,我们可以读取 counter
,但我们不能更改此变量(例如,通过 counter++
)。
以这种方式处理导入有两个好处
ESM 透明地支持循环导入。要了解这是如何实现的,请考虑以下示例:图 7 显示了导入其他模块的模块的有向图。P 导入 M 是这种情况下的循环。
解析后,这些模块分两个阶段设置
由于 ES 模块的两个特性,这种方法可以正确处理循环导入
由于 ES 模块的静态结构,导出在解析后就已经知道了。这使得可以在其子模块 M 之前实例化 P:P 已经可以查找 M 的导出。
当 P 被求值时,M 还没有被求值。但是,P 中的实体已经可以提及来自 M 的导入。它们只是还不能使用它们,因为导入的值稍后才会填充。例如,P 中的函数可以访问来自 M 的导入。唯一的限制是我们必须等到 M 求值之后才能调用该函数。
导入是导出上的“实时不可变视图”,因此可以稍后填充。
npm 软件注册表是分发用于 Node.js 和 Web 浏览器的 JavaScript 库和应用程序的主要方式。它通过npm 包管理器(简称:npm)进行管理。软件以所谓的包的形式分发。包是一个目录,其中包含任意文件和一个位于顶层的描述包的文件 package.json
。例如,当 npm 在目录 my-package/
中创建一个空包时,我们会得到这个 package.json
{
"name": "my-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
其中一些属性包含简单的元数据
name
指定此包的名称。一旦上传到 npm 注册表,就可以通过 npm install my-package
安装它。version
用于版本管理,并遵循语义化版本控制,包含三个数字description
、keywords
、author
使查找包更容易。license
说明了我们如何使用此包。其他属性启用高级配置
main
:指定“是”包的模块(本章稍后解释)。scripts
:是我们可通过 npm run
执行的命令。例如,脚本 test
可以通过 npm run test
执行。有关 package.json
的更多信息,请参阅npm 文档。
node_modules/
中npm 始终将包安装在目录 node_modules
中。通常会有很多这样的目录。npm 使用哪一个取决于当前所在的目录。例如,如果我们在目录 /tmp/a/b/
中,npm 会尝试在当前目录、其父目录、父目录的父目录等中查找 node_modules
。换句话说,它会搜索以下链位置
/tmp/a/b/node_modules
/tmp/a/node_modules
/tmp/node_modules
安装包 some-pkg
时,npm 使用最近的 node_modules
。例如,如果我们在 /tmp/a/b/
中,并且该目录中有一个 node_modules
,则 npm 会将包放在该目录中
/tmp/a/b/node_modules/some-pkg/
导入模块时,我们可以使用特殊的模块说明符来告诉 Node.js 我们要从已安装的包中导入它。稍后将解释其工作原理。现在,请考虑以下示例
// /home/jane/proj/main.mjs
import * as theModule from 'the-package/the-module.mjs';
为了找到 the-module.mjs
(Node.js 更喜欢文件名扩展名 .mjs
用于 ES 模块),Node.js 会向上遍历 node_module
链并搜索以下位置
/home/jane/proj/node_modules/the-package/the-module.mjs
/home/jane/node_modules/the-package/the-module.mjs
/home/node_modules/the-package/the-module.mjs
仅在 Node.js 上支持在 node_modules
目录中查找已安装的模块。那么为什么我们也可以使用 npm 为浏览器安装库呢?
这是通过打包工具(例如 webpack)实现的,这些工具在代码部署到网上之前对其进行编译和优化。在此编译过程中,npm 包中的代码经过调整,以便在浏览器中工作。
对于命名模块文件和导入它们的变量,没有既定的最佳实践。
在本章中,我使用以下命名样式
模块文件的名称采用短横线连接的方式,并以小写字母开头
./my-module.mjs
./some-func.mjs
命名空间导入的名称采用小写字母开头,并使用驼峰式命名法
import * as myModule from './my-module.mjs';
默认导入的名称采用小写字母开头,并使用驼峰式命名法
import someFunc from './some-func.mjs';
这种样式背后的理由是什么?
npm 不允许在包名称中使用大写字母(来源)。因此,我们避免使用驼峰式命名法,以便“本地”文件的名称与 npm 包的名称保持一致。仅使用小写字母还可以最大程度地减少区分大小写的文件系统与不区分大小写的文件系统之间的冲突:前者区分名称具有相同字母但大小写不同的文件;后者则不区分。
有一些明确的规则可以将短横线连接的文件名转换为驼峰式 JavaScript 变量名。由于我们命名命名空间导入的方式,这些规则适用于命名空间导入和默认导入。
我也喜欢使用下划线连接的模块文件名,因为我们可以直接将这些名称用于命名空间导入(无需任何转换)
import * as my_module from './my_module.mjs';
但这种样式不适用于默认导入:我喜欢对命名空间对象使用下划线连接,但它不适用于函数等。
模块说明符是标识模块的字符串。它们在浏览器和 Node.js 中的工作方式略有不同。在我们查看差异之前,我们需要了解不同类别的模块说明符。
在 ES 模块中,我们区分以下类别的说明符。这些类别起源于 CommonJS 模块。
相对路径:以点开头。示例
'./some/other/module.mjs'
'../../lib/counter.mjs'
绝对路径:以斜杠开头。示例
'/home/jane/file-tools.mjs'
URL:包含协议(从技术上讲,路径也是 URL)。示例
'https://example.com/some-module.mjs'
'file:///home/john/tmp/main.mjs'
裸路径:不以点、斜杠或协议开头,并且由不带扩展名的单个文件名组成。示例
'lodash'
'the-package'
深层导入路径:以裸路径开头,并且至少有一个斜杠。示例
'the-package/dist/the-module.mjs'
浏览器按如下方式处理模块说明符
text/javascript
提供即可。请注意,打包工具(例如 webpack,它将模块组合成更少的文件)通常对说明符的严格程度低于浏览器。这是因为它们在构建/编译时(而不是运行时)运行,并且可以通过遍历文件系统来搜索文件。
Node.js 按如下方式处理模块说明符
相对路径的解析方式与在 Web 浏览器中相同——相对于当前模块的路径。
当前不支持绝对路径。作为一种解决方法,我们可以使用以 file:///
开头的 URL。我们可以通过 url.pathToFileURL()
创建此类 URL。
对于 URL 说明符,仅支持 file:
作为协议。
裸路径被解释为包名称,并相对于最近的 node_modules
目录进行解析。要加载哪个模块,是通过查看包的 package.json
中的 "main"
属性来确定的(类似于 CommonJS)。
深度导入路径也相对于最近的 node_modules
目录进行解析。它们包含文件名,因此始终清楚指的是哪个模块。
除裸路径外,所有说明符都必须引用实际文件。也就是说,ESM 不支持以下 CommonJS 功能
CommonJS 会自动添加缺少的文件名扩展名。
如果存在一个带有 "main"
属性的 dir/package.json
,则 CommonJS 可以导入目录 dir
。
如果存在模块 dir/index.js
,则 CommonJS 可以导入目录 dir
。
所有内置 Node.js 模块都可以通过裸路径获得,并具有命名的 ESM 导出 - 例如
import * as assert from 'assert/strict';
import * as path from 'path';
.equal(
assert.join('a/b/c', '../d'), 'a/b/d'); path
Node.js 支持以下默认文件名扩展名
.mjs
用于 ES 模块.cjs
用于 CommonJS 模块文件名扩展名 .js
代表 ESM 或 CommonJS。它是哪一种由“最近的” package.json
(在当前目录、父目录等中)配置。以这种方式使用 package.json
与包无关。
在该 package.json
中,有一个属性 "type"
,它有两个设置
"commonjs"
(默认值):扩展名为 .js
或没有扩展名的文件被解释为 CommonJS 模块。
"module"
:扩展名为 .js
或没有扩展名的文件被解释为 ESM 模块。
并非所有由 Node.js 执行的源代码都来自文件。我们还可以通过 stdin、--eval
和 --print
向其发送代码。命令行选项 --input-type
允许我们指定如何解释此类代码
--input-type=commonjs
--input-type=module
import.meta
- 当前模块的元数据 [ES2020]对象 import.meta
保存当前模块的元数据。
import.meta.url
import.meta
最重要的属性是 .url
,它包含一个字符串,其中包含当前模块文件的 URL - 例如
'https://example.com/code/main.mjs'
import.meta.url
和类 URL
类 URL
可通过浏览器和 Node.js 中的全局变量获得。我们可以在 Node.js 文档 中查找其完整功能。在使用 import.meta.url
时,它的构造函数特别有用
new URL(input: string, base?: string|URL)
参数 input
包含要解析的 URL。如果提供了第二个参数 base
,则它可以是相对的。
换句话说,此构造函数允许我们解析相对于基本 URL 的相对路径
> new URL('other.mjs', 'https://example.com/code/main.mjs').href'https://example.com/code/other.mjs'
> new URL('../other.mjs', 'https://example.com/code/main.mjs').href'https://example.com/other.mjs'
这就是我们如何获得指向位于当前模块旁边的文件 data.txt
的 URL
实例
const urlOfData = new URL('data.txt', import.meta.url);
import.meta.url
在 Node.js 上,import.meta.url
始终是一个带有 file:
URL 的字符串 - 例如
'file:///Users/rauschma/my-module.mjs'
许多 Node.js 文件系统操作接受带有路径的字符串或 URL
的实例。这使我们能够读取当前模块的同级文件 data.txt
import * as fs from 'fs';
function readData() {
// data.txt sits next to current module
const urlOfData = new URL('data.txt', import.meta.url);
return fs.readFileSync(urlOfData, {encoding: 'UTF-8'});
}
fs
和 URL对于模块 fs
的大多数函数,我们可以通过以下方式引用文件
Buffer
的实例中。URL
的实例中(使用协议 file:
)有关此主题的更多信息,请参阅 Node.js API 文档。
file:
URL 和路径之间转换Node.js 模块 url
有两个函数用于在 file:
URL 和路径之间转换
fileURLToPath(url: URL|string): string
file:
URL 转换为路径。pathToFileURL(path: string): URL
file:
URL。如果我们需要一个可以在本地文件系统中使用的路径,那么 URL
实例的属性 .pathname
并不总是有效
.equal(
assertnew URL('file:///tmp/with%20space.txt').pathname,
'/tmp/with%20space.txt');
因此,最好使用 fileURLToPath()
import * as url from 'url';
.equal(
assert.fileURLToPath('file:///tmp/with%20space.txt'),
url'/tmp/with space.txt'); // result on Unix
类似地,pathToFileURL()
不仅仅是在绝对路径前面加上 'file://'
。
import()
动态加载模块 [ES2020](高级) import()
运算符使用 Promise
Promise 是一种处理异步计算结果(即不是立即计算)的技术。它们在 §40 “用于异步编程的 Promise [ES6]” 中进行了说明。在您了解它们之前,推迟阅读本节可能是有意义的。
import
语句的局限性到目前为止,导入模块的唯一方法是通过 import
语句。该语句有几个限制
if
语句内部时,我们不能导入任何东西。import()
运算符进行动态导入import()
运算符没有 import
语句的限制。它看起来像这样
import(moduleSpecifierStr)
.then((namespaceObject) => {
console.log(namespaceObject.namedExport);
; })
此运算符的使用方式类似于函数,接收一个带有模块说明符的字符串,并返回一个 Promise,该 Promise 解析为一个命名空间对象。该对象的属性是导入模块的导出。
通过 await
使用 import()
甚至更方便
const namespaceObject = await import(moduleSpecifierStr);
console.log(namespaceObject.namedExport);
请注意,await
可以在模块的顶层使用(请参阅 下一节)。
让我们看一个使用 import()
的示例。
考虑以下文件
lib/my-math.mjs
main1.mjs
main2.mjs
我们已经见过模块 my-math.mjs
// Not exported, private to module
function times(a, b) {
return a * b;
}export function square(x) {
return times(x, x);
}export const LIGHTSPEED = 299792458;
我们可以使用 import()
按需加载此模块
// main1.mjs
const moduleSpecifier = './lib/my-math.mjs';
function mathOnDemand() {
return import(moduleSpecifier)
.then(myMath => {
const result = myMath.LIGHTSPEED;
.equal(result, 299792458);
assertreturn result;
;
})
}
mathOnDemand()
.then((result) => {
.equal(result, 299792458);
assert; })
此代码中的两件事是 import
语句无法完成的
接下来,我们将实现与 main1.mjs
中相同的功能,但通过一个称为 异步函数 或 async/await 的功能,它为 Promise 提供了更简洁的语法。
// main2.mjs
const moduleSpecifier = './lib/my-math.mjs';
async function mathOnDemand() {
const myMath = await import(moduleSpecifier);
const result = myMath.LIGHTSPEED;
.equal(result, 299792458);
assertreturn result;
}
为什么 import()
是运算符而不是函数?
import()
看起来像一个函数,但不能作为函数实现
import()
是一个函数,我们就必须将此信息显式传递给它(例如,通过参数)。import()
的用例Web 应用程序的某些功能在启动时不必存在,可以按需加载。然后 import()
就派上用场了,因为我们可以将此类功能放入模块中 - 例如
.addEventListener('click', event => {
buttonimport('./dialogBox.mjs')
.then(dialogBox => {
.open();
dialogBox
}).catch(error => {
/* Error handling */
}); })
我们可能希望根据条件是否为真来加载模块。例如,一个带有 polyfill 的模块,它可以在旧平台上使用新功能
if (isLegacyPlatform()) {
import('./my-polyfill.mjs')
.then(···);
}
对于国际化等应用程序,如果我们可以动态计算模块说明符,则会有所帮助
import(`messages_${getLocale()}.mjs`)
.then(···);
await
[ES2022](高级) await
是异步函数的功能
await
在 §41 “异步函数” 中进行了说明。在您了解异步函数之前,推迟阅读本节可能是有意义的。
我们可以在模块的顶层使用 await
运算符。如果我们这样做,模块将变为异步并以不同的方式工作。值得庆幸的是,作为程序员,我们通常不会看到这一点,因为它是由语言透明地处理的。
await
的用例为什么我们想在模块的顶层使用 await
运算符?它允许我们使用异步加载的数据初始化模块。接下来的三个小节展示了它在哪些方面有用的三个示例。
const params = new URLSearchParams(location.search);
const language = params.get('lang');
const messages = await import(`./messages-${language}.mjs`); // (A)
console.log(messages.welcome);
在 A 行,我们 动态导入 一个模块。多亏了顶层 await
,这几乎与使用普通的静态导入一样方便。
let lodash;
try {
= await import('https://primary.example.com/lodash');
lodash catch {
} = await import('https://secondary.example.com/lodash');
lodash }
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
; ])
由于 Promise.any()
,变量 resource
通过首先完成的任何下载进行初始化。
await
在幕后是如何工作的?考虑以下两个文件。
first.mjs
:
const response = await fetch('http://example.com/first.txt');
export const first = await response.text();
main.mjs
:
import {first} from './first.mjs';
import {second} from './second.mjs';
.equal(first, 'First!');
assert.equal(second, 'Second!'); assert
两者大致等效于以下代码
first.mjs
:
export let first;
export const promise = (async () => { // (A)
const response = await fetch('http://example.com/first.txt');
= await response.text();
first ; })()
main.mjs
:
import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => { // (B)
await Promise.all([firstPromise, secondPromise]); // (C)
.equal(first, 'First content!');
assert.equal(second, 'Second content!');
assert; })()
模块在以下情况下变为异步
await
(first.mjs
)。main.mjs
)。每个异步模块都导出一个 Promise(A 行和 B 行),该 Promise 在其主体执行后完成。此时,可以安全地访问该模块的导出。
在情况 (2) 中,导入模块会等待所有导入的异步模块的 Promise 都完成,然后再进入其主体(C 行)。同步模块的处理方式与往常一样。
等待的拒绝和同步异常的管理方式与异步函数相同。
await
的优缺点顶层 await
的两个最重要的优点是
不利的一面是,顶层 await
会延迟导入模块的初始化。因此,最好谨慎使用它。需要更长时间的异步任务最好稍后按需执行。
但是,即使没有顶层 await
的模块也可以阻塞导入程序(例如,通过顶层的无限循环),因此阻塞本身并不是反对它的理由。
后端也有 Polyfill
本节是关于前端开发和 Web 浏览器的,但类似的想法也适用于后端开发。
Polyfill 帮助我们解决在 JavaScript 中开发 Web 应用程序时面临的冲突
给定一个 Web 平台功能 X
每次我们的 Web 应用程序启动时,它都必须首先为可能并非随处可用的功能执行所有 polyfill。之后,我们可以确保这些功能本身可用。
测验
参见测验应用程序。