Webpack學(xué)習(xí)之動(dòng)態(tài)import原理及源碼分析
前言
在開始之前,先給我的mini-react打個(gè)廣告。對(duì)react源碼感興趣的朋友,走過路過的朋友點(diǎn)個(gè)star
在平時(shí)的開發(fā)中,我們經(jīng)常使用 import()實(shí)現(xiàn)代碼分割和懶加載。在低版本的瀏覽器中并不支持動(dòng)態(tài) import(),那 webpack 是如何實(shí)現(xiàn) import() polyfill 的?
原理分析
我們先來看看下面的 demo
function component() { const btn = document.createElement("button"); btn.onclick = () => { import("./a.js").then((res) => { console.log("動(dòng)態(tài)加載a.js..", res); }); }; btn.innerHTML = "Button"; return btn; } document.body.appendChild(component());
點(diǎn)擊按鈕,動(dòng)態(tài)加載 a.js
腳本,查看瀏覽器網(wǎng)絡(luò)請(qǐng)求可以發(fā)現(xiàn),a.js
請(qǐng)求返回的內(nèi)容如下:
簡(jiǎn)單看,實(shí)際上返回的就是下面這個(gè)東西:
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([ ["src_a_js"], { "./src/a.js": () => {}, }, ]);
從上面可以看出 3 點(diǎn)信息:
- 1.webpackChunkwebpack_demo 是掛到全局 window 對(duì)象上的屬性
- 2.webpackChunkwebpack_demo 是個(gè)數(shù)組
- 3.webpackChunkwebpack_demo 有個(gè) push 方法,用于添加動(dòng)態(tài)的模塊。當(dāng)
a.js
腳本請(qǐng)求成功后,這個(gè)方法會(huì)自動(dòng)執(zhí)行。
再來看看 main.js 返回的內(nèi)容
仔細(xì)觀察,動(dòng)態(tài) import 經(jīng)過 webpack 編譯后,變成了下面的一坨東西:
__webpack_require__.e("src_a_js") .then(__webpack_require__.bind(__webpack_require__, "./src/a.js")) .then((res) => { console.log("動(dòng)態(tài)加載a.js..", res); });
上面代碼中,__webpack_require__
用于執(zhí)行模塊,比如上面我們通過webpackChunkwebpack_demo.push
添加的模塊,里面的./src/a.js
函數(shù)就是在__webpack_require__
里面執(zhí)行的。
__webpack_require__.e
函數(shù)就是用來動(dòng)態(tài)加載遠(yuǎn)程腳本。因此,從上面的代碼中我們可以看出:
- 首先 webpack 將動(dòng)態(tài) import 編譯成
__webpack_require__.e
函數(shù) __webpack_require__.e
函數(shù)加載遠(yuǎn)程的腳本,加載完成后調(diào)用__webpack_require__
函數(shù)__webpack_require__
函數(shù)負(fù)責(zé)調(diào)用遠(yuǎn)程腳本返回來的模塊,獲取腳本里面導(dǎo)出的對(duì)象并返回
源碼分析及實(shí)現(xiàn)
如何動(dòng)態(tài)加載遠(yuǎn)程模塊
在開始之前,我們先來看下如何使用 script 標(biāo)簽加載遠(yuǎn)程模塊
var inProgress = {}; // url: "http://localhost:8080/src_a_js.main.js" // done: 加載完成的回調(diào) const loadScript = (url, done) => { if (inProgress[url]) { inProgress[url].push(done); return; } const script = document.createElement("script"); script.charset = "utf-8"; script.src = url; inProgress[url] = [done]; var onScriptComplete = (prev, event) => { var doneFns = inProgress[url]; delete inProgress[url]; script.parentNode && script.parentNode.removeChild(script); doneFns && doneFns.forEach((fn) => fn(event)); if (prev) return prev(event); }; script.onload = onScriptComplete.bind(null, script.onload); document.head.appendChild(script); };
loadScript(url, done)
函數(shù)比較簡(jiǎn)單,就是通過創(chuàng)建 script 標(biāo)簽加載遠(yuǎn)程腳本,加載完成后執(zhí)行 done 回調(diào)。inProgress
用于避免多次創(chuàng)建 script 標(biāo)簽。比如我們多次調(diào)用loadScript('http://localhost:8080/src_a_js.main.js', done)
時(shí),應(yīng)該只創(chuàng)建一次 script 標(biāo)簽,不需要每次都創(chuàng)建。這也是為什么我們調(diào)用多次 import('a.js')
,瀏覽器 network 請(qǐng)求只看到家在一次腳本的原因
實(shí)際上,這就是 webpack 用于加載遠(yuǎn)程模塊的極簡(jiǎn)版本。
__webpack_require__.e 函數(shù)的實(shí)現(xiàn)
首先我們使用installedChunks
對(duì)象保存動(dòng)態(tài)加載的模塊。key 是 chunkId
// 存儲(chǔ)已經(jīng)加載和正在加載的chunks,此對(duì)象存儲(chǔ)的是動(dòng)態(tài)import的chunk,對(duì)象的key是chunkId,值為 // 以下幾種: // undefined: chunk not loaded // null: chunk preloaded/prefetched // [resolve, reject, Promise]: chunk loading // 0: chunk loaded var installedChunks = { main: 0, };
由于 import()
返回的是一個(gè) promise,然后import()
經(jīng)過 webpack 編譯后就是一個(gè)__webpack_require__.e
函數(shù),因此可以得出__webpack_require__.e
返回的也是一個(gè) promise,如下所示:
const scriptUrl = document.currentScript.src .replace(/#.*$/, "") .replace(/\?.*$/, "") .replace(/\/[^\/]+$/, "/"); __webpack_require__.e = (chunkId) => { return Promise.resolve(ensureChunk(chunkId, promises)); }; const ensureChunk = (chunkId) => { var installedChunkData = installedChunks[chunkId]; if (installedChunkData === 0) return; let promise; // 1.如果多次調(diào)用了__webpack_require__.e函數(shù),即多次調(diào)用import('a.js')加載相同的模塊,只要第一次的加載還沒完成,就直接使用第一次的Promise if (installedChunkData) { promise = installedChunkData[2]; } else { promise = new Promise((resolve, reject) => { // 2.注意,此時(shí)的resolve,reject還沒執(zhí)行 installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; //3. 此時(shí)的installedChunkData 為[resolve, reject, promise] var url = scriptUrl + chunkId; var error = new Error(); // 4.在script標(biāo)簽加載完成或者加載失敗后執(zhí)行l(wèi)oadingEnded方法 var loadingEnded = (event) => { if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId)) { installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0) installedChunks[chunkId] = undefined; if (installedChunkData) { console.log("加載失敗....."); installedChunkData[1](error); // 5.執(zhí)行上面的reject,那resolve在哪里執(zhí)行呢? } } }; loadScript(url, loadingEnded, "chunk-" + chunkId, chunkId); } return promise; };
__webpack_require__.e
的主要邏輯在ensureChunk
方法中,注意該方法里面的第 1 到第 5 個(gè)注釋。這個(gè)方法創(chuàng)建一個(gè) promise,并調(diào)用loadScript
方法加載動(dòng)態(tài)模塊。需要特別主要的是,返回的 promise 的 resolve 方法并不是在 script 標(biāo)簽加載完成后改變。如果腳本加載錯(cuò)誤或者超時(shí),會(huì)在 loadingEnded 方法里調(diào)用 promise 的 reject 方法。
實(shí)際上,promise 的 resolve 方法是在腳本請(qǐng)求完成后,在 self["webpackChunkwebpack_demo"].push()執(zhí)行的時(shí)候調(diào)用的
如何執(zhí)行遠(yuǎn)程模塊?
遠(yuǎn)程模塊是通過self["webpackChunkwebpack_demo"].push()
函數(shù)執(zhí)行的
前面我們提到,a.js
請(qǐng)求返回的內(nèi)容是一個(gè)self["webpackChunkwebpack_demo"].push()
函數(shù)。當(dāng)請(qǐng)求完成,會(huì)自動(dòng)執(zhí)行這個(gè)函數(shù)。實(shí)際上,這就是一個(gè) jsonp 的回調(diào)方式。該方法的實(shí)現(xiàn)如下:
var webpackJsonpCallback = (data) => { var [chunkIds, moreModules] = data; var moduleId, chunkId, i = 0; for (moduleId in moreModules) { // 1.__webpack_require__.m存儲(chǔ)的是所有的模塊,包括靜態(tài)模塊和動(dòng)態(tài)模塊 __webpack_require__.m[moduleId] = moreModules[moduleId]; } for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (installedChunks[chunkId]) { // 2.調(diào)用ensureChunk方法生成的promise的resolve回調(diào) installedChunks[chunkId][0](); } // 3.將該模塊標(biāo)記為0,表示已經(jīng)加載過 installedChunks[chunkId] = 0; } }; self["webpackChunkwebpack_demo"] = []; self["webpackChunkwebpack_demo"].push = webpackJsonpCallback.bind(null);
所有通過import()
加載的模塊,經(jīng)過 webpack 編譯后,都會(huì)被 self["webpackChunkwebpack_demo"].push()
包裹。
總結(jié)
在 webpack 構(gòu)建編譯階段,import()
會(huì)被編譯成類似__webpack_require__.e("src_a_js").then(__webpack_require__.bind(__webpack_require__, "./src/a.js"))
的調(diào)用方式
__webpack_require__ .e("src_a_js") .then(__webpack_require__.bind(__webpack_require__, "./src/a.js")) .then((res) => { console.log("動(dòng)態(tài)加載a.js..", res); });
__webpack_require__.e()
方法會(huì)創(chuàng)建一個(gè) script 標(biāo)簽用于請(qǐng)求腳本,方法執(zhí)行完返回一個(gè) promise,此時(shí)的 promise 狀態(tài)還沒改變。
script 標(biāo)簽被添加到 document.head 后,觸發(fā)瀏覽器網(wǎng)絡(luò)請(qǐng)求。請(qǐng)求成功后,動(dòng)態(tài)的腳本會(huì)自動(dòng)執(zhí)行,此時(shí)self["webpackChunkwebpack_demo"].push()
方法執(zhí)行,將動(dòng)態(tài)的模塊添加到__webpack_require__.m
屬性中。同時(shí)調(diào)用 promise 的 resolve 方法改變狀態(tài),模塊加載完成。
腳本執(zhí)行完成后,最后執(zhí)行 script 標(biāo)簽的 onload 回調(diào)。onload 回調(diào)主要是用于處理腳本加載失敗或者超時(shí)的場(chǎng)景,并調(diào)用 promise 的 reject 回調(diào),表示腳本加載失敗
以上就是Webpack學(xué)習(xí)之動(dòng)態(tài)import原理及源碼分析的詳細(xì)內(nèi)容,更多關(guān)于Webpack動(dòng)態(tài)import的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript SweetAlert插件實(shí)現(xiàn)超酷消息警告框
SweetAlert是一款使用純js制作的消息警告框插件.這篇文章主要介紹了JavaScript SweetAlert插件實(shí)現(xiàn)超酷消息警告框的相關(guān)資料,需要的朋友可以參考下2016-01-01EasyUI的DataGrid綁定Json數(shù)據(jù)源的示例代碼
本篇文章主要介紹了EasyUI的DataGrid綁定Json數(shù)據(jù)源的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12Highcharts使用簡(jiǎn)例及異步動(dòng)態(tài)讀取數(shù)據(jù)
Highcharts 是一個(gè)用純JavaScript編寫的一個(gè)圖表庫, 能夠很簡(jiǎn)單便捷的在web網(wǎng)站或是web應(yīng)用程序添加有交互性的圖表,并且免費(fèi)提供給個(gè)人學(xué)習(xí)、個(gè)人網(wǎng)站和非商業(yè)用途使用,通過本文給大家介紹Highcharts使用簡(jiǎn)例及異步動(dòng)態(tài)讀取數(shù)據(jù)的相關(guān)知識(shí),感興趣的朋友一起學(xué)習(xí)吧2015-12-12uniapp小程序使用高德地圖api實(shí)現(xiàn)路線規(guī)劃的示例代碼
路線規(guī)劃常用于出行路線的提前預(yù)覽,我們提供4種類型的路線規(guī)劃,分別為:駕車、步行、公交和騎行,滿足各種的出行場(chǎng)景,這篇文章主要介紹了uniapp小程序使用高德地圖api實(shí)現(xiàn)路線規(guī)劃,需要的朋友可以參考下2023-01-01Javascript createElement和innerHTML增加頁面元素的性能對(duì)比
Javascript之createElement和innerHTML增加頁面元素的性能對(duì)比2009-09-09Javascript格式化并高亮xml字符串的方法及注意事項(xiàng)
這篇文章主要介紹了Javascript格式化并高亮xml字符串的方法及注意事項(xiàng),需要的朋友可以參考下2018-08-08