瀏覽器中的 ES6 module 實現
原文鏈接: jakearchibald.com
ES6 的模塊特性(module) 開始在瀏覽器端實現啦!一切正在路上...
瀏覽器 | 備註 |
---|---|
Safari 10.1 | (無) |
Chrome Canary 60 | 打開 chrome:flags 啓用“實驗性網絡平臺功能” |
Firefox 54 | 打開 about:config 啓用 dom.moduleScripts.enabled |
Edge 15 | 打開 about:flags 啓用“實驗性 JavaScript 功能” |
<script type="module">
import {addTextToBody} from './utils.js';
addTextToBody('Modules are pretty cool.');
</script>
// utils.js
export function addTextToBody(text) {
const div = document.createElement('div');
div.textContent = text;
document.body.appendChild(div);
}
只需爲 script
元素添加 type=module
屬性,瀏覽器就會把該元素對應的內聯腳本或外部腳本當成 ECMAScript 模塊進行處理。
目前已經有一些 很棒的關於 ECMAScript 模塊的文章了,不過我還是想分享一些和瀏覽器相關的東西,它們都是我在測試代碼、閱讀規範的過程中學習到的。
尚未得到支持的 import 路徑符號
// 支持
import {foo} from 'https://jakearchibald.com/utils/bar.js';
import {foo} from '/utils/bar.js';
import {foo} from './bar.js';
import {foo} from '../bar.js';
// 不支持
import {foo} from 'bar.js';
import {foo} from 'utils/bar.js';
有效的路徑符號應當符合以下條件規則之一:
- 完整的非相對路徑。這樣在將其傳給
new URL(moduleSpecifier)
的時候纔不會報錯。 - 以
/
開頭。 - 以
./
開頭。 - 以
../
開頭。
其他形式的符號被保留下來,未來將用於其他功能(如引入[import]內置模塊)。
使用 nomodule
屬性向後兼容
<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>
支持 type=module
的瀏覽器將會忽略帶有 nomodule
屬性的 script
標籤。這意味着我們可以爲支持模塊的瀏覽器提供模塊形式的代碼,同時爲那些不支持模塊的瀏覽器提供降級處理。
瀏覽器 issue
- Firefox 暫不支持
nomodule
(issue)。 - Edge 暫不支持
nomodule
(issue)。 - Safari 10.1 暫不支持
nomodule
,但在最新的技術預覽版中已經解決了此問題。對於 10.1 來說,有一個相當棒的解決方案。
默認延遲執行
<!-- 這個腳本會在… -->
<script type="module" src="1.js"></script>
<!-- …這個腳本之後、… -->
<script src="2.js"></script>
<!-- …這個腳本之前執行。 -->
<script defer src="3.js"></script>
Live demo。腳本執行順序爲 2.js
, 1.js
, 3.js
。
獲取腳本會導致 HTML parser 阻塞,這簡直太太太太噁心了。對正常的腳本,我們可以使用 defer
屬性來防止阻塞,腳本將延遲至文檔解析完畢後執行,同時保持與其他使用 defer
的腳本之間的執行順序。模塊腳本的默認行爲與 defer
相同 —— 無法使模塊腳本阻塞 HTML parser。
模塊腳本與使用 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>
Live demo。執行順序依次爲 1.js
、內聯腳本、內聯模塊、2.js
。
正常的內聯腳本會忽略 defer
屬性,而內聯模塊則總是延遲執行,無論是否引入其他內容。
async
對內聯、外部模塊同樣適用
<!-- 腳本將會在其引入的模塊加載完成後立即執行 -->
`<script async type="module">`
import {addTextToBody} from './utils.js';
addTextToBody('Inline module executed.');
</script>
<!-- 腳本及其引入的模塊加載完成後立即執行 -->
`<script async type="module" src="1.js">`</script>
Live demo。先完成加載的腳本先執行。
與正常腳本相同,帶有 async
屬性的腳本在下載時不會阻塞 HTML parser,一旦加載完畢,立即執行。不同的是,async
對內聯模塊也同樣適用。
使用 async
時,腳本的執行順序可能會和它們在 DOM 中出現的順序不盡相同。
瀏覽器 issue
- Firefox 不支持內聯模塊使用
async
(issue)。
模塊只執行一次
<!-- 1.js 只執行一次 -->
<script type="module" src="1.js"></script>
<script type="module" src="1.js"></script>
<script type="module">
import "./1.js";
</script>
<!-- 普通腳本會執行多次 -->
<script src="2.js"></script>
<script src="2.js"></script>
引入同一個模塊多次的時候,模塊只會執行一次。這對 HTML 中的模塊腳本同樣適用 —— 在同一個頁面中,URL 相同的模塊只會執行一次。
瀏覽器 issue
- Edge 會執行多次 (issue)。
總是使用 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 獲取的。這意味着,跨域模塊腳本必須返回類似 Access-Control-Allow-Origin: *
這樣的有效的響應頭。
瀏覽器 issue
不攜帶憑證信息
<!-- 請求腳本時會攜帶相關憑證 (如 cookie) -->
<script src="1.js"></script>
<!-- 不會攜帶相關憑證 -->
<script type="module" src="1.js"></script>
<!-- 會攜帶相關憑證 -->
<script type="module" crossorigin src="1.js?"></script>
<!-- 不會攜帶相關憑證 -->
<script type="module" crossorigin src="https://other-origin/1.js"></script>
<!-- 會攜帶相關憑證-->
<script type="module" crossorigin="use-credentials" src="https://other-origin/1.js?"></script>
在請求同源的情況下,多數基於 CORS 的 API 都會發送憑證信息(credentials,如 Cookie)。但 fetch()
和模塊腳本恰恰例外 —— 除非手動聲明,否則是不會發送相關憑證的。
對於一個同源的模塊腳本,可以爲其添加 crossorigin
屬性(這看起來挺怪的,我已經在規範中提出這個問題了),這樣在請求時就可以攜帶相關憑證了。如果你還想將憑證發給其他域,請使用 crossorigin="use-credentials"
。需要注意的是,接收憑證的域必須返回 Access-Control-Allow-Credentials: true
的響應頭。
此外,還有一個與“模塊只會執行一次”這條規則相關的問題。瀏覽器是通過 URL 來區別不同模塊的,所以如果你先請求了一個模塊而不攜帶任何憑證,緊接着又攜帶憑證信息去請求該模塊,那麼第二次得到的依然是不攜帶憑證的請求所返回的模塊。這正是我在上面代碼中的 URL 後面加上“?”的原因。
瀏覽器 issue
- 請求同源模塊時,Chrome 會攜帶憑證信息(issue)。
- 即使添加了
crossorigin
屬性,Safari 在請求同源腳本時也不會攜帶憑證信息(issue)。 - Edge 則完全弄反了。請求同源模塊時,Edge 默認會發送憑證信息,但如果手動添加了
crossorigin
屬性,則又不會攜帶 (issue)。
Firefox 是唯一正確實現這一點的瀏覽器 —— 好樣的!
Mime-types
不同於普通腳本,對於通過 module 引入的腳本,服務器必須返回合法的 MIME type,否則腳本將不會執行。