大多數情況下,我們並不關心 webpack 是怎麼做非同步載入的,但是作為前端開發工程師我們需要對非同步載入有一定的瞭解。
在講解之前,先讓我們搭建一個簡單的webpack
工程。
一、工程搭建
// package.json檔案
{
"name": "webpack-study",
"version": "1.0.0",
"description": "",
"main": "index.js",
"author": "",
"license": "ISC",
"scripts": {
"dev": "cross-env NODE_ENV=development webpack",
"build": "cross-env NODE_ENV=production webpack"
},
"dependencies": {
"cross-env": "^6.0.3",
"css-loader": "^3.2.0",
"rimraf": "^3.0.0",
"webpack": "^4.41.2"
},
"devDependencies": {
"webpack-chain": "^6.0.0",
"webpack-cli": "^3.3.10"
}
}
複製程式碼
這裡我使用了webpack-chain
的方式配置 webpack。有興趣的朋友可以去了解一下。
webpack-chain 常用配置
//webpack.config.js
const path = require("path");
const rimraf = require("rimraf");
const Config = require("webpack-chain");
const config = new Config();
const resolve = src => {
return path.join(process.cwd(), src);
};
// 刪除 dist 目錄
rimraf.sync("dist");
config
// 入口
.entry("src/index")
.add(resolve("src/index.js"))
.end()
// 模式
// .mode(process.env.NODE_ENV) 等價下面
.set("mode", process.env.NODE_ENV)
// 出口
.output.path(resolve("dist"))
.filename("[name].bundle.js");
config.module
.rule("css")
.test(/\.css$/)
.use("css")
.loader("css-loader");
module.exports = config.toConfig();
複製程式碼
然後在src
目錄下新增兩個檔案
// index.js
const css = import("./index.css");
const css2 = import("./index2.css");
複製程式碼
/* index.css和index2.css一樣 */
body {
width: 100%;
height: 100%;
background-color: red;
}
複製程式碼
二、原理解析
在講解之前讓我們允許一下,yarn dev
,對沒錯,這時您可以在dist
目錄下檢視到生成了 3 個檔案。
其中0.bundle.js
和1.bundle.js
分別對應index.css和index2.css
。非同步載入的模組會產生一個單獨的模組。
dist
┣ src
┃ ┗ index.bundle.js
┣ 0.bundle.js
┗ 1.bundle.js
複製程式碼
檢視index.bundle.js
原始碼,好像很多程式碼,其實精簡下來就是一個自執行函式.
(function(modules) {
// 模擬 require 語句
function __webpack_require__() {}
// 執行存放所有模組陣列中的第0個模組
__webpack_require__((__webpack_require__.s = 0));
})([
/*存放所有模組的陣列*/
]);
複製程式碼
(一) chunk.bundle.js 初識
非同步載入的 js,打包時會額外的打包成一個 js 檔案,比如0.bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
[0],
{
"./node_modules/css-loader/dist/runtime/api.js": function(
module,
exports,
__webpack_require__
) {
"use strict";
eval("...忽略其中的程式碼");
},
// 執行具體的模組程式碼
"./src/index.css": function(module, exports, __webpack_require__) {
eval("...忽略其中的程式碼");
}
}
]);
複製程式碼
通過分析0.bundle.js
我們瞭解到:
- 非同步載入的程式碼,會儲存在一個全域性的
webpackJsonp
中 webpackJsonp
push 的的值,兩個引數分別為- 非同步載入的檔案中存放的需要安裝的模組對應的 Chunk ID
- 非同步載入的檔案中存放的需要安裝的模組列表
- 在
滿足某種情況
下,會執行具體模組中的程式碼,那麼在什麼時候執行,請檢視下面的分析
(二)初識 bundle.js
bundle
是一個立即執行函式,是入口檔案。- webpack 將所有模組打包成了 bundle 的依賴,通過一個物件注入
jsonpScriptSrc
jsonpScriptSrc
的主要作用是通過publicPath
+chunkId
的方式獲取到非同步載入模組的url
地址。
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + ({}[chunkId] || chunkId) + ".bundle.js";
}
複製程式碼
webpack_require
__webpack_require__
是webpack
的核心,webpack
通過__webpack_require__
引入模組。
__webpack_require__
對require
包裹了一層,主要功能是載入 js 檔案。
function __webpack_require__(moduleId) {
//如果需要載入的模組已經被載入過,就直接從記憶體快取中返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
//如果快取中不存在需要載入的模組,就新建一個模組,並把它存在快取中
var module = (installedModules[moduleId] = {
i: moduleId, // 模組在陣列中的 index
l: false, // 該模組是否已經載入完畢
exports: {} // 該模組的匯出值
});
// 從 modules 中獲取 index 為 moduleId 的模組對應的函式
// 再呼叫這個函式,同時把函式需要的引數傳入
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// 把這個模組標記為已載入
module.l = true;
// Return the exports of the module
return module.exports;
}
複製程式碼
webpack_require.e 非同步載入核心
非同步載入的核心其實是使用類jsonp
的方式,通過動態建立script
的方式實現非同步載入。
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// 判斷當前chunk是否已經安裝,如果已經使用
var installedChunkData = installedChunks[chunkId];
// installedChunkData為0表示已經載入了
if (installedChunkData !== 0) {
//installedChunkData 不為空且不為0表示該 Chunk 正在網路載入中
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
//installedChunkData 為空,表示該 Chunk 還沒有載入過,去載入該 Chunk 對應的檔案
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push((installedChunkData[2] = promise));
// 通過 DOM 操作,往 HTML head 中插入一個 script 標籤去非同步載入 Chunk 對應的 JavaScript 檔案
var script = document.createElement("script");
var onScriptComplete;
script.charset = "utf-8";
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
// 檔案的路徑為配置的 publicPath、chunkId 拼接而成
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
// 當指令碼載入完成,執行對應回撥
onScriptComplete = function(event) {
// 避免IE的記憶體洩漏
script.onerror = script.onload = null;
clearTimeout(timeout);
// 去檢查 chunkId 對應的 Chunk 是否安裝成功,安裝成功時才會存在於 installedChunks 中
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
var errorType =
event && (event.type === "load" ? "missing" : event.type);
var realSrc = event && event.target && event.target.src;
error.message =
"Loading chunk " +
chunkId +
" failed.\n(" +
errorType +
": " +
realSrc +
")";
error.name = "ChunkLoadError";
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
// 設定非同步載入的最長超時時間
var timeout = setTimeout(function() {
onScriptComplete({ type: "timeout", target: script });
}, 120000);
// 在 script 載入和執行完成時回撥
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
複製程式碼
webpackJsonpCallback
webpackJsonpCallback
的主要作用是每個非同步模組載入並安裝。
webpack 會安裝對應的 webpackJsonp 檔案。
var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []);
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重寫陣列 push 方法,重寫之後,每當webpackJsonp.push的時候,就會執行webpackJsonpCallback程式碼
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
複製程式碼
function webpackJsonpCallback(data) {
//chunkIds 非同步載入的檔案中存放的需要安裝的模組對應的 Chunk ID
// moreModules 非同步載入的檔案中存放的需要安裝的模組列表
var chunkIds = data[0];
var moreModules = data[1];
//迴圈去判斷對應的chunk是否已經被安裝,如果,沒有被安裝就吧對應的chunk標記為安裝。
var moduleId,
chunkId,
i = 0,
resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (
Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
installedChunks[chunkId]
) {
// 此處的resolves push的是在__webpack_require__.e 非同步載入中的 installedChunks[chunkId] = [resolve, reject];的resolve
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
// 執行非同步載入的所有 promise 的 resolve 函式
resolves.shift()();
}
}
複製程式碼
三、總結
原理很簡單,就是利用的 jsonp 的實現原理載入模組,只是在這裡並不是從 server 拿資料而是從其他模組中。 整體的流程為:
- 載入入口 js 檔案,
__webpack_require__(__webpack_require__.s = 0)
- 執行入口 js 檔案:modules[moduleId].call(module.exports, module, module.exports, webpack_require);
- 具體執行的程式碼為:
(function(module, exports, __webpack_require__) {
eval(
'module.exports = __webpack_require__(/*! D:\\webpack\\src\\index.js */"./src/index.js");\n\n\n//# sourceURL=webpack:///multi_./src/index.js?'
);
/***/
});
//和
eval(
'\r\nconst css = __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.t.bind(null, /*! ./index.css */ "./src/index.css", 7))\r\nconst css2 = __webpack_require__.e(/*! import() */ 1).then(__webpack_require__.t.bind(null, /*! ./index2.css */ "./src/index2.css", 7))\r\n\n\n//# sourceURL=webpack:///./src/index.js?'
);
複製程式碼
-
由於上述程式碼分別
__webpack_require__.e
了0 和 1,分別使用類jsonp
的方式非同步載入對應 chunk,並快取到 promise 的 resolve 中,並標記對應 chunk 已經載入** -
呼叫對應 chunk 模組時會在 window 上註冊一個 webpackJsonp 陣列,
window['webpackJsonp'] = window['webpackJsonp'] || []
。並且執行push
操作。由於push
操作是使用webpackJsonpCallback
進行重寫的,所以每當執行push
的時候就會觸發webpackJsonpCallback
. webpackJsonpCallback 標記對應 chunk 已經載入並執行程式碼。
while (resolves.length) {
// 執行非同步載入的所有 promise 的 resolve 函式
resolves.shift()();
}
複製程式碼
- 完成各個模組的載入