一啸蜜、前言
JS中直接import其他模塊是個很棒的能力坑雅,ES6規(guī)范中就提供了這樣的特性。然后衬横,長久以來裹粤,都只有在Node.js中才能無阻使用,瀏覽器都沒有原生支持蜂林。
Node.js對于我而言遥诉,就像是個在另外一個城市結交的好朋友拇泣,簡單了解,能和睦相處即可矮锈,因此霉翔,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瀏覽器支持盗冷,不過相信很快其他瀏覽器也會跟進怠苔。
本文會對這兩種模塊導入都做介紹,因此仪糖,本文內容篇幅較長柑司,且有一定深度,需要預留較多時間閱讀锅劝。
二攒驰、靜態(tài)import
我們先從最簡單的案例說起,例如故爵,我想想玻粪,demo比較方便演示的效果,啊,那就實現(xià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"呢铆,這個時候,瀏覽器會把這段內聯(lián)script或者外鏈script認為是ECMAScript模塊蹲缠。
模塊JS文件棺克,業(yè)界或者官方約定俗成命名為.mjs文件格式,一來可以和普通JavaScript文件(.js后綴)進行區(qū)分线定,一看就知道是模塊文件娜谊;二來Node.js中ES6的模塊化特性只支持.mjs后綴的腳本,可以和Node.js保持一致斤讥。當然纱皆,我們直接使用.js作為模塊JS文件的后綴也是可以的。
在瀏覽器側進行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命令呻拌。再看下面一個例子葱轩,
文字變紅,以及垂直翻轉,演示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)
元素文字變紅同時垂直翻轉的效果,如下截圖:
您可以狠狠地點擊這里:靜態(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. 內聯(lián)script同樣defer特性
如下代碼:
? console.log("Inline module執(zhí)行");
? console.log("Inline script執(zhí)行");
最后的執(zhí)行順序是:1.js,Inline script葛躏,Inline module澈段,2.js。
從在線demo控制臺輸出可以證明上面的結論舰攒。
原因在于败富,傳統(tǒng)的內聯(lián)是沒有defer這種概念的,從不異步摩窃,大家可以直接忽略兽叮,認為什么也沒設置即可;而type="module"的天然defer猾愿。因此鹦聪,先1.js,Inline script蒂秘;然后按照defer規(guī)則泽本,從前往后依次是Inline module,2.js姻僧。
4. 支持async
無論是內聯(lián)的module?還是外鏈的规丽,都支持async這個異步標識屬性蒲牧。這個有別于傳統(tǒng)的,也就是傳統(tǒng)僅外鏈JS才支持async嘁捷,內聯(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控制臺輸出的結果蛀缝,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蝗碎,結果瀏覽器報CORS policy跨域相關錯誤,自然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笑窜,打開控制臺,可以看到輸出如下內容:
7. 無憑證
如果請求來自同一個源(域名一樣)登疗,大多數(shù)基于CORS的API將發(fā)送憑證(如cookie等)排截,但fetch()和模塊腳本是例外 – 除非您要求,否則它們不會發(fā)送憑證辐益。
我們通過下面例子理解上面這句話的含義:
crossOrigin可以有下面兩個值:
關鍵字釋義
anonymous元素的跨域資源請求不需要憑證標志設置断傲。
use-credentials元素的跨域資源請求需要憑證標志設置,意味著該請求需要提供憑證智政。
其中认罩,只要crossOrigin的屬性值不是use-credentials,全部都會解析為anonymous续捂。
回到本節(jié)案例垦垂。
傳統(tǒng)JS加載,都是默認帶憑證的(對應注釋①)牙瓢。
module模塊加載默認不帶憑證(注釋②)乔外。
如果我們設置crossOrigin為匿名anonymous,又會帶憑證(注釋③)一罩。
如果import模塊跨域杨幼,則設置crossOrigin為anonymous不帶憑證(注釋④)。
如果import模塊跨域聂渊,且明確設置crossOrigin為使用憑證use-credentials差购,則帶憑證(注釋⑤)。
注意汉嗽,如果跨域欲逃,需要同時服務器側返回Access-Control-Allow-Credentials:true頭信息。
然后饼暑,上面的憑證規(guī)則以后有可能會調整稳析,歡迎大家及時反饋。
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()那個紅色翻轉案例我們改造成動態(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這種回調形式房资。
(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
需求如下轰异,點擊不同的美女選項卡的時候岖沛,去加載對應的模塊,模塊有個方法可以改變元素內容搭独。
則婴削,我們的的交互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`的方法,內容是寫入一段HTMLmodule.loadPageInto(main);? ? });? }
結果牙肝,當我們點擊其他選項卡的時候唉俗,元素中的美女圖片就會發(fā)生變化,例如默認是這個:
點擊“美女2”選項卡按鈕配椭,此時瀏覽器會動態(tài)加載mm2.mjs這個模塊虫溜,然后執(zhí)行這個模塊中暴露的loadPageInfo方法,從而改變呈現(xiàn)內容股缸。