給急躁的程式設計師的 JavaScript(ES2022 版)
請支持這本書:購買捐款
(廣告,請不要封鎖。)

27 模組



27.1 秘笈:模組

27.1.1 匯出

// 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

27.1.2 匯入

// 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';

27.2 JavaScript 原始碼格式

目前 JavaScript 模組的樣貌相當多元:ES6 帶來了內建模組,但它們之前的原始碼格式仍然存在。了解後者有助於了解前者,因此我們來研究一下。以下各節說明傳送 JavaScript 原始碼的下列方式

表 18 概述了這些程式碼格式。請注意,對於 CommonJS 模組和 ECMAScript 模組,通常會使用兩個檔案副檔名。要使用哪一個取決於我們要如何使用檔案。詳細資訊會在本章稍後提供。

表格 18:傳遞 JavaScript 原始碼的方式。
執行於 載入 檔案名稱副檔名
指令碼 瀏覽器 非同步 .js
CommonJS 模組 伺服器 同步 .js .cjs
AMD 模組 瀏覽器 非同步 .js
ECMAScript 模組 瀏覽器和伺服器 非同步 .js .mjs

27.2.1 在內建模組撰寫之前,程式碼以 ECMAScript 5 撰寫

在我們探討內建模組(ES6 引進)之前,我們將看到的程式碼都將以 ES5 撰寫。在其他事項中

27.3 在我們擁有模組之前,我們有指令碼

最初,瀏覽器只有指令碼,也就是在全域範圍內執行的程式碼片段。舉例來說,考量一個透過以下 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 不是區塊範圍(例如 constlet),它是函式範圍:建立 var 宣告變數的新範圍的唯一方法是透過函式或方法(使用 constlet,我們可以使用函式、方法或區塊 {})。因此,範例中的 IIFE 會將下列所有變數從全域範圍隱藏起來,並將名稱衝突降至最低:importedFunc1importedFunc2internalFuncexportedFunc

請注意,我們以特定方式使用 IIFE:最後,我們選擇我們想要匯出的內容,並透過物件文字傳回它。這稱為揭露模組模式(由 Christian Heilmann 創造)。

這種模擬模組的方式有幾個問題

27.4 ES6 之前建立的模組系統

在 ECMAScript 6 之前,JavaScript 沒有內建模組。因此,語言的彈性語法用於在語言內部實作自訂模組系統。兩個熱門的模組系統為

27.4.1 伺服器端:CommonJS 模組

模組的原始 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
module.exports = {
  exportedFunc: exportedFunc,
};

CommonJS 可以特徵化如下

27.4.2 用戶端:AMD(非同步模組定義)模組

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())。這在網路上並不總是允許的。

27.4.3 JavaScript 模組的特徵

觀察 CommonJS 和 AMD,JavaScript 模組系統之間的相似性浮現

27.5 ECMAScript 模組

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 模組」。

27.5.1 ES 模組:語法、語意、載入器 API

ES 模組的完整標準包含下列部分

  1. 語法(程式碼撰寫方式):什麼是模組?如何宣告匯入和匯出?等等。
  2. 語意(程式碼執行方式):如何匯出變數繫結?如何將匯入與匯出連結?等等。
  3. 用於設定模組載入的程式化載入器 API。

第 1 和第 2 部分在 ES6 中引入。第 3 部分的工作正在進行中。

27.6 命名匯出和匯入

27.6.1 命名匯出

每個模組可以有零個或多個命名匯出

舉例來說,考慮下列兩個檔案

lib/my-math.mjs
main.mjs

模組 my-math.mjs 有兩個命名匯出:squareLIGHTSPEED

// 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。未匯出的實體對模組是私密的,無法從外部存取。

27.6.2 命名匯入

模組 main.mjs 有單一命名匯入,square

import {square} from './lib/my-math.mjs';
assert.equal(square(3), 9);

它也可以重新命名其匯入

import {square as sq} from './lib/my-math.mjs';
assert.equal(sq(3), 9);
27.6.2.1 語法陷阱:命名匯入不是解構

命名匯入和解構看起來很相似

import {foo} from './bar.mjs'; // import
const {foo} = require('./bar.mjs'); // destructuring

但它們完全不同

  練習:命名匯出

exercises/modules/export_named_test.mjs

27.6.3 命名空間匯入

命名空間匯入是命名匯入的替代方案。如果我們對模組進行命名空間匯入,它會變成一個物件,其屬性是命名匯出。如果我們使用命名空間匯入,main.mjs 會如下所示

import * as myMath from './lib/my-math.mjs';
assert.equal(myMath.square(3), 9);

assert.deepEqual(
  Object.keys(myMath), ['LIGHTSPEED', 'square']);

27.6.4 命名匯出樣式:內嵌與子句(進階)

