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.
(' + 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懒加载的执行原理和过程

原文地址:https://www.cnblogs.com/xiaonanxia/p/14763151.html