[譯] 瀏覽器中的 ECMAScript 模組

司楠發表於2019-02-16

原文連結:ECMAScript modules in browsers

作者:Jake Archibald

瀏覽器現在可以使用 ES 模組(module)了!它們是:

  • Safari 10.1
  • Chrome 61
  • Firefox 60
  • Microsoft Edge 16
<script type="module">
  import {addTextToBody} from `./utils.mjs`;

  addTextToBody(`Modules are pretty cool.`);
</script>
// utils.mjs
export function addTextToBody(text) {
  const div = document.createElement(`div`);
  div.textContent = text;
  document.body.appendChild(div);
}

線上演示

您只需要在 script 元素上新增 type=module,瀏覽器就會將內聯指令碼或外部指令碼作為 ECMAScript module 處理。

關於模組(module)已經有一些很棒的文章,但是我想分享一些在我測試和閱讀規範的時候學到的瀏覽器特有的內容。

目前還不支援的某些 import 用法

// 已支援:
import {foo} from `https://jakearchibald.com/utils/bar.mjs`;
import {foo} from `/utils/bar.mjs`;
import {foo} from `./bar.mjs`;
import {foo} from `../bar.mjs`;

// 不支援:
import {foo} from `bar.mjs`;
import {foo} from `utils/bar.mjs`;

有效的模組路徑說明符必須符合下列條件之一:

  • 一個完整的非相對URL,這樣在將其傳給 new URL(moduleSpecifier) 的時候才不會報錯。
  • / 開頭的。
  • ./ 開頭的。
  • ../ 開頭的。

其他形式的說明符保留供將來使用,例如匯入內建模組。

使用 nomodule 來向後相容

<script type="module" src="module.mjs"></script>
<script nomodule src="fallback.js"></script>

線上演示

支援 type=module 的瀏覽器會忽略屬性為 nomodule 的指令碼。這意味著您可以給支援模組的瀏覽器提供模組樹,同時給其他瀏覽器提供一個降級版本。

瀏覽器問題

  • Firefox 瀏覽器不支援 nomodule (issue)。已在 Firefox nightly 中修復!
  • Edge 瀏覽器不支援 nomodule (issue)。已在 Edge 16 中修復!
  • Safari 瀏覽器不支援 nomodule。已在 Safari 11 中修復!對於 10.1,這裡有一個非常聰明的替代辦法

預設情況下延遲執行

<!-- 這個指令碼的執行會晚於… -->
<script type="module" src="1.mjs"></script>

<!-- …這個指令碼… -->
<script src="2.js"></script>

<!-- …但是會在這個指令碼之前執行。 -->
<script defer src="3.js"></script>

線上演示

執行的順序是:2.js1.mjs3.js

script 在獲取期間會阻塞 HTML 解析器,簡直太糟糕了。對於常規指令碼,您可以使用 defer 來避免阻塞,當然這也會推遲指令碼的執行,直到文件完成解析,並與其他延遲指令碼一起維護執行順序。模組指令碼的預設表現行為就像 defer ——當它正在獲取時,沒有辦法讓一個模組指令碼阻塞 HTML 解析器。

模組指令碼使用和新增了 defer 的常規指令碼相同的執行佇列。

內聯指令碼也是延時的

<!-- 這個指令碼的執行會晚於… -->
<script type="module">
  addTextToBody("Inline module executed");
</script>

<!-- …這個指令碼… -->
<script src="1.js"></script>

<!-- …和這個指令碼… -->
<script defer>
  addTextToBody("Inline script executed");
</script>

<!-- …但是會在這個指令碼之前執行。 -->
<script defer src="2.js"></script>

線上演示

執行順序是1.js ,內聯指令碼,內聯指令碼,2.js

常規的內聯指令碼會忽略 defer ,然而內聯模組指令碼卻總是被延遲,無論它們有沒有匯入任何東西。

Async 對內聯、外部模組同樣適用

<!-- 一旦獲取了匯入,就會執行此操作 -->
<script async type="module">
  import {addTextToBody} from `./utils.mjs`;

  addTextToBody(`Inline module executed.`);
</script>
<!-- 一旦獲取了指令碼和它的匯入,就會執行此操作 -->
<script async type="module" src="1.mjs"></script>

線上演示

快速下載的指令碼會在慢速下載的指令碼之前執行。

與常規指令碼一樣,async 會讓指令碼在下載過程中不會阻塞 HTML 解析器,並且儘快地執行。與常規指令碼不同,async 也適用於內聯模組。

