webpack打包后运行时文件分析(webpack5)

本文基于webpack5,分析打包后的文件,参考了网上的资料,同时加上了个人的理解,欢迎讨论。
代码已经关联到github: 链接地址 觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:

简单打包

文件介绍
//index.js 入口
import { cc } from './Test'
cc()

//Test.js
function cc(){}
export {cc}
运行时分析

webpack执行打包后生成的js文件是一个立即执行函数,其参数modules为一个对象{},包含我们所有要打包的文件。
这个立即执行函数主要分为以下部分:

  1. webpack_modules:保存webpack已经注册的模块,一个键值对的对象
  2. webpack_require:对应import(es6),实现模块加载和缓存,模块管理核心
  3. webpack_module_cache:模块缓存
  4. webpack_require.o:工具函数,判断是否有某属性
  5. webpack_require.d:对应export,用来定义导出变量对象
  6. webpack_require.r:区分是否es模块,给导出导出变量对象添加__esModule:true属性,用来兼容es和commonJS等模块的

模块加载执行流程:

  1. 模块使用 __webpack_require__ 加载模块,接受的参数是 moduleId (文件路径),返回的是模块的exports
  2. 首先会判断是否存在缓存,存在则返回模块的 exports ,不存在则创建一个模块并缓存
  3. 接着按照文件路径执行 __webpack_modules__ 中模块函数
  4. 执行完毕后返回模块的 exports
(() => { // webpackBootstrap
  "use strict";
  var __webpack_modules__ = ({

    "./src/web/Test.js":
      /*!*************************!*\
        !*** ./src/web/Test.js ***!
        \*************************/
      ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"cc\": () => (/* binding */ cc)\n/* harmony export */ });\nfunction cc(){\n\n}\n\n\n\n//# sourceURL=webpack://webpack_step1/./src/web/Test.js?");

      }),

    "./src/web/index.js":
      /*!**************************!*\
        !*** ./src/web/index.js ***!
        \**************************/
      ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _Test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Test */ \"./src/web/Test.js\");\n// import { count } from './Count'\n\n\n// count()\n(0,_Test__WEBPACK_IMPORTED_MODULE_0__.cc)()\n\n// // 异步加载\n// import('./Number').then(({number}) => {\n//     number()\n// });\n\n// if(module.hot){\n//     module.hot.accept('./Number',()=>{\n//         const numberDiv = document.getElementById('mynumber')\n//         document.body.removeChild(numberDiv)\n//         number()\n//     })\n// }\n\nconsole.log('web!!!333')\n\n//# sourceURL=webpack://webpack_step1/./src/web/index.js?");

      })

  });
  /************************************************************************/
  // 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)__webpack_exports__
    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;
  }

  /************************************************************************/
  /* 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)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();

  /* webpack/runtime/hasOwnProperty shorthand */
  (() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();

  /* 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 });
    };
  })();

  /************************************************************************/

  // startup
  // Load entry module and return exports
  // This entry module can't be inlined because the eval devtool is used.
  var __webpack_exports__ = __webpack_require__("./src/web/index.js");

})();

打包后模块解析
__webpack_require__.r(__webpack_exports__);//es模块
__webpack_require__.d(__webpack_exports__, {//定义模块,并将导出的函数定义到模块的导出变量export当中
    "cc": () => ( /* binding */ cc)
});

function cc() {} //# sourceURL=webpack://webpack_step1/./src/web/Test.js?

含有异步加载的打包

文件介绍
//index.js 入口
import { cc } from './Test'
cc()
// 异步加载
import('./Number').then(({ number }) => {
    number()
});
console.log('web!!!333')

//Test 省略

//Number.js
export function number(){
    const div = document.createElement('div')
    div.innerHTML = 100
    div.setAttribute('id','mynumber')
    document.body.appendChild(div)
}
运行时分析

异步打包的模块,多出了以下方法

  1. installedChunks:缓存异步加载的模块键值对集合,内部存的模块有未加载,加载中,加载完毕几种形式。
  2. webpack_require.e:加载异步模块的入口方法,使用 __webpack_require__.f.j 方法加载模块。
  3. webpack_require.f:异步加载方法集,方便后续添加其他异步加载方法。
  4. webpack_require.f.j :异步加载js的方法,主要是将js异步的Promise设置到installedChunks 中(webpackJsonpCallback会调用),还处理其他拦截相同文件加载、加载文件的url、加载异常回调的等,最后会调用 __webpack_require__.l 加载js文件。
  5. webpack_require.l::动态创建script标签去加载js文件。
  6. webpackJsonpCallback:异步js文件内部执行方法,主要是 1.将installedChunks 中的对应模块的Promise fullfill掉,执行引入异步加载import().then的代码 2.将异步模块存到 __webpack_modules__ 这个全局注册模块集合中。
  7. webpack_require.p:获得公共路径
  8. webpack_require.g:全局this

模块加载执行流程:

// 引入异步js模块
__webpack_require__.e("src_web_Number_js").then(
    __webpack_require__.bind(__webpack_require__,"./src/web/Number.js")//注意这里注入了一个加载js的函数
  ).then(
    ({number}) => {
      number()//执行异步函数
    }
);
  1. 异步加载时,从入口文件出发,发现有异步加载的js则调用 __webpack_require__.e -> __webpack_require__.f.j -> __webpack_require__.l , 使用动态创建script标签的形式 下载模块文件,下载完毕后最终 __webpack_require__.e 的会返回一个的promise的实例(Promise.all)
  2. 下载完毕的js会自动执行self["webpackChunkwebpack_step1"].push 方法,该方法已经在入口文件加载时重置成为了 webpackJsonpCallback 函数,用来触发引入该异步模块的回调(将第一步的Promise.all状态修改成fullfilled)和缓存该异步模块。
  3. 如果在我们的js代码中,如果是 import('').then() 格式,则在__webpack_require__.e 执行完毕返回,会执行 __webpack_require__ 去引入对应的异步模块文件
  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = __webpack_modules__; 

	/* 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) => {
      return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
        __webpack_require__.f[key](chunkId, promises);
        return promises;
      }, []));
    };
  })();

	/* webpack/runtime/global 获得this*/
	(() => {
		__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/get javascript chunk filename */
  (() => {
    // This function allow to reference async chunks
    __webpack_require__.u = (chunkId) => {
      // return url for filenames based on template
      return "" + chunkId + ".js";
    };
  })();

	/* webpack/runtime/load script JSONPscript标签的方式异步模块加载函数,真正动态去加载js文件*/
	(() => {
		var inProgress = {};
		var dataWebpackPrefix = "webpack_step1:";
		// loadScript function to load a script via script tag
		__webpack_require__.l = (url, done, key, chunkId) => {
			//存在正在加载相同的模块则存入回调函数,待js文件加载完再执行
			if(inProgress[url]) { inProgress[url].push(done); return; }
			var script, needAttach;
			//判断是否该模块的js文件已经加载过,加载过则重新
			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; }
				}
			}
			// 动态创建srcipt标签,使用jsonp的模式异步加载
			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;
			}
			// js加载的处理:1.js加载出错,2.js加载完毕,执行回调 3.js加载超时
			// 3种情况满足一种均会进行删除该scrpit标签
			inProgress[url] = [done];
			var onScriptComplete = (prev, event) => {
				// 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);
			}
			;
			// 加载超时(2分钟)处理
			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);
			needAttach && document.head.appendChild(script);
		};
	})();

