Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wqhui opened this issue Apr 4, 2021 · 0 comments
Open

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

wqhui opened this issue Apr 4, 2021 · 0 comments

Comments

@wqhui
Copy link
Owner

wqhui commented Apr 4, 2021

简单打包

文件介绍

//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));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant