一、前言

JS中直接import其他模塊是個很棒的能力,ES6規(guī)范中就提供了這樣的特性。然后,長久以來,都只有在Node.js中才能無阻使用,瀏覽器都沒有原生支持。
Node.js對于我而言,就像是個在另外一個城市結(jié)交的好朋友,簡單了解,能和睦相處即可,因此,Node.js支持import功能,就好像朋友升職賺了大錢一樣,替他開心,不過也就只是替他開心,自己其實還是淡然的。但是,web瀏覽器就不一樣了,這個可是我打算廝守一生的伴侶,因此,web瀏覽器原生支持import功能,那就好像自己的老婆升職賺了大錢一樣,那比自己賺了大錢還開心,心中一百個“萬歲”。
ES6在瀏覽器中的import功能分為靜態(tài)import和動態(tài)import。
其中靜態(tài)import出現(xiàn)更早,瀏覽器兼容性更好,支持瀏覽器包括:Safari 10.1+,Chrome 61+,F(xiàn)irefox 60+,Edge 16+。

動態(tài)import支持晚一些,兼容性要差一些,目前Chrome瀏覽器和Safari瀏覽器支持,不過相信很快其他瀏覽器也會跟進。

本文會對這兩種模塊導入都做介紹,因此,本文內(nèi)容篇幅較長,且有一定深度,需要預留較多時間閱讀。
二、靜態(tài)import
我們先從最簡單的案例說起,例如,我想想,demo比較方便演示的效果,啊,那就實現(xiàn)改變
元素的文字顏色。
主頁面相關(guān)script代碼如下:
// 導入firstBlood模塊
import { pColor } from './firstBlood.mjs';// 設置顏色為紅色pColor('red');
然后firstBlood.mjs文件中代碼為:
// export一個改變
元素顏色的方法export function pColor (color) {? const p = document.querySelector('p');? p.style.color = color;}
您可以狠狠地點擊這里:瀏覽器原生import實現(xiàn)文字變紅demo
可以看到
文字變紅了:

有了案例,下面基礎知識就更好消化與理解了。
對于需要引入模塊的元素,我們需要添加type="module",這個時候,瀏覽器會把這段內(nèi)聯(lián)script或者外鏈script認為是ECMAScript模塊。
模塊JS文件,業(yè)界或者官方約定俗成命名為.mjs文件格式,一來可以和普通JavaScript文件(.js后綴)進行區(qū)分,一看就知道是模塊文件;二來Node.js中ES6的模塊化特性只支持.mjs后綴的腳本,可以和Node.js保持一致。當然,我們直接使用.js作為模塊JS文件的后綴也是可以的。
在瀏覽器側(cè)進行import模塊引入,其對模塊JS文件的mime type要求非常嚴格,務必和JS文件一致。這就導致,如果我們使用.mjs文件格式,則需要在服務器配置mime type類型,否則會報錯:
Failed to load module script: The server responded with a non-JavaScript MIME type of “”. Strict MIME type checking is enforced for module scripts per HTML spec.

Nginx對于不識別后綴默認會給一個application/octet-stream的MIME type,方便下載等處理,但是,不好意思,在模塊化引入這里,這個MIME type無效,需要足夠精準才行,為application/javascript,然后根據(jù)自己測試,IIS服務器中application/x-javascript也是可以的。
無論是Apache服務器還是Nginx,都可以修改mime.types文件使.mjs的MIME type和.js文件一樣。
除了export普通的function,我們還可以export?const或者其他任何變量或者聲明。也支持default命令。再看下面一個例子,
文字變紅,以及垂直翻轉(zhuǎn),演示const和default使用。
假設模塊腳本文件名是doubleKill.mjs,其代碼如下:
// doubleKill.mjs
// const 和 default功能演示export default () => {? const p = document.querySelector('p');? p.style.transform = 'scaleY(-1)';};export const pColor = (color) => {? const p = document.querySelector('p');? p.style.color = color;}
import部分邏輯代碼為:
// 導入doubleKill模塊import * as module from './doubleKill.mjs';// 執(zhí)行默認方法module.default();// 設置顏色為紅色module.pColor('red');
就可以實現(xiàn)
元素文字變紅同時垂直翻轉(zhuǎn)的效果,如下截圖:

您可以狠狠地點擊這里:靜態(tài)import模塊const和default使用demo
三、nomodule與向下兼容
模塊腳本我們可以使用type="module"進行設定,對于并不支持export和import的瀏覽器,我們可以使用nomodule進行向下兼容。
對于支持ES6模塊導入的瀏覽器,自然也支持原生的nomodule屬性,此時fallback.js是忽略的;但是,對于不支持的老瀏覽器,無視nomodule,此時fallback.js就會執(zhí)行,于是瀏覽器全兼顧。
理論就如上面分析得這么完美,然后實際上,還是存在問題的。
主要問題在低端瀏覽器.mjs資源會冗余加載,例如這個測試demo在IE11下的網(wǎng)絡請求:

