W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
隨著我們的應(yīng)用越來(lái)越大,我們想要將其拆分成多個(gè)文件,即所謂的“模塊(module)”。一個(gè)模塊可以包含用于特定目的的類(lèi)或函數(shù)庫(kù)。
很長(zhǎng)一段時(shí)間,JavaScript 都沒(méi)有語(yǔ)言級(jí)(language-level)的模塊語(yǔ)法。這不是一個(gè)問(wèn)題,因?yàn)樽畛醯哪_本又小又簡(jiǎn)單,所以沒(méi)必要將其模塊化。
但是最終腳本變得越來(lái)越復(fù)雜,因此社區(qū)發(fā)明了許多種方法來(lái)將代碼組織到模塊中,使用特殊的庫(kù)按需加載模塊。
列舉一些(出于歷史原因):
現(xiàn)在,它們都在慢慢成為歷史的一部分,但我們?nèi)匀豢梢栽谂f腳本中找到它們。
語(yǔ)言級(jí)的模塊系統(tǒng)在 2015 年的時(shí)候出現(xiàn)在了標(biāo)準(zhǔn)(ES6)中,此后逐漸發(fā)展,現(xiàn)在已經(jīng)得到了所有主流瀏覽器和 Node.js 的支持。因此,我們將從現(xiàn)在開(kāi)始學(xué)習(xí)現(xiàn)代 JavaScript 模塊(module)。
一個(gè)模塊(module)就是一個(gè)文件。一個(gè)腳本就是一個(gè)模塊。就這么簡(jiǎn)單。
模塊可以相互加載,并可以使用特殊的指令 export
和 import
來(lái)交換功能,從另一個(gè)模塊調(diào)用一個(gè)模塊的函數(shù):
export
? 關(guān)鍵字標(biāo)記了可以從當(dāng)前模塊外部訪問(wèn)的變量和函數(shù)。import
? 關(guān)鍵字允許從其他模塊導(dǎo)入功能。例如,我們有一個(gè) sayHi.js
文件導(dǎo)出了一個(gè)函數(shù):
// sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
……然后另一個(gè)文件可能導(dǎo)入并使用了這個(gè)函數(shù):
// main.js
import { sayHi } from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
import
指令通過(guò)相對(duì)于當(dāng)前文件的路徑 ./sayHi.js
加載模塊,并將導(dǎo)入的函數(shù) sayHi
分配(assign)給相應(yīng)的變量。
讓我們?cè)跒g覽器中運(yùn)行一下這個(gè)示例。
由于模塊支持特殊的關(guān)鍵字和功能,因此我們必須通過(guò)使用 <script type="module">
特性(attribute)來(lái)告訴瀏覽器,此腳本應(yīng)該被當(dāng)作模塊(module)來(lái)對(duì)待。
像這樣:
export function sayHi(user) {
return `Hello, ${user}!`;
}
<!doctype html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
瀏覽器會(huì)自動(dòng)獲取并解析(evaluate)導(dǎo)入的模塊(如果需要,還可以分析該模塊的導(dǎo)入),然后運(yùn)行該腳本。
模塊只通過(guò) HTTP(s) 工作,而非本地
如果你嘗試通過(guò)
file://
協(xié)議在本地打開(kāi)一個(gè)網(wǎng)頁(yè),你會(huì)發(fā)現(xiàn)import/export
指令不起作用。你可以使用本地 Web 服務(wù)器,例如 static-server,或者使用編輯器的“實(shí)時(shí)服務(wù)器”功能,例如 VS Code 的 Live Server Extension 來(lái)測(cè)試模塊。
與“常規(guī)”腳本相比,模塊有什么不同呢?
下面是一些核心的功能,對(duì)瀏覽器和服務(wù)端的 JavaScript 來(lái)說(shuō)都有效。
模塊始終在嚴(yán)格模式下運(yùn)行。例如,對(duì)一個(gè)未聲明的變量賦值將產(chǎn)生錯(cuò)誤(譯注:在瀏覽器控制臺(tái)可以看到 error 信息)。
<script type="module">
a = 5; // error
</script>
每個(gè)模塊都有自己的頂級(jí)作用域(top-level scope)。換句話說(shuō),一個(gè)模塊中的頂級(jí)作用域變量和函數(shù)在其他腳本中是不可見(jiàn)的。
在下面這個(gè)例子中,我們導(dǎo)入了兩個(gè)腳本,hello.js
嘗試使用在 user.js
中聲明的變量 user
。它失敗了,因?yàn)樗且粋€(gè)單獨(dú)的模塊:
alert(user); // no such variable (each module has independent variables)
let user = "John";
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>
模塊應(yīng)該 export
它們想要被外部訪問(wèn)的內(nèi)容,并 import
它們所需要的內(nèi)容。
user.js
? 應(yīng)該導(dǎo)出 ?user
? 變量。hello.js
? 應(yīng)該從 ?user.js
? 模塊中導(dǎo)入它。換句話說(shuō),對(duì)于模塊,我們使用導(dǎo)入/導(dǎo)出而不是依賴全局變量。
這是正確的變體:
import {user} from './user.js';
document.body.innerHTML = user; // John
export let user = "John";
<!doctype html>
<script type="module" src="hello.js"></script>
在瀏覽器中,對(duì)于 HTML 頁(yè)面,每個(gè) <script type="module">
都存在獨(dú)立的頂級(jí)作用域。
下面是同一頁(yè)面上的兩個(gè)腳本,都是 type="module"
。它們看不到彼此的頂級(jí)變量:
<script type="module">
// 變量?jī)H在這個(gè) module script 內(nèi)可見(jiàn)
let user = "John";
</script>
<script type="module">
alert(user); // Error: user is not defined
</script>
請(qǐng)注意:
在瀏覽器中,我們可以通過(guò)將變量顯式地分配給
window
的一個(gè)屬性,使其成為窗口級(jí)別的全局變量。例如window.user = "John"
。
這樣所有腳本都會(huì)看到它,無(wú)論腳本是否帶有
type="module"
。
也就是說(shuō),創(chuàng)建這種全局變量并不是一個(gè)好的方式。請(qǐng)盡量避免這樣做。
如果同一個(gè)模塊被導(dǎo)入到多個(gè)其他位置,那么它的代碼只會(huì)執(zhí)行一次,即在第一次被導(dǎo)入時(shí)。然后將其導(dǎo)出(export)的內(nèi)容提供給進(jìn)一步的導(dǎo)入(importer)。
只執(zhí)行一次會(huì)產(chǎn)生很重要的影響,我們應(yīng)該意識(shí)到這一點(diǎn)。
讓我們看幾個(gè)例子。
首先,如果執(zhí)行一個(gè)模塊中的代碼會(huì)帶來(lái)副作用(side-effect),例如顯示一條消息,那么多次導(dǎo)入它只會(huì)觸發(fā)一次顯示 —— 即第一次:
// alert.js
alert("Module is evaluated!");
// 在不同的文件中導(dǎo)入相同的模塊
// 1.js
import `./alert.js`; // Module is evaluated!
// 2.js
import `./alert.js`; // (什么都不顯示)
第二次導(dǎo)入什么也沒(méi)顯示,因?yàn)槟K已經(jīng)執(zhí)行過(guò)了。
這里有一條規(guī)則:頂層模塊代碼應(yīng)該用于初始化,創(chuàng)建模塊特定的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。如果我們需要多次調(diào)用某些東西 —— 我們應(yīng)該將其以函數(shù)的形式導(dǎo)出,就像我們?cè)谏厦媸褂?nbsp;sayHi
那樣。
現(xiàn)在,讓我們看一個(gè)更復(fù)雜的例子。
我們假設(shè)一個(gè)模塊導(dǎo)出了一個(gè)對(duì)象:
// admin.js
export let admin = {
name: "John"
};
如果這個(gè)模塊被導(dǎo)入到多個(gè)文件中,模塊僅在第一次被導(dǎo)入時(shí)被解析,并創(chuàng)建 admin
對(duì)象,然后將其傳入到所有的導(dǎo)入。
所有的導(dǎo)入都只獲得了一個(gè)唯一的 admin
對(duì)象:
// 1.js
import { admin } from './admin.js';
admin.name = "Pete";
// 2.js
import { admin } from './admin.js';
alert(admin.name); // Pete
// 1.js 和 2.js 引用的是同一個(gè) admin 對(duì)象
// 在 1.js 中對(duì)對(duì)象做的更改,在 2.js 中也是可見(jiàn)的
正如你所看到的,當(dāng)在 1.js
中修改了導(dǎo)入的 admin
中的 name
屬性時(shí),我們?cè)?nbsp;2.js
中可以看到新的 admin.name
。
這正是因?yàn)樵撃K只執(zhí)行了一次。生成導(dǎo)出,然后這些導(dǎo)出在導(dǎo)入之間共享,因此如果更改了 admin
對(duì)象,在其他導(dǎo)入中也會(huì)看到。
這種行為實(shí)際上非常方便,因?yàn)樗试S我們“配置”模塊。
換句話說(shuō),模塊可以提供需要配置的通用功能。例如身份驗(yàn)證需要憑證。那么模塊可以導(dǎo)出一個(gè)配置對(duì)象,期望外部代碼可以對(duì)其進(jìn)行賦值。
這是經(jīng)典的使用模式:
例如,admin.js
模塊可能提供了某些功能(例如身份驗(yàn)證),但希望憑證可以從模塊之外賦值到 config
對(duì)象:
// admin.js
export let config = { };
export function sayHi() {
alert(`Ready to serve, ${config.user}!`);
}
這里,admin.js
導(dǎo)出了 config
對(duì)象(最初是空的,但也可能有默認(rèn)屬性)。
然后,在 init.js
中,我們應(yīng)用的第一個(gè)腳本,我們從 init.js
導(dǎo)入了 config
并設(shè)置了 config.user
:
// init.js
import { config } from './admin.js';
config.user = "Pete";
……現(xiàn)在模塊 admin.js
已經(jīng)是被配置過(guò)的了。
其他導(dǎo)入可以調(diào)用它,它會(huì)正確顯示當(dāng)前用戶:
// another.js
import { sayHi } from './admin.js';
sayHi(); // Ready to serve, Pete!
import.meta
對(duì)象包含關(guān)于當(dāng)前模塊的信息。
它的內(nèi)容取決于其所在的環(huán)境。在瀏覽器環(huán)境中,它包含當(dāng)前腳本的 URL,或者如果它是在 HTML 中的話,則包含當(dāng)前頁(yè)面的 URL。
<script type="module">
alert(import.meta.url); // 腳本的 URL
// 對(duì)于內(nèi)聯(lián)腳本來(lái)說(shuō),則是當(dāng)前 HTML 頁(yè)面的 URL
</script>
這是一個(gè)小功能,但為了完整性,我們應(yīng)該提到它。
在一個(gè)模塊中,頂級(jí) this
是 undefined。
將其與非模塊腳本進(jìn)行比較會(huì)發(fā)現(xiàn),非模塊腳本的頂級(jí) this
是全局對(duì)象:
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
與常規(guī)腳本相比,擁有 type="module"
標(biāo)識(shí)的腳本有一些特定于瀏覽器的差異。
如果你是第一次閱讀或者你不打算在瀏覽器中使用 JavaScript,那么你可以跳過(guò)本節(jié)內(nèi)容。
模塊腳本 總是 被延遲的,與 defer
特性(在 腳本:async,defer 一章中描述的)對(duì)外部腳本和內(nèi)聯(lián)腳本(inline script)的影響相同。
也就是說(shuō):
<script type="module" src="...">
? 不會(huì)阻塞 HTML 的處理,它們會(huì)與其他資源并行加載。它的一個(gè)副作用是,模塊腳本總是會(huì)“看到”已完全加載的 HTML 頁(yè)面,包括在它們下方的 HTML 元素。
例如:
<script type="module">
alert(typeof button); // object:腳本可以“看見(jiàn)”下面的 button
// 因?yàn)槟K是被延遲的(deferred,所以模塊腳本會(huì)在整個(gè)頁(yè)面加載完成后才運(yùn)行
</script>
相較于下面這個(gè)常規(guī)腳本:
<script>
alert(typeof button); // button 為 undefined,腳本看不到下面的元素
// 常規(guī)腳本會(huì)立即運(yùn)行,常規(guī)腳本的運(yùn)行是在在處理頁(yè)面的其余部分之前進(jìn)行的
</script>
<button id="button">Button</button>
請(qǐng)注意:上面的第二個(gè)腳本實(shí)際上要先于前一個(gè)腳本運(yùn)行!所以我們會(huì)先看到 undefined
,然后才是 object
。
這是因?yàn)槟K腳本是被延遲的,所以要等到 HTML 文檔被處理完成才會(huì)執(zhí)行它。而常規(guī)腳本則會(huì)立即運(yùn)行,所以我們會(huì)先看到常規(guī)腳本的輸出。
當(dāng)使用模塊腳本時(shí),我們應(yīng)該知道 HTML 頁(yè)面在加載時(shí)就會(huì)顯示出來(lái),在 HTML 頁(yè)面加載完成后才會(huì)執(zhí)行 JavaScript 模塊,因此用戶可能會(huì)在 JavaScript 應(yīng)用程序準(zhǔn)備好之前看到該頁(yè)面。某些功能那時(shí)可能還無(wú)法正使用。我們應(yīng)該放置“加載指示器(loading indicator)”,否則,請(qǐng)確保不會(huì)使用戶感到困惑。
對(duì)于非模塊腳本,async
特性(attribute)僅適用于外部腳本。異步腳本會(huì)在準(zhǔn)備好后立即運(yùn)行,獨(dú)立于其他腳本或 HTML 文檔。
對(duì)于模塊腳本,它也適用于內(nèi)聯(lián)腳本。
例如,下面的內(nèi)聯(lián)腳本具有 async
特性,因此它不會(huì)等待任何東西。
它執(zhí)行導(dǎo)入(fetch ./analytics.js
),并在導(dǎo)入完成時(shí)運(yùn)行,即使 HTML 文檔還未完成,或者其他腳本仍在等待處理中。
這對(duì)于不依賴任何其他東西的功能來(lái)說(shuō)是非常棒的,例如計(jì)數(shù)器,廣告,文檔級(jí)事件監(jiān)聽(tīng)器。
<!-- 所有依賴都獲取完成(analytics.js)然后腳本開(kāi)始運(yùn)行 -->
<!-- 不會(huì)等待 HTML 文檔或者其他 <script> 標(biāo)簽 -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
具有 type="module"
的外部腳本(external script)在兩個(gè)方面有所不同:
src
的外部腳本僅運(yùn)行一次:<!-- 腳本 my.js 被加載完成(fetched)并只被運(yùn)行一次 -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
Access-Control-Allow-Origin
。<!-- another-site.com 必須提供 Access-Control-Allow-Origin -->
<!-- 否則,腳本將無(wú)法執(zhí)行 -->
<script type="module" src="http://another-site.com/their.js" rel="external nofollow" ></script>
默認(rèn)這樣做可以確保更好的安全性。
在瀏覽器中,import
必須給出相對(duì)或絕對(duì)的 URL 路徑。沒(méi)有任何路徑的模塊被稱為“裸(bare)”模塊。在 import
中不允許這種模塊。
例如,下面這個(gè) import
是無(wú)效的:
import {sayHi} from 'sayHi'; // Error,“裸”模塊
// 模塊必須有一個(gè)路徑,例如 './sayHi.js' 或者其他任何路徑
某些環(huán)境,像 Node.js 或者打包工具(bundle tool)允許沒(méi)有任何路徑的裸模塊,因?yàn)樗鼈冇凶约旱牟檎夷K的方法和鉤子(hook)來(lái)對(duì)它們進(jìn)行微調(diào)。但是瀏覽器尚不支持裸模塊。
舊時(shí)的瀏覽器不理解 type="module"
。未知類(lèi)型的腳本會(huì)被忽略。對(duì)此,我們可以使用 nomodule
特性來(lái)提供一個(gè)后備:
<script type="module">
alert("Runs in modern browsers");
</script>
<script nomodule>
alert("Modern browsers know both type=module and nomodule, so skip this")
alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>
在實(shí)際開(kāi)發(fā)中,瀏覽器模塊很少被以“原始”形式進(jìn)行使用。通常,我們會(huì)使用一些特殊工具,例如 Webpack,將它們打包在一起,然后部署到生產(chǎn)環(huán)境的服務(wù)器。
使用打包工具的一個(gè)好處是 —— 它們可以更好地控制模塊的解析方式,允許我們使用裸模塊和更多的功能,例如 CSS/HTML 模塊等。
構(gòu)建工具做以下這些事兒:
<script type="module">
? “主”模塊開(kāi)始。import
? 調(diào)用,以使其正常工作。還支持像 HTML/CSS 模塊等“特殊”的模塊類(lèi)型。console
? 和 ?debugger
? 這樣的語(yǔ)句。如果我們使用打包工具,那么腳本會(huì)被打包進(jìn)一個(gè)單一文件(或者幾個(gè)文件),在這些腳本中的 import/export
語(yǔ)句會(huì)被替換成特殊的打包函數(shù)(bundler function)。因此,最終打包好的腳本中不包含任何 import/export
,它也不需要 type="module"
,我們可以將其放入常規(guī)的 <script>
:
<!-- 假設(shè)我們從諸如 Webpack 這類(lèi)的打包工具中獲得了 "bundle.js" 腳本 -->
<script src="bundle.js"></script>
關(guān)于構(gòu)建工具說(shuō)了這么多,但其實(shí)原生模塊也是可以用的。所以,我們?cè)谶@兒將不會(huì)使用 Webpack:你可以稍后再配置它。
下面總結(jié)一下模塊的核心概念:
<script type="module">
? 以使 ?import/export
? 可以工作。模塊(譯注:相較于常規(guī)腳本)有幾點(diǎn)差別:import/export
? 交換功能。
use strict
?。
當(dāng)我們使用模塊時(shí),每個(gè)模塊都會(huì)實(shí)現(xiàn)特定功能并將其導(dǎo)出。然后我們使用 import
將其直接導(dǎo)入到需要的地方即可。瀏覽器會(huì)自動(dòng)加載并解析腳本。
在生產(chǎn)環(huán)境中,出于性能和其他原因,開(kāi)發(fā)者經(jīng)常使用諸如 Webpack 之類(lèi)的打包工具將模塊打包到一起。
在下一章里,我們將會(huì)看到更多關(guān)于模塊的例子,以及如何進(jìn)行導(dǎo)入/導(dǎo)出。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: