萬歲,瀏覽器原生支持ES6 export和import模塊啦!

一、前言

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)容。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容