您好,登錄后才能下訂單哦!
JavaScript引入模塊的歷史簡介是怎樣的,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
隨著我們的應用越來越大,我們想要將其拆分成多個文件,即所謂的“模塊(module)”。一個模塊可以包含用于特定目的的類或函數庫。
很長一段時間,JavaScript 都沒有語言級(language-level)的模塊語法。這不是一個問題,因為最初的腳本又小又簡單,所以沒必要將其模塊化。
但是最終腳本變得越來越復雜,因此社區發明了許多種方法來將代碼組織到模塊中,使用特殊的庫按需加載模塊。
列舉一些(出于歷史原因):
AMD —— 最古老的模塊系統之一,最初由 require.js 庫實現。
CommonJS —— 為 Node.js 服務器創建的模塊系統。
UMD —— 另外一個模塊系統,建議作為通用的模塊系統,它與 AMD 和 CommonJS 都兼容。
現在,它們都在慢慢成為歷史的一部分,但我們仍然可以在舊腳本中找到它們。
語言級的模塊系統在 2015 年的時候出現在了標準(ES6)中,此后逐漸發展,現在已經得到了所有主流瀏覽器和 Node.js 的支持。因此,我們將從現在開始學習現代 JavaScript 模塊(module)。
一、什么是模塊?
一個模塊(module)就是一個文件。一個腳本就是一個模塊。就這么簡單。
模塊可以相互加載,并可以使用特殊的指令 export 和 import 來交換功能,從另一個模塊調用一個模塊的函數:
export 關鍵字標記了可以從當前模塊外部訪問的變量和函數。
import 關鍵字允許從其他模塊導入功能。
例如,我們有一個 sayHi.js 文件導出了一個函數:
// sayHi.js export function sayHi(user) { alert(`Hello, ${user}!`); }
……然后另一個文件可能導入并使用了這個函數:
// main.js import {sayHi} from './sayHi.js'; alert(sayHi); // function... sayHi('John'); // Hello, John!
import 指令通過相對于當前文件的路徑 ./sayHi.js 加載模塊,并將導入的函數 sayHi 分配(assign)給相應的變量。
讓我們在瀏覽器中運行一下這個示例。
由于模塊支持特殊的關鍵字和功能,因此我們必須通過使用<script type="module"> 特性(attribute)來告訴瀏覽器,此腳本應該被當作模塊(module)來對待。
像這樣:
<!doctype html> <script type="module"> import {sayHi} from './say.js'; document.body.innerHTML = sayHi('John'); </script>
瀏覽器會自動獲取并解析(evaluate)導入的模塊(如果需要,還可以分析該模塊的導入),然后運行該腳本。
模塊只通過 HTTP(s) 工作,在本地文件則不行
如果你嘗試通過 file:// 協議在本地打開一個網頁,你會發現 import/export 指令不起作用。你可以使用本地 Web 服務器,例如 static-server,或者使用編輯器的“實時服務器”功能,例如 VS Code 的 Live Server Extension 來測試模塊。
二、模塊核心功能
與“常規”腳本相比,模塊有什么不同呢?
下面是一些核心的功能,對瀏覽器和服務端的 JavaScript 來說都有效。
三、始終使用 “use strict”
模塊始終默認使用 use strict,例如,對一個未聲明的變量賦值將產生錯誤(譯注:在瀏覽器控制臺可以看到 error 信息)。
<script type="module"> a = 5; // error </script>
四、模塊級作用域
每個模塊都有自己的頂級作用域(top-level scope)。換句話說,一個模塊中的頂級作用域變量和函數在其他腳本中是不可見的。
在下面這個例子中,我們導入了兩個腳本,hello.js 嘗試使用在 user.js 中聲明的變量 user,失敗了:
<!doctype html> <script type="module" src="user.js"></script> <script type="module" src="hello.js"></script>
模塊期望 export 它們想要被外部訪問的內容,并 import 它們所需要的內容。
所以,我們應該將 user.js 導入到 hello.js 中,并從中獲取所需的功能,而不要依賴于全局變量。
這是正確的變體:
import {user} from './user.js'; document.body.innerHTML = user; // John
在瀏覽器中,每個 <script type="module"> 也存在獨立的頂級作用域(譯注:在瀏覽器控制臺可以看到 error 信息)。
<script type="module"> // 變量僅在這個 module script 內可見 let user = "John"; </script> <script type="module"> alert(user); // Error: user is not defined </script>
如果我們真的需要創建一個 window-level 的全局變量,我們可以將其明確地賦值給 window,并以 window.user 來訪問它。但是這需要你有足夠充分的理由,否則就不要這樣做。
五、模塊代碼僅在第一次導入時被解析
如果同一個模塊被導入到多個其他位置,那么它的代碼僅會在第一次導入時執行,然后將導出(export)的內容提供給所有的導入(importer)。
這有很重要的影響。讓我們通過示例來看一下:
首先,如果執行一個模塊中的代碼會帶來副作用(side-effect),例如顯示一條消息,那么多次導入它只會觸發一次顯示 —— 即第一次:
// alert.js
alert("Module is evaluated!");
// 在不同的文件中導入相同的模塊 // 1.js import `./alert.js`; // Module is evaluated! // 2.js import `./alert.js`; // (什么都不顯示)
在實際開發中,頂級模塊代碼主要用于初始化,內部數據結構的創建,并且如果我們希望某些東西可以重用 — 請導出它。
下面是一個高級點的例子。
我們假設一個模塊導出了一個對象:
// admin.js export let admin = { name: "John" };
如果這個模塊被導入到多個文件中,模塊僅在第一次被導入時被解析,并創建 admin 對象,然后將其傳入到所有的導入。
所有的導入都只獲得了一個唯一的 admin 對象:
// 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 導入的是同一個對象 // 在 1.js 中對對象做的更改,在 2.js 中也是可見的
所以,讓我們重申一下 —— 模塊只被執行一次。生成導出,然后它被分享給所有對其的導入,所以如果某個地方修改了 admin 對象,其他的模塊也能看到這個修改。
這種行為讓我們可以在首次導入時 設置 模塊。我們只需要設置其屬性一次,然后在進一步的導入中就都可以直接使用了。
例如,下面的 admin.js 模塊可能提供了特定的功能,但是希望憑證(credential)從外部進入 admin 對象:
// admin.js export let admin = { }; export function sayHi() { alert(`Ready to serve, ${admin.name}!`); }
在 init.js 中 —— 我們 APP 的第一個腳本,設置了 admin.name。現在每個位置都能看到它,包括在 admin.js 內部的調用。
// 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
import.meta 對象包含關于當前模塊的信息。
它的內容取決于其所在的環境。在瀏覽器環境中,它包含當前腳本的 URL,或者如果它是在 HTML 中的話,則包含當前頁面的 URL。
<script type="module"> alert(import.meta.url); // 腳本的 URL(對于內嵌腳本來說,則是當前 HTML 頁面的 URL) </script>
七、在一個模塊中,“this” 是 undefined
這是一個小功能,但為了完整性,我們應該提到它。
在一個模塊中,頂級 this 是 undefined。
將其與非模塊腳本進行比較會發現,非模塊腳本的頂級 this 是全局對象:
<script> alert(this); // window </script> <script type="module"> alert(this); // undefined </script>
八、瀏覽器特定功能
與常規腳本相比,擁有 type="module" 標識的腳本有一些特定于瀏覽器的差異。
如果你是第一次閱讀或者你不打算在瀏覽器中使用 JavaScript,那么你可以跳過本節內容。
九、模塊腳本是延遲的
模塊腳本 總是 被延遲的,與 defer 特性(在 腳本:async,defer 一章中描述的)對外部腳本和內聯腳本(inline script)的影響相同。
也就是說:
下載外部模塊腳本 <script type="module" src="..."> 不會阻塞 HTML 的處理,它們會與其他資源并行加載。
模塊腳本會等到 HTML 文檔完全準備就緒(即使它們很小并且比 HTML 加載速度更快),然后才會運行。
保持腳本的相對順序:在文檔中排在前面的腳本先執行。
它的一個副作用是,模塊腳本總是會“看到”已完全加載的 HTML 頁面,包括在它們下方的 HTML 元素。
例如:
<script type="module"> alert(typeof button); // object:腳本可以“看見”下面的 button // 因為模塊是被延遲的(deferred,所以模塊腳本會在整個頁面加載完成后才運行 </script> 相較于下面這個常規腳本: <script> alert(typeof button); // button 為 undefined,腳本看不到下面的元素 // 常規腳本會立即運行,常規腳本的運行是在在處理頁面的其余部分之前進行的 </script> <button id="button">Button</button>
請注意:上面的第二個腳本實際上要先于前一個腳本運行!所以我們會先看到 undefined,然后才是 object。
這是因為模塊腳本是被延遲的,所以要等到 HTML 文檔被處理完成才會執行它。而常規腳本則會立即運行,所以我們會先看到常規腳本的輸出。
當使用模塊腳本時,我們應該知道 HTML 頁面在加載時就會顯示出來,在 HTML 頁面加載完成后才會執行 JavaScript 模塊,因此用戶可能會在 JavaScript 應用程序準備好之前看到該頁面。某些功能那時可能還無法正使用。我們應該放置“加載指示器(loading indicator)”,否則,請確保不會使用戶感到困惑。
十、Async 適用于內聯腳本(inline script)
對于非模塊腳本,async 特性(attribute)僅適用于外部腳本。異步腳本會在準備好后立即運行,獨立于其他腳本或 HTML 文檔。
對于模塊腳本,它也適用于內聯腳本。
例如,下面的內聯腳本具有 async 特性,因此它不會等待任何東西。
它執行導入(fetch ./analytics.js),并在準備導入完成時運行,即使 HTML 文檔還未完成,或者其他腳本仍在等待處理中。
這對于不依賴任何其他東西的功能來說是非常棒的,例如計數器,廣告,文檔級事件監聽器。
<!-- 所有依賴都獲取完成(analytics.js)然后腳本開始運行 --> <!-- 不會等待 HTML 文檔或者其他 <script> 標簽 --> <script async type="module"> import {counter} from './analytics.js'; counter.count(); </script>
十一、外部腳本
具有 type="module" 的外部腳本(external script)在兩個方面有所不同:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
具有相同 src 的外部腳本僅運行一次:<!-- 腳本 my.js 被加載完成(fetched)并只被運行一次 --> <script type="module" src="my.js"></script> <script type="module" src="my.js"></script>
從另一個源(例如另一個網站)獲取的外部腳本需要 CORS header,如我們在 Fetch:跨源請求 一章中所講的那樣。換句話說,如果一個模塊腳本是從另一個源獲取的,則遠程服務器必須提供表示允許獲取的 header Access-Control-Allow-Origin。<!-- another-site.com 必須提供 Access-Control-Allow-Origin --> <!-- 否則,腳本將無法執行 --> <script type="module" src="http://another-site.com/their.js"></script>默認這樣做可以確保更好的安全性。
十二、不允許裸模塊(“bare” module)
在瀏覽器中,import 必須給出相對或絕對的 URL 路徑。沒有任何路徑的模塊被稱為“裸(bare)”模塊。在 import 中不允許這種模塊。
例如,下面這個 import 是無效的:
import {sayHi} from 'sayHi'; // Error,“裸”模塊 // 模塊必須有一個路徑,例如 './sayHi.js' 或者其他任何路徑
某些環境,像 Node.js 或者打包工具(bundle tool)允許沒有任何路徑的裸模塊,因為它們有自己的查找模塊的方法和鉤子(hook)來對它們進行微調。但是瀏覽器尚不支持裸模塊。
十三、兼容性,“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>
十四、構建工具
在實際開發中,瀏覽器模塊很少被以“原始”形式進行使用。通常,我們會使用一些特殊工具,例如 Webpack,將它們打包在一起,然后部署到生產環境的服務器。
使用打包工具的一個好處是 —— 它們可以更好地控制模塊的解析方式,允許我們使用裸模塊和更多的功能,例如 CSS/HTML 模塊等。
構建工具做以下這些事兒:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
從一個打算放在 HTML 中的 <script type="module"> “主”模塊開始。
分析它的依賴:它的導入,以及它的導入的導入等。
使用所有模塊構建一個文件(或者多個文件,這是可調的),并用打包函數(bundler function)替代原生的 import 調用,以使其正常工作。還支持像 HTML/CSS 模塊等“特殊”的模塊類型。
在處理過程中,可能會應用其他轉換和優化:刪除無法訪問的代碼。刪除未使用的導出(“tree-shaking”)。刪除特定于開發的像 console 和 debugger 這樣的語句。可以使用 Babel 將前沿的現代的 JavaScript 語法轉換為具有類似功能的舊的 JavaScript 語法。壓縮生成的文件(刪除空格,用短的名字替換變量等)。
如果我們使用打包工具,那么腳本會被打包進一個單一文件(或者幾個文件),在這些腳本中的 import/export 語句會被替換成特殊的打包函數(bundler function)。因此,最終打包好的腳本中不包含任何 import/export,它也不需要 type="module",我們可以將其放入常規的 <script>:
<!-- 假設我們從諸如 Webpack 這類的打包工具中獲得了 "bundle.js" 腳本 --> <script src="bundle.js"></script>
也就是說,原生模塊也是可以使用的。所以,我們在這兒將不會使用 Webpack:你可以稍后再配置它。
十五、總結
下面總結一下模塊的核心概念:
一個模塊就是一個文件。瀏覽器需要使用 <script type="module"> 以使 import/export 可以工作。模塊(譯注:相較于常規腳本)有幾點差別:默認是延遲解析的(deferred)。Async 可用于內聯腳本。要從另一個源(域/協議/端口)加載外部腳本,需要 CORS header。重復的外部腳本會被忽略
模塊具有自己的本地頂級作用域,并可以通過 import/export 交換功能。
模塊始終使用 use strict。
模塊代碼只執行一次。導出僅創建一次,然后會在導入之間共享。
當我們使用模塊時,每個模塊都會實現特定功能并將其導出。然后我們使用 import 將其直接導入到需要的地方即可。瀏覽器會自動加載并解析腳本。
在生產環境中,出于性能和其他原因,開發者經常使用諸如 Webpack 之類的打包工具將模塊打包到一起。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。