WebPack之懒加载原理
代码结构
main.js
console.log("这是main页面");
import(/* webpackChunkName: "foo" */"./foo").then(res => {
console.log("动态导入foo")
console.log(res);
console.log(res.sum(1,10))
});
foo.js
export function sum(num1, num2) {
return num1 + num2;
}
webpack.common.js
const resolveApp = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const TerserPlugin = require("terser-webpack-plugin");
const { merge } = require("webpack-merge");
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");
const commonConfig = {
entry: {
// index: {import:"./src/index.js",dependOn:"shared"},
// main: {import:"./src/main.js",dependOn:"shared"},
// shared:['lodash']
main: "./src/main.js",
},
output: {
filename: "[name].bundle.js",
path: resolveApp("./build"),
chunkFilename: "[name].chunk.js"
},
resolve: {
extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
alias: {
"@": resolveApp("./src"),
pages: resolveApp("./src/pages"),
},
},
optimization:{
// 对代码进行压缩相关的操作
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
// chunkIds: "natural",
splitChunks:{
// async异步导入
// initial同步导入
// all 异步/同步导入
chunks: "all",
// 最小尺寸: 如果拆分出来一个, 那么拆分出来的这个包的大小最小为minSize
minSize: 200,
// 将大于maxSize的包, 拆分成不小于minSize的包
maxSize: 200,
// minChunks表示引入的包, 至少被导入了几次
minChunks: 1,
cacheGroups:{
vendor: {
test: /[\\/]node_modules[\\/]/,
filename: "[id]_vendors.js",
// name: "vendor-chunks.js",
priority: -10
},
default: {
minChunks: 2,
filename: "common_[id].js",
priority: -50
}
}
}
},
module: {
rules: [
{
test: /\.jsx?$/i,
use: "babel-loader",
},
{
test: /\.vue$/i,
use: "vue-loader",
},
{
test: /\.css/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
}),
new VueLoaderPlugin(),
]
};
module.exports = function(env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? "production": "development";
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig, config);
return mergeConfig;
};
其余部分参考代码地址:
https://github.com/JerryXu008/webapck_lazy_load_theory
打包之后代码
执行 npm run build ,然后查看生成的文件
foo.chunk.js
(self["webpackChunkwebpack_devserver"] = self["webpackChunkwebpack_devserver"] || []).push([["foo"], {
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"sum": () => (sum)
});
function sum(num1, num2) {
return num1 + num2;
}
})
}]);
main.bundle.js
(() => { // webpackBootstrap
var __webpack_modules__ = ({});
/************************************************************************/
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;
/************************************************************************/
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
//把所有的key和value都放到export中(这里是用代理的方式)
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = (chunkId) => {
const chunkKeyFunctionArr = Object.keys(__webpack_require__.f);//[j]
let promiseArr = chunkKeyFunctionArr.reduce((promises, key) => {
// 传入promises的地址到__webpack_require__.f,方法内部会把新创建的promise加入到promises
__webpack_require__.f[key](chunkId, promises);
//循环执行完毕,最终的promises会作为返回值传递给promiseArr
return promises;
}, [])
//Promise.all 的特点是 加入到里面的所有promise全部执行完毕(resolve或reject)之后,才会执行then
return Promise.all(promiseArr);
};
})();
/* webpack/runtime/get javascript chunk filename */
(() => {
// This function allow to reference async chunks
__webpack_require__.u = (chunkId) => {
// return url for filenames based on template
return "" + chunkId + ".chunk.js";
};
})();
/* webpack/runtime/global */
(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === 'object') return globalThis;
try {
return this || new Function('return this')();
} catch (e) {
if (typeof window === 'object') return window;
}
})();
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* webpack/runtime/load script */
(() => {
var inProgress = {};
var dataWebpackPrefix = "webpack_devserver:";
// loadScript function to load a script via script tag
__webpack_require__.l = (url, done, key, chunkId) => {
if (inProgress[url]) { inProgress[url].push(done); return; }
var script, needAttach;
if (key !== undefined) {
var scripts = document.getElementsByTagName("script");
for (var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if (s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
}
}
if (!script) {
needAttach = true;
script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.setAttribute("data-webpack", dataWebpackPrefix + key);
script.src = url;
}
inProgress[url] = [done];
var onScriptComplete = (prev, event) => {
console.log("脚本加载完毕")
//console.log("看看这里2",window.promise)
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var doneFns = inProgress[url];
delete inProgress[url];
script.parentNode && script.parentNode.removeChild(script);
doneFns && doneFns.forEach((fn) => (fn(event)));
if (prev) return prev(event);
}
;
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
console.log("开始插入脚本")
needAttach && document.head.appendChild(script);
console.log("插入脚本完毕")
};
})();
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
/* webpack/runtime/publicPath */
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript)
scriptUrl = document.currentScript.src
if (!scriptUrl) {
var scripts = document.getElementsByTagName("script");
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src
}
}
// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
__webpack_require__.p = scriptUrl;
})();
/* webpack/runtime/jsonp chunk loading */
(() => {
// no baseURI
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
//chunkId:foo
__webpack_require__.f.j = (chunkId, promises) => {
// JSONP chunk loading for javascript
var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
if (installedChunkData !== 0) { // 0 means "already installed".
// 这是缓存,可以不管
// a Promise means "currently loading".
if (installedChunkData) {
promises.push(installedChunkData[2]);
}
else {
if (true) {
// 为chunks建立一个promise
var promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
//window.promise = promise
//把新建的promise放到 promises数组中
promises.push(installedChunkData[2] = promise);
// start chunk loading
//获取chunk模块的url地址,用于动态在html中插入script标签
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
var error = new Error();
//url加载加载完毕之后,开始执行的方法
var loadingEnded = (event) => {
//console.log("看看这里4",window.promise)
if (__webpack_require__.o(installedChunks, chunkId)) {
installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) installedChunks[chunkId] = undefined;
if (installedChunkData) {
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;
installedChunkData[1](error);
}
}
};
//动态插入chunk模块的script标签
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
} else installedChunks[chunkId] = 0;
}
}
};
// install a JSONP callback for chunk loading
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
console.log("加载脚本push",parentChunkLoadingFunction,data)
var [chunkIds, moreModules, runtime] = data;
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
console.log("resolves:",resolves)
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
//把新加载的模块存入__webpack_modules__,供其他模块调用
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if (runtime) runtime(__webpack_require__);
//执行以前的push动作,传统的数组push
if (parentChunkLoadingFunction){
parentChunkLoadingFunction(data);
}
while (resolves.length) {
resolves.shift()();//执行resolve,此时会把promise的pending状态变为resolve
}
}
var chunkLoadingGlobal = self["webpackChunkwebpack_devserver"] = self["webpackChunkwebpack_devserver"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 这里很关键,重写了chunkLoadingGlobal的push方法,并把以前的push方法作为新push方法的第一个参数
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
// no deferred startup
}) ();
/************************************************************************/
var __webpack_exports__ = {};
/*!*********************!*\
!*** ./src/main.js ***!
\*********************/
console.log("这是main页面");
__webpack_require__.e("foo") //组装promise.all
//等promise.all内部的所有promise执行resolev或者reject,会执行then
//回调函数为__webpack_require__,并把"./src/foo.js"作为第一个参数,其实就是去调用key为"./src/foo.js"的模块
.then(__webpack_require__.bind(__webpack_require__, "./src/foo.js"))
.then(function (res) {
console.log("动态导入foo");
console.log(res);
console.log(res.sum(1, 10));
});
})()
;
//# sourceMappingURL=main.bundle.js.map
关键代码说明
foo.chunk.js
这个js主要是把打包前的代码封装到一个模块里,并用键值对的方式被push到 全局变量self["webpackChunkwebpack_devserver"]中,
这个全局变量是一个数组,最关键的地方是 他的push会被重写,在这个重写的方法中是实现懒加载的关键,后面会说。
- main.bundle.js
查看入口代码的位置:
console.log("这是main页面");
__webpack_require__.e("foo") //组装promise.all
//等promise.all内部的所有promise执行resolev或者reject,会执行then
//回调函数为__webpack_require__,并把"./src/foo.js"作为第一个参数,其实就是去调用key为"./src/foo.js"的模块
.then(__webpack_require__.bind(__webpack_require__, "./src/foo.js"))
.then(function (res) {
console.log("动态导入foo");
console.log(res);
console.log(res.sum(1, 10));
});
关键地方是__webpack_require__.e("foo"),这个从代码可以看出返回的是一个promise,方法的参数是chunkid,这个chunkid就是为foo.bundle.js 准备的
跳转到__webpack_require__.e,如下:
- webpack_require.e 方法
(() => {
__webpack_require__.f = {};
__webpack_require__.e = (chunkId) => {
const chunkKeyFunctionArr = Object.keys(__webpack_require__.f);//[j]
let promiseArr = chunkKeyFunctionArr.reduce((promises, key) => {
// 传入promises的地址到__webpack_require__.f,方法内部会把新创建的promise加入到promises
__webpack_require__.f[key](chunkId, promises);
//循环执行完毕,最终的promises会作为返回值传递给promiseArr
return promises;
}, [])
//Promise.all 的特点是 加入到里面的所有promise全部执行完毕(resolve或reject)之后,才会执行then
return Promise.all(promiseArr);
};
})();
这里为了方便观察,微调了打包之后的这个方法的代码,这个方法最后返回一个Primose.all ,这个值也是一个promise,特点是里面的所有promise全部执行完毕(resolve或reject)之后,才会执行then。也就是说 要等到prmiseArr里面的所有promise都为非pending模式之后,才执行。
通过调试,promise的状态改变是在__webpack_require__.f[key](chunkId, promises)方法中,所以要跳转到这个方法(此时的key为j,所以跳转到__webpack_require__.f.j)
- webpack_require.f.j 方法
这里只列关键点:
// 为chunks建立一个promise
var promise = new Promise((resolve, reject) =>
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
//把新建的promise放到 promises数组中
promises.push(installedChunkData[2] = promise);
这里是把promise加入到promises数组中,此时的promise的状态是pending
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
url 为 foo.chunk.js 文件在服务器的位置,需要首先下载下来
所以跳转到__webpack_require__.l
- webpack_require.l 方法
var onScriptComplete = (prev, event) => {
console.log("脚本加载完毕")
//console.log("看看这里2",window.promise)
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var doneFns = inProgress[url];
delete inProgress[url];
script.parentNode && script.parentNode.removeChild(script);
doneFns && doneFns.forEach((fn) => (fn(event)));
if (prev) return prev(event);
}
;
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
console.log("开始插入脚本")
needAttach && document.head.appendChild(script);
console.log("插入脚本完毕")
关键地方在document.head.appendChild(script),当把脚本插入到html中,就开始下载文件了。
但是有一个地方要注意:
promise 由 pending变为 fullfilling 并不是在onScriptComplete中变的,而是在 html加载 foo.chunk.js 的过程中改变的,关键地方就是foo.chunk.js中的
push方法
- 查看push方法
var chunkLoadingGlobal = self["webpackChunkwebpack_devserver"] = self["webpackChunkwebpack_devserver"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 这里很关键,重写了chunkLoadingGlobal的push方法,并把以前的push方法作为新push方法的第一个参数
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
-
webpackJsonpCallback 方法
关键点就是箭头的三个指向,主要做的就是把foo模块的键值对加入到 全局对象中,然后执行promise的resolve方法
等执行完resolve方法,promise.all 的 then方法就可以执行了。 -
prmise.all(xxx).then()
在then中的回调方法是__webpack_require__,这个方法是所有模块的通用方法,就是 通过传递key值,从全局对象中找出模块,然后执行模块内的代码。 -
webpack_require
标红处就是执行foo的模块代码,下面跳转到模块代码 -
foo模块代码
关键地方就是标红处,这个方法的作用是给要导出的module添加key和value -
webpack_require.d 方法
此时exports 里面就会有sum方法的键值对了。
执行完毕之后,会回到 __webpack_require__中,这个方法最后一个就是返回 module.exports -
下一个then方法
.then(function (res) {
console.log("动态导入foo");
console.log(res);
console.log(res.sum(1, 10));
});
第一个then执行完毕后,会返回一个新的promise,之后会在执行一个then,这个then里面的回调函数就是上一个then返回的exports,所以
从里面取出sum,执行即可
以上就是webpack懒加载的执行原理和过程