什么是模塊
模塊就是javascript文件以不同方式去加載(這個(gè)就和scripts的方式相反了,scripts是以原始的javascript工作方式加載的)。這種不同的模式很有必要,并且他們代表的語義也是不一樣的:
- 模塊模式會(huì)自動(dòng)運(yùn)行在嚴(yán)格模式下,沒有辦法選擇。
- 模塊頂層創(chuàng)建的變量不會(huì)共享到全局范圍,僅在模塊的范圍內(nèi)。
- 模塊的頂層的
this值是undefined。 - 模塊不允許在代碼中使用HTML樣式的注釋(
JavaScript早期瀏覽器時(shí)代的遺留功能)。 - 模塊必須導(dǎo)出模塊外部代碼可用的任何內(nèi)容。
- 模塊可以從其他模塊導(dǎo)入綁定
這些差異乍一看似乎很小,但它們代表了JavaScript代碼加載和評(píng)估方式的重大變化,下面會(huì)進(jìn)行介紹。模塊的真正強(qiáng)大之處在于能夠僅導(dǎo)出和導(dǎo)入所需的內(nèi)容,而不是文件中的所有內(nèi)容。
Basic Exporting
使用export把外部需要的內(nèi)容導(dǎo)出。在最簡(jiǎn)單的情況下,將export放在任何變量,函數(shù)或類聲明的前面,這樣從模塊中導(dǎo)出它。
export var name = 'my name'
export const say = () => 'your name'
// this function is private to the module
function subtract(num1, num2) {
return num1 - num2;
}
// define a function...
function multiply(num1, num2) {
return num1 * num2;
}
// ...and then export it later
export { multiply };
可以看出,每個(gè)導(dǎo)出都有個(gè)名稱,除非你導(dǎo)出默認(rèn)(后面會(huì)說)。否則沒法子使用這個(gè)語法導(dǎo)出匿名函數(shù)或類。當(dāng)然也可以聲明之后再導(dǎo)出。
Basic Importing
import { identifier1, identifier2 } from "./example.js";
這個(gè)看起來類似于解構(gòu)對(duì)象,但它不是。
當(dāng)你從module import的時(shí)候,他的行為就像const一樣。這意味著無法定義具有相同名稱的另一個(gè)變量(包括導(dǎo)入同名的另一個(gè)export),在import語句之前使用標(biāo)識(shí)符,或更改值。
Importing All of a Module
import * as example from "./example.js";
這里值得一提的是,無論在import語句中使用模塊多少次,模塊都只執(zhí)行一次。在導(dǎo)入模塊的代碼執(zhí)行之后,實(shí)例化的模塊保存在內(nèi)存中,并在另一個(gè)import語句引用它時(shí)重用。
import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";
盡管這里使用了多次module,但是只會(huì)加載一次。如果同一應(yīng)用程序中的其他模塊要從example.js導(dǎo)入,那么這些模塊將使用此代碼使用的相同模塊實(shí)例。
模塊語法限制
導(dǎo)出和導(dǎo)入的一個(gè)重要限制是它們必須在其他語句和函數(shù)之外使用。if (flag) { export flag; // syntax error }
export在if語句里,這是不被允許的。export是不可以有條件的,也不可以以其他的方式動(dòng)態(tài)export。這是為了靜態(tài)的確定需要導(dǎo)出的內(nèi)容。
同樣的,import也存在這個(gè)限制:function tryImport() { import flag from "./example.js"; // syntax error }
你不可以動(dòng)態(tài)的導(dǎo)入模塊就像你不能動(dòng)態(tài)的導(dǎo)出模塊那樣。導(dǎo)出和導(dǎo)入關(guān)鍵字設(shè)計(jì)為靜態(tài),因此文本編輯器等工具可以輕松地告知模塊中可用的信息。
A Subtle Quirk of Imported Bindings
這個(gè)是有些意思,你可以在導(dǎo)出的內(nèi)部更改,但是導(dǎo)出之后,就不可以更改了。
# export.js
export var name = "Nicholas";
export function setName(newName) {
name = newName;
}
# import.js
import { name, setName } from "./example.js";
console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"
name = "Nicholas"; // error
Renaming Exports and Imports
這個(gè)就是通過as來改名字。
#export.js
function sum(num1, num2) {
return num1 + num2;
}
export { sum as add };
#import.js
import { add as sum } from "./example.js";
console.log(typeof add); // "undefined"
console.log(sum(1, 2)); // 3
Exporting Default Values
這個(gè)和上面的差不多,就是在導(dǎo)出的時(shí)候加上default關(guān)鍵字。大同小異吧。
Importing Without Bindings
某些模塊可能不會(huì)導(dǎo)出任何內(nèi)容,而只是對(duì)全局范圍內(nèi)的對(duì)象進(jìn)行修改。盡管模塊內(nèi)的頂級(jí)變量,函數(shù)和類不會(huì)自動(dòng)結(jié)束于全局范圍,但這并不意味著模塊無法訪問全局范圍。
可以在模塊內(nèi)部訪問內(nèi)置對(duì)象(如Array和Object)的共享定義,對(duì)這些對(duì)象的更改將反映在其他模塊中。
#export.js
// module code without exports or imports
Array.prototype.pushAll = function(items) {
// items must be an array
if (!Array.isArray(items)) {
throw new TypeError("Argument must be an array.");
}
// use built-in push() and spread operator
return this.push(...items);
};
#import.js
import "./export.js";
let colors = ["red", "green", "blue"];
let items = [];
items.pushAll(colors);
像這種沒有具體內(nèi)容的導(dǎo)出,多用于墊片之類的功能。
Loading Modules
雖然ECMAScript 6定義了模塊的語法,但它沒有定義如何加載它們。這是規(guī)范的復(fù)雜性的一部分,該規(guī)范應(yīng)該與實(shí)現(xiàn)環(huán)境無關(guān)。ECMAScript 6不是嘗試創(chuàng)建適用于所有JavaScript環(huán)境的單一規(guī)范,而是僅指定語法并將加載機(jī)制抽象為未定義的內(nèi)部操作HostResolveImportedModule。Web瀏覽器和Node.js將決定如何以對(duì)各自環(huán)境有意義的方式實(shí)現(xiàn)HostResolveImportedModule`。
Using Modules in Web Browsers
ECMAScript 6之前,Web瀏覽器就有多種方法可以在Web應(yīng)用程序中包含JavaScript。腳本加載有:
- 使用帶有
src屬性的<script>元素加載JavaScript代碼文件,該屬性指定加載代碼的位置。 - 使用沒有
src屬性的<script>元素嵌入JavaScript代碼。 - 加載
JavaScript代碼文件以作為worker(例如web worker或service worker)執(zhí)行。
為了完全支持模塊,Web瀏覽器必須更新每個(gè)機(jī)制。這些細(xì)節(jié)在HTML規(guī)范中定義,在這里對(duì)它們進(jìn)行總結(jié)。
Using Modules With <script>
<script>元素的默認(rèn)行為是將JavaScript文件作為腳本(而不是模塊)加載。缺少type屬性或type屬性包含JavaScript內(nèi)容類型(例如“text / javascript”)時(shí)會(huì)發(fā)生這種情況。然后,<script>元素可以執(zhí)行內(nèi)聯(lián)代碼或加載src中指定的文件。為了支持模塊,“模塊”值被添加??為類型選項(xiàng)。將類型設(shè)置為“module”會(huì)告訴瀏覽器將src指定的文件中包含的任何內(nèi)聯(lián)代碼或代碼作為模塊而不是腳本加載。
<!-- load a module JavaScript file -->
<script type="module" src="module.js"></script>
<!-- include a module inline -->
<script type="module">
import { sum } from "./example.js";
let result = sum(1, 2);
</script>
第一個(gè)加載外部的module.第二個(gè)<script>元素包含直接嵌入網(wǎng)頁的模塊。變量結(jié)果不會(huì)全局公開,因?yàn)樗鼉H存在于模塊中(由<script>元素定義),因此不會(huì)作為屬性添加到窗口中。
正如所見,包括網(wǎng)頁中的模塊相當(dāng)簡(jiǎn)單,類似于包含腳本。但是,模塊的加載方式存在一些差異。
可能已經(jīng)注意到“模塊”不是像
“text / javascript”類型那樣的內(nèi)容類型。模塊JavaScript文件使用與腳本JavaScript文件相同的內(nèi)容類型提供,因此無法僅根據(jù)內(nèi)容類型進(jìn)行區(qū)分。此外,當(dāng)類型無法識(shí)別時(shí),瀏覽器會(huì)忽略<script>元素,因此不支持模塊的瀏覽器將自動(dòng)忽略<script type =“module”>行,從而提供良好的向后兼容性。
Module Loading Sequence in Web Browsers
模塊的獨(dú)特之處在于,與腳本不同,它們可以使用import來指定必須加載其他文件才能正確執(zhí)行。為了支持該功能,<script type =“module”>始終表現(xiàn)為應(yīng)用了defer屬性。defer屬性對(duì)于加載腳本文件是可選的,但始終應(yīng)用于加載模塊文件。 一旦HTML解析器遇到帶有src屬性的<script type =“module”>,模塊文件就會(huì)開始下載,但是在完全解析Document之后才會(huì)執(zhí)行。模塊也按它們?cè)?code>HTML文件中出現(xiàn)的順序執(zhí)行。這意味著第一個(gè)<script type =“module”>始終保證在第二個(gè)之前執(zhí)行,即使一個(gè)模塊包含內(nèi)聯(lián)代碼而不是指定src。
關(guān)于defer和async作用的script可以戳這里了解
<!-- this will execute first -->
<script type="module" src="module1.js"></script>
<!-- this will execute second -->
<script type="module">
import { sum } from "./example.js";
let result = sum(1, 2);
</script>
<!-- this will execute third -->
<script type="module" src="module2.js"></script>
每個(gè)模塊可以從一個(gè)或多個(gè)其他模塊導(dǎo)入,這使問題復(fù)雜化。 這就是為什么首先完全解析模塊以識(shí)別所有import語句的原因。 然后,每個(gè)import語句都會(huì)觸發(fā)一次獲取(來自網(wǎng)絡(luò)或來自緩存),并且在首次加載和執(zhí)行所有導(dǎo)入資源之前不會(huì)執(zhí)行任何模塊。
所有模塊,包括使用<script type =“module”>顯式包含的模塊和使用import隱式包含的模塊,都按順序加載和執(zhí)行。在前面的示例中,完整的加載順序是:
- 下載解析
module1.js. - 遞歸下載并解析
module1.js中的導(dǎo)入資源。 - 解析內(nèi)聯(lián)模塊。
- 遞歸下載并解析內(nèi)聯(lián)模塊中的導(dǎo)入資源
- 下載解析
module2.js - 遞歸下載并解析
module2.js中的導(dǎo)入資源。
加載完成后,在文檔完全解析之后才會(huì)執(zhí)行任何操作。文檔解析完成后,將執(zhí)行以下操作:
- 遞歸執(zhí)行
module1.js的導(dǎo)入資源 - 執(zhí)行
module1.js - 遞歸執(zhí)行內(nèi)聯(lián)模塊的導(dǎo)入資源
- 執(zhí)行內(nèi)聯(lián)模塊
- 遞歸執(zhí)行
module2.js的導(dǎo)入資源 - 執(zhí)行
module2.js
內(nèi)聯(lián)模塊的作用與其他兩個(gè)模塊類似,不是先下載代碼。加載導(dǎo)入資源和執(zhí)行模塊的順序完全相同。
在
<script type =“module”>上會(huì)忽略??defer屬性,因?yàn)樗男袨榫拖駪?yīng)用了defer一樣。
Asynchronous Module Loading in Web Browsers
與腳本一起使用時(shí),async會(huì)在下載和解析文件后立即執(zhí)行腳本文件。 但是,文檔中異步腳本的順序不會(huì)影響腳本的執(zhí)行順序。 腳本在完成下載后始終執(zhí)行,而不等待包含文檔完成解析。
async屬性也可以應(yīng)用于模塊。 在<script type =“module”>上使用async會(huì)導(dǎo)致模塊以類似于腳本的方式執(zhí)行。 唯一的區(qū)別是模塊的所有導(dǎo)入資源都是在執(zhí)行模塊之前下載的。 這保證了模塊執(zhí)行前所需的所有資源都將被下載; 但是你無法保證模塊何時(shí)執(zhí)行??聪旅娴拇a:
<!-- 不確定哪個(gè)先被執(zhí)行 -->
<script type="module" async src="module1.js"></script>
<script type="module" async src="module2.js"></script>
Loading Modules as Workers
Web Worker和Service Worker等Worker在Web頁面上下文之外執(zhí)行JavaScript代碼。 創(chuàng)建新worker需要?jiǎng)?chuàng)建一個(gè)新的實(shí)例Worker(或另一個(gè)類)并傳入JavaScript文件的位置。 默認(rèn)加載機(jī)制是將文件作為腳本加載,如下所示:
// load script.js as a script
let worker = new Worker("script.js");
為了支持加載模塊,HTML標(biāo)準(zhǔn)的開發(fā)人員為這些構(gòu)造函數(shù)添加了第二個(gè)參數(shù),第二個(gè)參數(shù)是一個(gè)具有type屬性的對(duì)象,其默認(rèn)值為“script”。您可以將類型設(shè)置為“module”以加載模塊文件:
// load module.js as a module
let worker = new Worker("module.js", { type: "module" });
module type worker一般類似于script type worker,但是有些例外。首先,script worker限制于同源,但是module worker不存在這些同源限制。 雖然module worker具有相同的默認(rèn)限制,但它們也可以加載具有適當(dāng)?shù)目缭促Y源共享(CORS)標(biāo)頭的文件以允許訪問。其次,雖然script worker可以使用self.importScripts方法將其他腳本加載到worker中,但self.importScripts總是在module worker上失敗,因?yàn)楦鼞?yīng)該使用import。