我們到目前為止看到的命名匯出樣式是內嵌:我們透過在實體前加上關鍵字 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 {
  sq as square,
  LS as LIGHTSPEED, // trailing comma is optional
};

27.7 預設匯出和匯入

每個模組最多只能有一個預設匯出。概念是模組就是預設匯出的值。

  避免混合命名匯出和預設匯出

一個模組可以同時有命名匯出和預設匯出,但通常最好每個模組都只使用一種匯出樣式。

以下兩個檔案是預設匯出的範例

my-func.mjs
main.mjs

模組 my-func.mjs 有預設匯出

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

模組 main.mjs 預設匯入匯出的函式

import myFunc from './my-func.mjs';
assert.equal(myFunc(), 'Hello!');

請注意語法上的差異:命名匯入周圍的大括號表示我們正在進入模組,而預設匯入就是模組。

  預設匯出的使用案例是什麼?

預設匯出最常見的使用案例是包含單一函式或單一類別的模組。

27.7.1 預設匯出的兩種樣式

預設匯出有兩種樣式。

首先,我們可以使用 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 };
27.7.1.1 為什麼會有兩種預設匯出樣式?

原因是 export default 不能用來標記 constconst 可以定義多個值,但 export default 需要正好一個值。請考慮以下假設的程式碼

// Not legal JavaScript!
export default const foo = 1, bar = 2, baz = 3;

使用此程式碼,我們不知道哪一個值是預設匯出。

  練習:預設匯出

exercises/modules/export_default_test.mjs

27.7.2 預設匯出作為命名匯出(進階)

在內部,預設匯出只是一個名稱為 default 的命名匯出。舉例來說,考慮先前具有預設匯出的模組 my-func.mjs

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

下列模組 my-func2.mjs 與該模組等效

const GREETING = 'Hello!';
function greet() {
  return GREETING;
}

export {
  greet as default,
};

對於匯入,我們可以使用一般的預設匯入

import myFunc from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

或者,我們可以使用命名匯入

import {default as myFunc} from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

預設匯出也可以透過命名空間匯入的屬性 .default 取得

import * as mf from './my-func2.mjs';
assert.equal(mf.default(), 'Hello!');

  default 不是作為變數名稱是非法的嗎?

default 無法作為變數名稱,但它可以作為匯出名稱和屬性名稱

const obj = {
  default: 123,
};
assert.equal(obj.default, 123);

27.8 更多關於匯出和匯入的詳細資訊

27.8.1 匯入是匯出的唯讀檢視

到目前為止,我們已經直覺地使用匯入和匯出,而且一切似乎都如預期般運作。但現在是時候仔細探討匯入和匯出的實際關聯性了。

考慮下列兩個模組

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
assert.equal(counter, 3);
incCounter();
assert.equal(counter, 4);

請注意,雖然連線是即時的,而且我們可以讀取 counter,但我們無法變更這個變數(例如透過 counter++)。

以這種方式處理匯入有兩個好處

27.8.2 ESM 對循環匯入的透明支援(進階)

ESM 透明地支援循環匯入。為了瞭解如何達成此目的,請考慮下列範例:圖 7 顯示匯入其他模組的模組有向圖。此情況中,P 匯入 M 是循環。

Figure 7: A directed graph of modules importing modules: M imports N and O, N imports P and Q, etc.

在剖析後,這些模組會分兩個階段設定

這種方法可以正確地處理循環匯入,這是因為 ES 模組的兩個特點

27.9 npm 套件

npm 軟體註冊中心是分發 JavaScript 函式庫和應用程式的主要方式,適用於 Node.js 和網路瀏覽器。它透過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"
}

其中一些屬性包含簡單的元資料

其他屬性啟用進階設定

有關 package.json 的更多資訊,請參閱 npm 文件

27.9.1 套件安裝在目錄 node_modules/

npm 始終在目錄 node_modules 內安裝套件。通常有許多這樣的目錄。npm 使用哪一個,取決於目前所在的目錄。例如,如果我們在目錄 /tmp/a/b/ 內,npm 會嘗試在目前目錄、其父目錄、父目錄的父目錄等中找到 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 鏈向上爬,並搜尋以下位置

27.9.2 為什麼可以使用 npm 安裝前端函式庫?

node_modules 目錄中尋找已安裝的模組,僅在 Node.js 上受支援。那麼,為什麼我們也可以使用 npm 安裝瀏覽器的函式庫?

這是透過 套件工具(例如 webpack)啟用的,這些工具會在線上部署程式碼之前,編譯和最佳化程式碼。在此編譯過程中,npm 套件中的程式碼會進行調整,以便在瀏覽器中運作。

27.10 命名模組

對於命名模組檔案及其匯入的變數,沒有既定的最佳實務做法。

