探究 webpack 如何實現模塊異步加載
之前一篇文章講了 webpack 如何實現模塊的同步加載,在此基礎上,今天接着講如何實現異步加載模塊。
一、源碼
還是用原來的例子,但是主文件中改成按需加載(異步加載)
原來:import func from './func.js'
現在:const func = () => import('./func.js')
打包後源碼(main.js 文件):
(function (modules) {
function webpackJsonpCallback(data) {}
var installedModules = {};
var installedChunks = {
"main": 0
};
function __webpack_require__(moduleId) {}
__webpack_require__.e = function requireEnsure(chunkId) {}
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({ //主文件
"./src/index.js":
(function (module, __webpack_exports__, __webpack_require__) {
const func = () => __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/func.js"))
func.add(1,2)
})
});
打包後源碼(0.main.js 文件):
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/func.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
class func{
static add(val1,val2){
return val1 + val2
}
}
__webpack_exports__["default"] = (func);
})
}
]);
和之前同步引入打包的文件對比,我們可以發現 main.js 打包內容多了一些函數,同時也分割出一個 0.main.js 文件(裏面的內容就是引入文件內容)。
我們已經知道如何同步加載模塊了(__webpack_require__函數),那現在疑問就有兩點,如何把異步文件加載到當前頁面?如何把異步文件內的模塊插入到主文件模塊對象內?
二、分析
我們現在從主文件源碼觀察
const func = () => __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/func.js"))
使用了 _webpack_require_.e 函數返回了 Promsie 對象,如果成功則加載引用文件。我們看看 main.js 中的 _webpack_require_.e 做了什麼
(1)異步文件加載到當前頁面
//moduleId 打包出的模塊id
//chunkId 分離出的js文件id
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// 用於 javascript 加載 JSONP 的模塊
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 0 標誌已緩存
// 1.查找緩存
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 2.模塊緩存
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 引入script,加載模塊
var script = document.createElement('script');
script.src = __webpack_require__.p + "" + chunkId + ".main.js"; //0.main.js
//onScriptComplete 超時、錯誤處理
var timeout = setTimeout(function () {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = onScriptComplete
document.head.appendChild(script);
}
}
//加載
return Promise.all(promises);
};
_webpack_require_.e 這個函數主要做了兩件事,
-
創建 script 標籤引入 0.main.js 文件
-
創建一個 Promise 對象,如果 script 引入文件 onload 則返回成功(接下去就是走上面的加載 func.js 文件流程),onerror 或者超時則進行失敗處理。
(2)異步文件的模塊插入到主文件模塊對象
我們在看看異步引用文件 0.main.js
與同步加載相比,多了 window[“webpackJsonp”]
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
...
...
}]);
我們再看看主文件中與之相關的代碼
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
我們可以看到 window[“webpackJsonp”] 是一個對象, window[“webpackJsonp”].push 指向 webpackJsonpCallback 函數。異步文件通過這個指針,把內容傳進 webpackJsonpCallback 函數內。
webpackJsonpCallback函數 (因爲類似於jsonp的callback函數,故有此名):
(function (modules) {
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i = 0, resolves = [];
//收集模塊,將所有“chunkIds”標記爲已加載
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
//異步文件的模塊內容插入到主文件模塊對象 modules 中
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
//parentJsonpFunction指針對象指向webpackJsonpCallback
if (parentJsonpFunction)
parentJsonpFunction(data);
//執行並觸發回調
while (resolves.length) {
resolves.shift()();
}
};
......
}
webpackJsonpCallback 首先收集文件內模塊,將所有“chunkIds”標記爲已加載,把該模塊插入主頁面模塊內,然後加載模塊。
主文件模塊就是通過 installedChunks 內的 chunkId 標記判斷異步文件模塊的加載情況從而返回 Promise 。