帶你深入了解 Module
模塊介紹
當(dāng)我們的應(yīng)用程序變大時,我們想要把它分割成多個文件,也就是所謂的“模塊”。一個模塊可以包含一個用于特定目的的類或函數(shù)庫。
很長一段時間以來,JavaScript都沒有語言級的模塊語法。這不是問題,因為最初的腳本很小很簡單,所以沒有必要。
但最終腳本變得越來越復(fù)雜,因此社區(qū)發(fā)明了各種方法來將代碼組織到模塊中,以及根據(jù)需要加載模塊的特殊庫。
AMD——最古老的模塊系統(tǒng)之一,最初由require.js庫實現(xiàn)。
CommonJS -為Node.js服務(wù)器創(chuàng)建的模塊系統(tǒng)。
UMD -另一個模塊系統(tǒng),建議作為一個通用的,兼容AMD和CommonJS。
現(xiàn)在所有這些慢慢地成為歷史的一部分,但我們?nèi)匀豢梢栽诠爬系哪_本中找到它們。
語言級模塊系統(tǒng)于2015年出現(xiàn)在標(biāo)準(zhǔn)中,后來逐漸演變,現(xiàn)在所有主流瀏覽器和Node.js都支持它。因此,我們將從現(xiàn)在開始學(xué)習(xí)現(xiàn)代JavaScript模塊。
什么是模塊
模塊只是一個文件。一個腳本就是一個模塊。就這么簡單。
模塊可以相互加載,并使用特殊的指令導(dǎo)出和導(dǎo)入來交換功能,從一個模塊調(diào)用另一個模塊的函數(shù):
export關(guān)鍵字標(biāo)簽變量和函數(shù),這些變量和函數(shù)應(yīng)該可以從當(dāng)前模塊外部訪問。import允許從其他模塊導(dǎo)入功能。
例如,如果我們有一個文件sayHi.js導(dǎo)出一個函數(shù):
// ?? sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
然后另一個文件可以導(dǎo)入并使用它:
// ?? main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
import指令通過相對于當(dāng)前文件的path ./sayHi.js加載模塊,并將導(dǎo)出的函數(shù)sayHi賦給相應(yīng)的變量。
讓我們在瀏覽器中運(yùn)行這個示例。
由于模塊支持特殊的關(guān)鍵字和特性,所以我們必須通過屬性
sayHi.js
export function sayHi(user) {
return `Hello, ${user}!`;
}
index.html
<!doctype html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
核心模塊功能
與“常規(guī)”腳本相比,模塊有什么不同?
有一些核心特性,對瀏覽器和服務(wù)器端JavaScript都有效。
use strict
默認(rèn)情況下,模塊總是使用嚴(yán)格模式的。例如,給未聲明的變量賦值會產(chǎn)生錯誤。
<script type="module">
a = 5; // error
</script>
塊級作用域
每個模塊都有自己的頂級作用域。換句話說,一個模塊中的頂級變量和函數(shù)在其他腳本中看不到。
在下面的例子中,導(dǎo)入了兩個腳本,hello.js嘗試使用user.js中聲明的user變量:
user.js
let user = "John";
hello.js
alert(user); // no such variable (each module has independent variables)
index.html
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>
模塊應(yīng)該導(dǎo)出它們希望從外部訪問的內(nèi)容,并導(dǎo)入它們需要的內(nèi)容。
因此,我們應(yīng)該將user.js導(dǎo)入到hello.js中,并從中獲取所需的功能,而不是依賴全局變量。
這是正確的變體:
user.js
export let user = "John";
hello.js
import {user} from './user.js';
document.body.innerHTML = user; // John
index.html
<!doctype html>
<script type="module" src="hello.js"></script>
在瀏覽器中,每個<script type="module">對象都有獨立的頂級作用域
<script type="module">
// The variable is only visible in this module script
let user = "John";
</script>
<script type="module">
alert(user); // Error: user is not defined
</script>
如果我們真的需要創(chuàng)建一個窗口級全局變量,我們可以顯式地將它分配給window,并作為window.user訪問。但這是一個需要充分理由的例外。
模塊代碼只在第一次導(dǎo)入時才被求值
如果同一個模塊被導(dǎo)入到其他多個位置,它的代碼只在第一次執(zhí)行,然后導(dǎo)出將被交給所有導(dǎo)入器。
這有重要的后果。讓我們來看看他們的例子:
首先,如果執(zhí)行一個模塊代碼會帶來副作用,比如顯示一條消息,那么多次導(dǎo)入它只會觸發(fā)一次-第一次:
// ?? alert.js
alert("Module is evaluated!");
// Import the same module from different files
// ?? 1.js
import `./alert.js`; // Module is evaluated!
// ?? 2.js
import `./alert.js`; // (shows nothing)
在實踐中,頂級模塊代碼主要用于初始化、內(nèi)部數(shù)據(jù)結(jié)構(gòu)的創(chuàng)建,如果我們想要某些東西可重用—導(dǎo)出它。
現(xiàn)在,一個更高級的例子。
比方說,一個模塊導(dǎo)出了一個對象:
// ?? admin.js
export let admin = {
name: "John"
};
如果從多個文件導(dǎo)入此模塊,則只在第一次評估該模塊,創(chuàng)建admin對象,然后傳遞給所有進(jìn)一步的導(dǎo)入器。
所有的導(dǎo)入器都只有一個admin對象:
// ?? 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// ?? 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// Both 1.js and 2.js imported the same object
// Changes made in 1.js are visible in 2.js
所以,讓我們重申一下——這個模塊只執(zhí)行一次。導(dǎo)出將生成,然后它們將在導(dǎo)入器之間共享,因此,如果管理對象發(fā)生了更改,其他模塊將看到這一點。
這樣的行為允許我們在第一次導(dǎo)入時配置模塊。我們可以設(shè)置它的屬性一次,然后在進(jìn)一步導(dǎo)入時,它就準(zhǔn)備好了。
例如,admin.js模塊可能提供某些功能,但希望憑據(jù)從外部進(jìn)入admin對象:
// ?? admin.js
export let admin = { };
export function sayHi() {
alert(`Ready to serve, ${admin.name}!`);
}
在init.js 在應(yīng)用程序的第一個腳本中,我們設(shè)置admin.name。然后所有人都會看到它,包括從admin.js內(nèi)部調(diào)用:
// ?? init.js
import {admin} from './admin.js';
admin.name = "Pete";
另一個模塊也可以看到admin.name:
// ?? other.js
import {admin, sayHi} from './admin.js';
alert(admin.name); // Pete
sayHi(); // Ready to serve, Pete!
import.meta
導(dǎo)入的對象。元包含關(guān)于當(dāng)前模塊的信息。
它的內(nèi)容取決于環(huán)境。在瀏覽器中,它包含腳本的url,或者當(dāng)前網(wǎng)頁的url,如果在HTML中:
<script type="module">
alert(import.meta.url); // script url (url of the html page for an inline script)
</script>
In a module, “this” is undefined
這是一個小特性,但是為了完整性,我們應(yīng)該提到它。
在模塊中,這是未定義的頂層。
與非模塊腳本相比,它是一個全局對象:
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
瀏覽器 特定功能
與常規(guī)的腳本相比,使用type="module"的腳本還有一些特定于瀏覽器的差異。
如果您是第一次閱讀,或者您沒有在瀏覽器中使用JavaScript,那么您可能想要跳過這一部分。
模塊腳本被延遲
<script type="module">
alert(typeof button); // object: the script can 'see' the button below
// as modules are deferred, the script runs after the whole page is loaded
</script>
Compare to regular script below:
<script>
alert(typeof button); // button is undefined, the script can't see elements below
// regular scripts run immediately, before the rest of the page is processed
</script>
<button id="button">Button</button>
請注意:第二個腳本實際上在第一個腳本之前運(yùn)行!首先是undefined,然后是object。
這是因為模塊被延遲了,所以我們等待文檔被處理。常規(guī)腳本立即運(yùn)行,所以我們首先看到它的輸出。
當(dāng)使用模塊時,我們應(yīng)該注意HTML頁面在加載時顯示,JavaScript模塊在加載后運(yùn)行,所以用戶可能在JavaScript應(yīng)用程序準(zhǔn)備好之前看到頁面。有些功能可能還不能工作。我們應(yīng)該設(shè)置“加載指示符”,否則將確保訪問者不會被混淆。
異步在內(nèi)聯(lián)腳本上工作
對于非模塊腳本,async屬性只對外部腳本有效。異步腳本在準(zhǔn)備好后立即運(yùn)行,獨立于其他腳本或HTML文檔。
對于模塊腳本,它也適用于內(nèi)聯(lián)腳本。
例如,下面的內(nèi)聯(lián)腳本是異步的,所以它不等待任何東西。
它執(zhí)行導(dǎo)入(fetch ./analytics.js)并在準(zhǔn)備好時運(yùn)行,即使HTML文檔還沒有完成,或者其他腳本仍在等待中。
這對于不依賴于任何東西的功能來說是很好的,比如計數(shù)器、廣告、文檔級事件偵聽器。
<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
外部腳本
有type="module"的外部腳本有兩個不同:
具有相同src的外部腳本只運(yùn)行一次:
<!-- the script my.js is fetched and executed only once -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
從另一個來源(例如另一個站點)獲取的外部腳本需要CORS頭,如“獲取:跨來源請求”章節(jié)所述。換句話說,如果一個模塊腳本是從另一個來源獲取的,遠(yuǎn)程服務(wù)器必須提供一個頭部Access-Control-Allow-Origin允許獲取。
<!-- another-site.com must supply Access-Control-Allow-Origin -->
<!-- otherwise, the script won't execute -->
<script type="module" src="http://another-site.com/their.js"></script>
不允許出現(xiàn)裸模塊
在瀏覽器中,import必須獲得一個相對URL或絕對URL。沒有任何路徑的模塊稱為“裸”模塊。這樣的模塊是不允許導(dǎo)入的。
例如,此導(dǎo)入無效:
import {sayHi} from 'sayHi'; // Error, "bare" module
// the module must have a path, e.g. './sayHi.js' or wherever the module is
Compatibility, “nomodule”
舊的瀏覽器不理解type="module"。未知類型的腳本將被忽略。對于它們,可以使用nomodule屬性提供回退:
<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>