/* 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;
})();


(() => {
    // no baseURI

    // 该对象用户缓存已经加载和正在加载的chunk,在入口文件(把入口文件也当做一个chunk)中初始化,初始化后包含了入口chunk的状态,
    // 此例中入口chunk的Id为web,webpack分配chunkId是0开始计数递增的,实际上入口chunk的Id一定是最大的,从上面的代码中值0表示当前的入口chunk已经加载了。
		// undefined = chunk not loaded, null = chunk preloaded/prefetched
		// Promise = chunk loading, 0 = chunk loaded
		var installedChunks = {
			"web": 0
		};


		__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".
		
					// 正在加载则存入promises等待加载完毕 a Promise means "currently loading".
					if(installedChunkData) {
						promises.push(installedChunkData[2]);
					} else {
						if(true) { // all chunks have JS
							// setup Promise in chunk cache 将加载的模块缓存,将其缓存到installedChunks及推到promises
							var promise = new Promise((resolve, reject) => {
								installedChunkData = installedChunks[chunkId] = [resolve, reject];
							});
							promises.push(installedChunkData[2] = promise);
		
							// start chunk loading
							var url = __webpack_require__.p + __webpack_require__.u(chunkId);
							// create error before stack unwound to get useful stacktrace later
							var error = new Error();
							//加载完毕回调函数,处理异步加载js出错
							var loadingEnded = (event) => {
								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);
									}
								}
							};
							__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
						} else installedChunks[chunkId] = 0;
					}
				}
		};

    // no prefetching

    // no preloaded

    // no HMR

    // no HMR manifest

    // no deferred startup

		// install a JSONP callback for chunk loading
		var webpackJsonpCallback = (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 = [];
      // 遍历需要执行的chunk
			for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        // 如果该chunk正在加载中状态
				if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
          // 暂存该chunk对应Promise的resolve方法
          resolves.push(installedChunks[chunkId][0]);
        }
        // 将该chunk的状态置为加载完成
				installedChunks[chunkId] = 0;
      }
      // 遍历这些chunk依赖的模块并缓存模块到modules对象中,这个对象是在入口文件的最外层方法当做参数传入的
			for(moduleId in moreModules) {
				if(__webpack_require__.o(moreModules, moduleId)) {
					__webpack_require__.m[moduleId] = moreModules[moduleId];
				}
			}
      if(runtime) runtime(__webpack_require__);
      // 将加载的chunk存入chunkLoadingGlobal
      if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
      // 将加载的chunk对应的Promise fullfill掉,这时就会加载import().then的代码
			while(resolves.length) {
				resolves.shift()();
			}
		
		}
		//全局加载的chunk
		var chunkLoadingGlobal = self["webpackChunkwebpack_step1"] = self["webpackChunkwebpack_step1"] || [];
    //如果已经存在全局加载的chank模块信息,则遍历去加载
    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    //将全局加载的chunk的push函数修改成webpackJsonpCallback,并将chunkLoadingGlobal以前的push函数作为第一个预置参数,将后续加载的异步模块存到chunkLoadingGlobal
		chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
		// no deferred startup
  })();
打包后的模块解析解析
//入口
__webpack_require__.r(__webpack_exports__);//es模块
var _Test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./Test */ "./src/web/Test.js");//引入同步js模块
(0, _Test__WEBPACK_IMPORTED_MODULE_0__.cc)()
// 引入异步js模块
__webpack_require__.e("src_web_Number_js").then(
    __webpack_require__.bind(__webpack_require__,"./src/web/Number.js")//注意这里注入了一个加载js的函数
  ).then(
    ({number}) => {
      number()//执行异步函数
    }
);
console.log('web!!!333') //# sourceURL=webpack://webpack_step1/./src/web/index.js?

//Number.js
(self["webpackChunkwebpack_step1"] = self["webpackChunkwebpack_step1"] || []).push([["src_web_Number_js"], {
    "./src/web/Number.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

        "use strict";
        eval(
            "__webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, {   "
            number ": () => (/* binding */ number) });function number(){    const div = document.createElement('div')    div.innerHTML = 100    div.setAttribute('id','mynumber')    document.body.appendChild(div)}//# sourceURL=[module]//# sourceURL=webpack-internal:///./src/web/Number.js"
        );
    })
}]);

runtime.js

从上面的打包可以看出,在入口 web.js 文件中,包含了webpack的运行环境(具体作用就是模块解析, 加载) 和 模块信息清单,在webpack中称为 runtime ,模块信息清单在每次有模块变更(hash 变更)时都会变更, 所以我们想把这部分代码单独打包出来, 配合后端缓存策略, 这样就不会因为某个模块的变更导致包含模块信息的模块(通常会被包含在最后一个 bundle 中)缓存失效。
webpack.config.js 就可以配置:


module.exports = {
    optimization: {
        runtimeChunk: {
            name: 'runtime' // 将runtime分离
        },
    },
}

这样子实现,则模块的和runtime就会分别打包,我们都知道js标签的加载顺序会影响到相关js的执行,这里我们的模块必须依赖runtime.js ,如果模块js先加载,而runtime.js而后才加载,会不会导致问题呢?
经测试是不会,其实就跟我们之前的异步加载js模块类似,还记得全局变量 chunkLoadingGlobal 么?

webpack 运行时内部维护了一个数组变量, 这个变量被挂载在 window 对象上:

window["webpackChunkwebpack_step1"] = []

无论是 runtime 还是普通的 chunk 都会在IIFE函数中试图去读取这个属性, 如果没有读取到就为其赋值一个数组.

(window["webpackChunkwebpack_step1"] = window["webpackChunkwebpack_step1"] || []).push(xxx) // 除了runtime,每一个chunk都有

runtime的立即执行函数中, 会判断如果 window["webpackChunkwebpack_step1"] 的是否已包含内容, 如果有,也就意味着 runtime 加载之前有其他 chunk 加载了, 此时 runtime 就会读取这个数组中的内容然后在进行解析上之前加载完成的 chunk 。

var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
		//...
}
//全局加载的chunk
var chunkLoadingGlobal = self["webpackChunkwebpack_step1"] = self["webpackChunkwebpack_step1"] || [];
  //如果已经存在全局加载的chunk模块信息,则遍历去加载
  chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
  //将全局加载的chunk的push函数修改成webpackJsonpCallback,并将chunkLoadingGlobal以前的push函数作为第一个预置参数,将后续加载的异步模块存到chunkLoadingGlobal
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值