與往常的 async 一樣,指令碼不會按照它們出現在 DOM 中的順序執行。

瀏覽器問題

  • Firefox 瀏覽器不支援內聯模組指令碼上的 async (issue)。已在 Firefox 59 中修復!

模組僅執行一次

<!-- 1.mjs 僅執行一次 -->
<script type="module" src="1.mjs"></script>
<script type="module" src="1.mjs"></script>
<script type="module">
  import "./1.mjs";
</script>

<!-- 然而,普通的指令碼卻執行多次 -->
<script src="2.js"></script>
<script src="2.js"></script>

線上演示

如果您理解 ES 模組,您就會知道您雖然可以引入它們很多次,但是它們卻僅僅會執行一次。當然,這同樣適用於HTML中的指令碼模組 – 特定URL的模組指令碼每頁只執行一次。

瀏覽器問題

  • Edge 執行多次模組 (issue)。已修復,但是還沒釋出(希望 Edge 17 會帶上這個修復內容)。

總是使用 CORS

<!-- 該指令碼不會執行, 因為它不能通過 CORS 檢查 -->
<script type="module" src="https://….now.sh/no-cors"></script>

<!-- 該指令碼不會執行, 因為它引入的指令碼之一不能通過 CORS 檢查 -->
<script type="module">
  import `https://….now.sh/no-cors`;

  addTextToBody("This will not execute.");
</script>

<!-- 該指令碼會執行,因為它通過了 CORS 檢查 -->
<script type="module" src="https://….now.sh/cors"></script>

線上演示

與常規指令碼不同,模組指令碼(及其引入的內容)是通過 CORS 獲取的。這就意味著跨域的模組指令碼必須返回有效的 CORS 響應頭 ,比如 Access-Control-Allow-Origin: *

瀏覽器問題

  • Firefox 載入 Demo 頁面失敗 (issue)
  • Edge 載入沒有 CORS header 的模組指令碼 (issue)。 已在 Edge 16 中修復!

不攜帶憑據

<!-- 攜帶憑據獲取(cookie 等) -->
<script src="1.js"></script>

<!-- 不攜帶憑據獲取 -->
<script type="module" src="1.mjs"></script>

<!-- 攜帶憑據獲取 -->
<script type="module" crossorigin src="1.mjs?"></script>

<!-- 不攜帶憑據獲取 -->
<script type="module" crossorigin src="https://other-origin/1.mjs"></script>

<!-- 攜帶憑據獲取 -->
<script type="module" crossorigin="use-credentials" src="https://other-origin/1.mjs?"></script>

線上演示

如果請求來自相同的源,大多數基於 CORS 的 API 會傳送憑據(cookie 等),但是 fetch() 和模組指令碼卻是例外的——非您要求它們,否則它們不會傳送憑據除。

您可以通過新增 crossorigin 屬性來向同源模組新增憑據(這對我來說似乎有點奇怪,我在規範中對此提出質疑)。如果您打算向其他的源也傳送憑據,使用 crossorigin="use-credentials"。注意其他源必須使用 Access-Control-Allow-Credentials:true 的響應頭來響應。

此外,還有一個與“模組只執行一次”規則相關的問題。模組由其URL標記,因此如果首次請求了一個模組而不攜帶憑據,然後再次攜帶憑據請求該模組,那麼第二次獲得的依然是不攜帶憑證的模組。 這就是為啥我在上面的URL中使用 問號 ? 的原因,使它們成為唯一的。

更新: 上面的情況可能很快就會發生改變。fetch() 和模組指令碼預設都會向同源的 URL 傳送憑據。Issue

瀏覽器問題

  • Chrome 使用憑據請求同源模組(issue。已在 Chrome 61 中修復!
  • Safari 即使新增了 crossorigin 屬性,也不使用憑據請求同源模組(issue)。
  • Edge 即使新增了 crossorigin 屬性,也不使用憑據請求同源模組(issue。已在 Edge 16 中修復!
  • Edge 預設請求同源模組的時候攜帶了憑據(issue)。

MIME 型別

不同於常規指令碼,模組指令碼必須是有效的 JavaScript MIME 型別中的一種型別,否則模組就不會執行。HTML 標準建議使用 text/javascript

瀏覽器問題

  • Edge 執行無效的 MIME 型別指令碼(issue

這就是我目前學到的內容啦。毋庸置疑,我對 ES 模組登陸瀏覽器感到非常興奮!

效能建議,動態匯入等等!

請查閱有關 Web Fundamentals 的文章,深入瞭解模組使用情況。

相關文章