在本章中,我使用以下命名樣式

這種樣式的基本原理是什麼?

我也喜歡使用底線分隔的模組檔案名稱,因為我們可以直接使用這些名稱進行命名空間匯入(無需任何轉換)

import * as my_module from './my_module.mjs';

但這種樣式不適用於預設匯入:我喜歡使用底線分隔的命名空間物件,但它不適合用於函式等。

27.11 模組指定符

模組規範符號 是用來識別模組的字串。它們在瀏覽器和 Node.js 中的運作方式略有不同。在探討差異之前,我們需要先了解模組規範符號的不同類別。

27.11.1 模組規範符號的類別

在 ES 模組中,我們區分以下類別的規範符號。這些類別源自 CommonJS 模組。

27.11.2 瀏覽器中的 ES 模組規範符號

瀏覽器處理模組規範符號的方式如下

請注意,將模組組合成較少檔案的組合工具(例如 webpack),通常對規範符號的嚴格性低於瀏覽器。這是因為它們在建置/編譯時間(而非執行時間)運作,並可以透過遍歷檔案系統來搜尋檔案。

27.11.3 Node.js 中的 ES 模組規範符號

Node.js 處理模組規範符號的方式如下

除了裸路徑之外,所有規範符號都必須指向實際檔案。也就是說,ESM 不支援以下 CommonJS 功能

所有內建的 Node.js 模組都可透過裸路徑取得,並具有命名的 ESM 匯出,例如

import * as assert from 'assert/strict';
import * as path from 'path';

assert.equal(
  path.join('a/b/c', '../d'), 'a/b/d');
27.11.3.1 Node.js 上的檔案副檔名

Node.js 支援下列預設檔案副檔名

檔案副檔名 .js 代表 ESM 或 CommonJS。它會透過「最近的」package.json(在目前目錄、父目錄等)進行設定。以這種方式使用 package.json 與套件無關。

package.json 中,有一個屬性 "type",它有兩個設定

27.11.3.2 將非檔案原始碼解譯為 CommonJS 或 ESM

並非所有由 Node.js 執行的原始碼都來自檔案。我們也可以透過 stdin、--eval--print 傳送程式碼給它。命令列選項 --input-type 讓我們可以指定如何解譯此類程式碼

27.12 import.meta – 目前模組的元資料 [ES2020]

物件 import.meta 包含目前模組的元資料。

27.12.1 import.meta.url

import.meta 最重要的屬性是 .url,它包含一個字串,其中有目前模組檔案的 URL – 例如

'https://example.com/code/main.mjs'

27.12.2 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.txtURL 執行個體

const urlOfData = new URL('data.txt', import.meta.url);

27.12.3 Node.js 上的 import.meta.url

在 Node.js 上,import.meta.url 永遠是一個包含 file: URL 的字串 – 例如

'file:///Users/rauschma/my-module.mjs'
27.12.3.1 範例:讀取模組的兄弟檔

許多 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'});
}
27.12.3.2 模組 `fs` 和 URL

對於模組 `fs` 的大部分函式,我們可以透過以下方式來參考檔案

有關此主題的更多資訊,請參閱 Node.js API 文件

27.12.3.3 在 `file:` URL 和路徑之間轉換

Node.js 模組 `url` 有兩個函式可用於在 `file:` URL 和路徑之間轉換

如果我們需要可以在本地檔案系統中使用的路徑,則 `URL` 執行個體的屬性 `pathname` 並非總是有效

assert.equal(
  new URL('file:///tmp/with%20space.txt').pathname,
  '/tmp/with%20space.txt');

因此,最好使用 `fileURLToPath()`

import * as url from 'url';
assert.equal(
  url.fileURLToPath('file:///tmp/with%20space.txt'),
  '/tmp/with space.txt'); // result on Unix

類似地,`pathToFileURL()` 不僅僅只是在絕對路徑前面加上 `'file://'`。

27.13 透過 `import()` 動態載入模組 [ES2020](進階)

  `import()` 運算子使用 Promise

Promise 是一種處理非同步運算(亦即非立即運算)結果的技術。它們在 §40「非同步程式設計的 Promise [ES6]」 中有說明。在您了解 Promise 之前,延後閱讀本節可能比較有意義。

27.13.1 靜態 `import` 陳述式的限制

到目前為止,載入模組的唯一方法是透過 `import` 陳述式。該陳述式有幾個限制

27.13.2 透過 `import()` 運算子動態載入

`import()` 運算子沒有 `import` 陳述式的限制。它看起來像這樣

import(moduleSpecifierStr)
.then((namespaceObject) => {
  console.log(namespaceObject.namedExport);
});

這個運算子像函式一樣使用,接收包含模組指定符的字串,並傳回解析為命名空間物件的 Promise。該物件的屬性是載入模組的匯出。