不過這并不是什么大問題,多一點請求和流量,功能這塊可以不影響的。
四、靜態(tài)import更多細節(jié)
1. 目前import不支持裸露的說明符
目前import不支持裸露的說明符,用白話講就是import的地址前面不能是光禿禿的。例如下面這些就不支持:
// 目前不支持,以后可能支持import {foo} from 'bar.mjs';import {foo} from 'utils/bar.mjs';
下面這些則支持,可以是根路徑的/,同級路徑./亦或者是父級../,甚至完整的非相對地址也是可以的。
// 支持import {foo} from 'https://www.zhangxinxu.com/utils/bar.mjs';import {foo} from '/utils/bar.mjs';import {foo} from './bar.mjs';import {foo} from '../bar.mjs';
2. 默認Defer行為
傳統(tǒng)屬性支持一個名為defer的屬性值,可以讓JS資源異步加載,同時保持順序。例如:
加載順序一定是1.js,?2.js,?3.js。我們只要看2.js和3.js,由于設置了defer,這兩個JS異步加載,因此,就算1.js放在最下面,也多半1.js先加載完。而多個同時設置defer會從前往后依次加載執(zhí)行。因此,一定是先加載完2.js然后是3.js。
回到本文的ES6 module導入,對于type="module"的元素,天然外掛defer特性,也就是天然異步,所有module腳本按順序,因此,下面這段腳本執(zhí)行順序就好理解了:
最終的加載執(zhí)行順序是:2.js,?1.mjs,?3.js。2.js同步,解析這里就加載。1.mjs雖然沒有設置defer,但默認defer,因此和3.js其實是一樣的,都是異步defer加載。由于1.mjs對于的在3.js前面,因此,先1.mjs后3.js。
相信不難理解。
3. 內(nèi)聯(lián)script同樣defer特性
如下代碼:
? console.log("Inline module執(zhí)行");
? console.log("Inline script執(zhí)行");
最后的執(zhí)行順序是:1.js,Inline script,Inline module,2.js。
從在線demo控制臺輸出可以證明上面的結(jié)論。

原因在于,傳統(tǒng)的內(nèi)聯(lián)是沒有defer這種概念的,從不異步,大家可以直接忽略,認為什么也沒設置即可;而type="module"的天然defer。因此,先1.js,Inline script;然后按照defer規(guī)則,從前往后依次是Inline module,2.js。
4. 支持async
無論是內(nèi)聯(lián)的module?還是外鏈的,都支持async這個異步標識屬性。這個有別于傳統(tǒng)的,也就是傳統(tǒng)僅外鏈JS才支持async,內(nèi)聯(lián)JS直接忽略async。
async和defer都可以讓JavaScript異步加載,區(qū)別在于defer保證執(zhí)行順序,而async誰先加載好誰先執(zhí)行。這個特性表現(xiàn)在type="module"的元素這里同樣適用。
例如下面例子:
? import { pColor } from './firstBlood.mjs';? pColor('red');
無論是firstBlood.mjs還是doubleKill.mjs都是異步加載,然后執(zhí)行順序不固定,有可能先firstBlood.mjs,也有可能先doubleKill.mjs,這樣看哪個模塊腳本先加載完畢。
5. 模塊只會執(zhí)行一次
傳統(tǒng)的如果引入的JS文件地址是一樣的,則JS會執(zhí)行多次。但是,對于type="module"的元素,即使模塊地址一模一樣,也只會執(zhí)行一次。例如:
? import "./1.mjs";
我們看下在線demo控制臺輸出的結(jié)果,2.js執(zhí)行了2次,而1.mjs模塊雖然3次引入,但只執(zhí)行了一次。截圖如下:

6. 總是CORS跨域
傳統(tǒng)JS文件的加載,我們直接跨域也可以解析,例如,我們會使用一些大網(wǎng)站的CDN服務,例如,加載個百度提供的jQuery地址:
可以正常解析。但是,如果是module模式下import腳本資源,則不會執(zhí)行,例如:
window.addEventListener('DOMContentLoaded', function () {
? ? console.log(window.$);
});
我們使用Chrome瀏覽器跑一下在線demo,結(jié)果瀏覽器報CORS policy跨域相關(guān)錯誤,自然window.$是undefined:

