自從ES2015定稿以來,我們通過 Babel 等轉換工具可以在專案中直接使用【模組】。前端模組化開發已經是不可逆轉,在 ECMAScript module 之前我們通過 requirejs、seajs、LABjs,甚至最早的時候我們通過閉包來實現模組化開發。目前一些主流的的瀏覽器廠商已經在他們新版的瀏覽器中原生支援了【模組】,今天我們就來原生瀏覽器中的模組到底如何。
目前原生支援模組用法的瀏覽器有:
- Safari 10.1
- Chrome 61
- Firefox 60
- Edge 16
要使用原生瀏覽器的模組,你只需要在 script
標籤上新增一個 type=module
屬性, 瀏覽器就會把這個指令碼(內聯指令碼或者外聯指令碼)當作模組來處理。
1 2 3 4 |
<script type="module"> import {addTextToBody} from './utils.mjs'; addTextToBody('Modules are pretty cool.'); </script> |
1 2 3 4 5 6 |
// utils.mjs export function addTextToBody(text) { const div = document.createElement('div'); div.textContent = text; document.body.appendChild(div); } |
不支援裸匯入(不能通過模組名直接匯入)
一個合格的模組識別符號必須滿足下列條件之一:
- 一個完整的非相對URL。通過
new URL(moduleSpecifier)
使用時不會報錯。 - 以
/
開頭。 - 以
./
開頭。 - 以
../
開頭。
保留其他說明符供將來使用,如匯入內建模組。
1 2 3 4 5 6 7 8 |
// 支援: 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'; |
通過 nomodule
向後相容
如果當前瀏覽器支援 type=module
標籤的話會自動忽略 nomodule
標籤。這意味著你可以將模組暴露給支援模組的瀏覽器,同時可以給不支援模組的瀏覽器提供相容方案。
1 2 |
<script type="module" src="module.mjs"></script> <script nomodule src="fallback.js"></script> |
預設延遲載入
當網路狀況不好的時候,指令碼載入會阻塞瀏覽器解析 HTML。通常我們可以通過在 script 標籤上使用 defer
屬性來解決阻塞問題,但是這也會造成指令碼只有在文件解析完成後才執行,同時還要兼顧其他延遲指令碼的執行順序。預設情況下模組指令碼的表現類似於 defer
— 它不會阻塞 HTML 的解析。
模組指令碼的執行佇列與使用了 defer
的常規指令碼一致。
1 2 3 4 5 6 |
<!-- 這個指令碼執行滯後於… --> <script type="module" src="1.mjs"></script> <!-- …這個指令碼… --> <script src="2.js"></script> <!-- …但是先於這個指令碼 --> <script defer src="3.js"></script> |
內聯模組也是延遲載入的
唱過指令碼會忽略 defer
然而內聯模組總是 defer
的,不管它是否引入了動西。
1 2 3 4 5 6 7 8 9 10 11 12 |
<!-- 這個指令碼執行滯後於… --> <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> |
內聯/外聯 模組都支援非同步載入
在普通指令碼中,async
能讓指令碼的下載不阻塞HTML的解析並在下載完成後儘快執行。和普通指令碼不同,內聯模組指令碼支援非同步載入的。
同樣的,非同步載入的模組可能不會按照它們在DOM中出現的順序執行。
1 2 3 4 5 6 7 |
<!-- 這個會在它引入的指令碼載入完成後立即執行 --> <script async type="module"> import {addTextToBody} from './utils.mjs'; addTextToBody('Inline module executed.'); </script> <!-- 這個會在其本身以及其引入的指令碼載入完成後立即執行 --> <script async type="module" src="1.mjs"></script> |
模組只執行一次
如果你使用過ES6的模組, 那麼你肯定知道你可以多次引入同一模組但是他們只會執行一次。在Html中也一樣, 一個URL模組指令碼在一個頁面中只會執行一次。
1 2 3 4 5 6 7 8 9 |
<!-- 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> |
遵循 CORS
不同於普通指令碼,跨站引用模組指令碼(及其引入)需要遵循CORS。 這意味著跨源模組指令碼必須返回有效的CORS頭,例如Access-Control-Allow-Origin:*。
1 2 3 4 5 6 7 8 9 |
<!-- 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的API需要請求帶上憑證(如:cookie),但是 fetch()
和模組指令碼是個例外,他們預設不會帶上相關憑證除非你明確指定。
如果你想在同源請求模組指令碼時帶上憑證,可以設定 crossorigin
屬性。如果跨站請求也想帶上的話,可以設定 crossorigin="use-credentials"
,需要注意的是跨站的站點需要在請求返回頭中加上 Access-Control-Allow-Credentials: true
。
1 2 3 4 5 6 7 8 9 10 |
<!-- 有憑證 (cookies) --> <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> |
這裡還有一個關於 模組只執行一次 的坑。當你通過一個URL引入一個模組時,如果一開始你以無憑證的方式請求,然後又以有憑證的方式再請求一次,你得到的返回都是無憑證請求的那次。這就是我為什麼會在第二次請求時在URL後加上?
,用於區分兩次請求。
更新:以上可能很快會改變。 預設情況下,fetch()和模組指令碼都會向相同來源的URL傳送憑據。
Mime-types
與普通指令碼不同,模組指令碼必須提供有效的JavaScript MIME型別,否則它們將不會執行。 HTML標準建議使用 text/javascript
。