zoukankan      html  css  js  c++  java
  • 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懒加载的执行原理和过程

  • 相关阅读:
    课后作业-阅读任务-阅读笔记-4
    团队编程项目作业5-小组评分
    课后作业-阅读任务-阅读提问-3
    20171110-构建之法:现代软件工程-阅读笔记
    团队-中国象棋-开发文档
    结对-贪吃蛇-结对项目总结
    20171129-构建之法:现代软件工程-阅读笔记
    课后作业-阅读任务-阅读提问-4
    软件工程课程总结
    团队-石头剪刀布-项目总结
  • 原文地址:https://www.cnblogs.com/xiaonanxia/p/14763151.html
Copyright © 2011-2022 走看看