如何使支持跨域呢?
需要模塊資源服務端配置Access-Control-Allow-Origin,可以指定具體域名,或者直接使用*通配符,Access-Control-Allow-Origin:*。
本站cdn.zhangxinxu.com域名有配置Access-Control-Allow-Origin,所以,下面代碼打印出來的值就不是undefined。
window.addEventListener('DOMContentLoaded', function () {
? ? console.log(window.$);
});
訪問在線demo,打開控制臺,可以看到輸出如下內(nèi)容:

7. 無憑證
如果請求來自同一個源(域名一樣),大多數(shù)基于CORS的API將發(fā)送憑證(如cookie等),但fetch()和模塊腳本是例外 – 除非您要求,否則它們不會發(fā)送憑證。
我們通過下面例子理解上面這句話的含義:
crossOrigin可以有下面兩個值:
關(guān)鍵字釋義
anonymous元素的跨域資源請求不需要憑證標志設置。
use-credentials元素的跨域資源請求需要憑證標志設置,意味著該請求需要提供憑證。
其中,只要crossOrigin的屬性值不是use-credentials,全部都會解析為anonymous。
回到本節(jié)案例。
傳統(tǒng)JS加載,都是默認帶憑證的(對應注釋①)。
module模塊加載默認不帶憑證(注釋②)。
如果我們設置crossOrigin為匿名anonymous,又會帶憑證(注釋③)。
如果import模塊跨域,則設置crossOrigin為anonymous不帶憑證(注釋④)。
如果import模塊跨域,且明確設置crossOrigin為使用憑證use-credentials,則帶憑證(注釋⑤)。
注意,如果跨域,需要同時服務器側(cè)返回Access-Control-Allow-Credentials:true頭信息。
然后,上面的憑證規(guī)則以后有可能會調(diào)整,歡迎大家及時反饋。
8. 天然嚴格模式
import的JS模塊代碼天然嚴格模式,如果里面有不太友好的代碼會報錯,例如:

四、動態(tài)import
靜態(tài)import在首次加載時候會把全部模塊資源都下載下來,但是,我們實際開發(fā)時候,有時候需要動態(tài)import(dynamic import),例如點擊某個選項卡,才去加載某些新的模塊,這個動態(tài)import特性瀏覽器也是支持的。
具體是使用一個長得像函數(shù)的import(),注意,只是長得像函數(shù),import()實際上就是個單純的語法,類似于super()。這就意味著import()不會從Function.prototype獲得繼承,因此您無法call或apply它,并且const importAlias = import之類的東西不起作用,甚至import()都不是對象!
語法為:
import(moduleSpecifier);
moduleSpecifier為模塊說明符,其實就是模塊地址,規(guī)則和靜態(tài)import一樣,不能是裸露的地址。
案例
靜態(tài)import()那個紅色翻轉(zhuǎn)案例我們改造成動態(tài)import,也就是把import xxxx from 'xxxx'改成import('xxxx'),代碼如下:
// 導入doubleKill模塊import('./doubleKill.mjs').then((module) => {// 執(zhí)行默認方法module.default();// 設置顏色為紅色module.pColor('red');? });
最后效果和靜態(tài)import一樣:

您可以狠狠地點擊這里:ES6動態(tài)import模塊基本使用demo
由于import()返回一個promise,所以,我們可以使用async/await來代替then這種回調(diào)形式。
(async () => {// 導入doubleKill模塊const module = await import('./doubleKill.mjs');// 執(zhí)行默認方法module.default();// 設置顏色為紅色module.pColor('red');})();
您可以狠狠地點擊這里:async/await下的動態(tài)import演示demo
五、交互中的動態(tài)import
不像靜態(tài)import只能用在
首先,頁面HTML代碼如下:
? ? 美女1
? ? 美女2
? ? 美女3
需求如下,點擊不同的美女選項卡的時候,去加載對應的模塊,模塊有個方法可以改變元素內(nèi)容。
則,我們的的交互JS和動態(tài)import()JS如下:
? const main = document.querySelector('main');? const links = document.querySelectorAll('nav > a');? for (const link of links) {? ? link.addEventListener('click', async (event) => {? ? ? const module = await import(`./${link.dataset.module}.mjs`);// 模塊暴露名為`loadPageInto`的方法,內(nèi)容是寫入一段HTMLmodule.loadPageInto(main);? ? });? }
結(jié)果,當我們點擊其他選項卡的時候,元素中的美女圖片就會發(fā)生變化,例如默認是這個:

點擊“美女2”選項卡按鈕,此時瀏覽器會動態(tài)加載mm2.mjs這個模塊,然后執(zhí)行這個模塊中暴露的loadPageInfo方法,從而改變呈現(xiàn)內(nèi)容。