透過 await 使用 import() 更加方便

const namespaceObject = await import(moduleSpecifierStr);
console.log(namespaceObject.namedExport);

請注意,await 可用於模組的頂層(請參閱 下一節)。

讓我們來看一個使用 import() 的範例。

27.13.2.1 範例:動態載入模組

考慮下列檔案

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;
    assert.equal(result, 299792458);
    return result;
  });
}

mathOnDemand()
.then((result) => {
  assert.equal(result, 299792458);
});

此程式碼中有兩件事無法使用 import 陳述式完成

接下來,我們將透過稱為 非同步函式非同步/等待 的功能來實作與 main1.mjs 相同的功能,此功能提供更友善的 Promise 語法。

// main2.mjs
const moduleSpecifier = './lib/my-math.mjs';

async function mathOnDemand() {
  const myMath = await import(moduleSpecifier);
  const result = myMath.LIGHTSPEED;
  assert.equal(result, 299792458);
  return result;
}

  為何 import() 是運算子而非函式?

import() 看起來像函式,但無法實作為函式

27.13.3 import() 的使用案例

27.13.3.1 依需求載入程式碼

網路應用程式的某些功能不必在啟動時存在,它可以依需求載入。然後 import() 會有所幫助,因為我們可以將此類功能放入模組中,例如

button.addEventListener('click', event => {
  import('./dialogBox.mjs')
    .then(dialogBox => {
      dialogBox.open();
    })
    .catch(error => {
      /* Error handling */
    })
});
27.13.3.2 模組的條件式載入

我們可能想要在條件為真時載入模組。例如,具有 多重載入 的模組,可在舊有平台上提供新功能

if (isLegacyPlatform()) {
  import('./my-polyfill.mjs')
    .then(···);
}
27.13.3.3 計算模組指定項

對於國際化等應用程式,如果我們可以動態計算模組指定項,將有所幫助

import(`messages_${getLocale()}.mjs`)
  .then(···);

27.14 模組中的頂層 await [ES2022](進階)

  await 是非同步函式的功能

await§41「非同步函式」 中有說明。在了解非同步函式之前,延後閱讀本節可能比較有意義。

我們可以在模組的最上層使用 await 算子。如果我們這樣做,模組就會變成非同步,而且運作方式不同。感謝老天,我們通常不會看到這一點,因為語言會透明地處理它。

27.14.1 最上層 await 的使用案例

為什麼我們想要在模組的最上層使用 await 算子?它讓我們可以使用非同步載入的資料來初始化模組。接下來的 3 個小節會顯示 3 個有用的範例。

27.14.1.1 動態載入模組
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,這幾乎和使用一般的靜態載入一樣方便。

27.14.1.2 在模組載入失敗時使用備用方案
let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
27.14.1.3 使用載入最快的資源
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 會透過最先完成下載的資源來初始化。

27.14.2 最上層 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';
assert.equal(first, 'First!');
assert.equal(second, 'Second!');

兩個都大致等於以下程式碼

first.mjs:

export let first;
export const promise = (async () => { // (A)
  const response = await fetch('http://example.com/first.txt');
  first = await response.text();
})();

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)
  assert.equal(first, 'First content!');
  assert.equal(second, 'Second content!');
})();

一個模組會在以下情況下變成非同步

  1. 它直接使用最上層 awaitfirst.mjs)。
  2. 它載入一個或多個非同步模組(main.mjs)。

每個非同步模組會匯出一個 Promise(A 行和 B 行),在執行完主體後會完成。在那個時間點,存取該模組的匯出是安全的。

在案例 (2) 中,載入模組會等到所有載入的非同步模組的 Promise 完成後,才會進入其主體(C 行)。同步模組會像往常一樣處理。

等待的拒絕和同步例外會像在非同步函式中一樣管理。

27.14.3 最上層 await 的優缺點

最上層 await 的兩個最重要好處是

缺點是,最上層 await 會延遲載入模組的初始化。因此,最好謹慎使用。需要較長時間的非同步工作最好在稍後依需求執行。

然而,即使沒有頂層 await 的模組也可能阻擋匯入器(例如,透過頂層的無限迴圈),因此阻擋本身並非反對它的論點。

27.15 Polyfill:模擬原生網路平台功能(進階)

  後端也有 polyfill

本節探討前端開發和網路瀏覽器,但類似的概念也適用於後端開發。

Polyfill 有助於解決我們在 JavaScript 中開發網路應用程式時所面臨的衝突

假設有一個網路平台功能 X

每次我們的網路應用程式啟動時,它都必須先執行所有可能無法在各處使用的功能的 polyfill。之後,我們可以確定這些功能是原生可用的。

27.15.1 本節來源

  測驗

請參閱 測驗應用程式