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()
)。這在網路上並不總是允許的。
觀察 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 軟體註冊中心是分發 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"
}
其中一些屬性包含簡單的元資料
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_modules
目錄中尋找已安裝的模組,僅在 Node.js 上受支援。那麼,為什麼我們也可以使用 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 處理模組規範符號的方式如下
相對路徑的解析方式與網路瀏覽器相同,也就是相對於目前模組的路徑。
目前不支援絕對路徑。作為解決方法,我們可以使用以 file:///
開頭的 URL。我們可以透過 url.pathToFileURL()
來建立此類 URL。
僅支援 file:
作為 URL 規範符號的協定。
裸路徑會被解釋為套件名稱,並相對於最接近的 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
.cjs
檔案副檔名 .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` 的大部分函式,我們可以透過以下方式來參考檔案
有關此主題的更多資訊,請參閱 Node.js API 文件。
Node.js 模組 `url` 有兩個函式可用於在 `file:` URL 和路徑之間轉換
fileURLToPath(url: URL|string): string
pathToFileURL(path: string): 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()` 運算子使用 Promise
Promise 是一種處理非同步運算(亦即非立即運算)結果的技術。它們在 §40「非同步程式設計的 Promise [ES6]」 中有說明。在您了解 Promise 之前,延後閱讀本節可能比較有意義。
到目前為止,載入模組的唯一方法是透過 `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()
的範例。
考慮下列檔案
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
相同的功能,此功能提供更友善的 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()
的使用案例網路應用程式的某些功能不必在啟動時存在,它可以依需求載入。然後 import()
會有所幫助,因為我們可以將此類功能放入模組中,例如
.addEventListener('click', event => {
buttonimport('./dialogBox.mjs')
.then(dialogBox => {
.open();
dialogBox
}).catch(error => {
/* Error handling */
}); })
我們可能想要在條件為真時載入模組。例如,具有 多重載入 的模組,可在舊有平台上提供新功能
if (isLegacyPlatform()) {
import('./my-polyfill.mjs')
.then(···);
}
對於國際化等應用程式,如果我們可以動態計算模組指定項,將有所幫助
import(`messages_${getLocale()}.mjs`)
.then(···);
await
[ES2022](進階)
await
是非同步函式的功能
await
在 §41「非同步函式」 中有說明。在了解非同步函式之前,延後閱讀本節可能比較有意義。
我們可以在模組的最上層使用 await
算子。如果我們這樣做,模組就會變成非同步,而且運作方式不同。感謝老天,我們通常不會看到這一點,因為語言會透明地處理它。
await
的使用案例為什麼我們想要在模組的最上層使用 await
算子?它讓我們可以使用非同步載入的資料來初始化模組。接下來的 3 個小節會顯示 3 個有用的範例。
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 行),在執行完主體後會完成。在那個時間點,存取該模組的匯出是安全的。
在案例 (2) 中,載入模組會等到所有載入的非同步模組的 Promise 完成後,才會進入其主體(C 行)。同步模組會像往常一樣處理。
等待的拒絕和同步例外會像在非同步函式中一樣管理。
await
的優缺點最上層 await
的兩個最重要好處是
缺點是,最上層 await
會延遲載入模組的初始化。因此,最好謹慎使用。需要較長時間的非同步工作最好在稍後依需求執行。
然而,即使沒有頂層 await
的模組也可能阻擋匯入器(例如,透過頂層的無限迴圈),因此阻擋本身並非反對它的論點。
後端也有 polyfill
本節探討前端開發和網路瀏覽器,但類似的概念也適用於後端開發。
Polyfill 有助於解決我們在 JavaScript 中開發網路應用程式時所面臨的衝突
假設有一個網路平台功能 X
每次我們的網路應用程式啟動時,它都必須先執行所有可能無法在各處使用的功能的 polyfill。之後,我們可以確定這些功能是原生可用的。
測驗
請參閱 測驗應用程式。