blog icon indicating copy to clipboard operation
blog copied to clipboard

webpack runtime 源码分析

Open creeperyang opened this issue 2 years ago • 0 comments
trafficstars

项目基本配置可参考基于 https://github.com/taniarascia/webpack-boilerplate.git

(一)从打包产物——html 开始

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <title>webpack-demo</title>
    <link rel="icon" href="/favicon.ico">
    <script defer="defer" src="/js/runtime.dacd45666f2c5eb35b8d.bundle.js"></script>
    <script defer="defer" src="/js/main.7086fc1df0c0b6a0d989.bundle.js"></script>
    <link href="/styles/main.64e10d654e2697258d08.css" rel="stylesheet">
</head>

<body>
    <div id="root"></div>
</body>

</html>

从浏览器角度,html 是应用入口和运行起点。可以看到,编译出的JS通过<script>标签插入了 html。

  • 其中 /js/runtime.dacd45666f2c5eb35b8d.bundle.js 是 webpack 提供的运行时,是编译出代码能运行的基础;
  • /js/main.7086fc1df0c0b6a0d989.bundle.js 是我们的入口模块(可能因为 CONCATENATE MODULE 的优化,除了入口module外有其它module的代码)。

这里需额外注意到:

defer 异步加载,不阻塞 html 下载和解析,且保证这些 script 按序执行(保证了runtime先执行);按序执行非常重要,runtime 脚本提供了webpack的全局变量/方法,提供了模块加载执行的能力等等。

(二)打包产物——模块/chunk 是怎么包装的?

简单来说,我们写的js文件(模块/module)被webpack处理后变成了什么?

有最简单的 src/js/info.js

export const text = '2021/02/01'

编译后就是dist/js/info.be139a6f62da8ba74d4c.chunk.js

"use strict";
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[996],{

/***/ 67:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "text": () => (/* binding */ text)
/* harmony export */ });
var text = '2021/02/01';

/***/ })

}]);

为了让编译后的代码保持可读性,webpack的配置去除了压缩。我们可以看到:

  1. 代码被包装在 (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 我们的真正代码 }

    1. 这个并不是一定的,也可能是 (module) => { 我们的真正代码 },这取决于代码是否 ES Module 形式的等等。
    2. webpack 一定帮我们提供了 module|exports|require 的环境,保证了我们代码能正常运行。当然这三个变量webpack会编译并匹配包装函数。
    3. __webpack_require__ 上提供了一些工具函数,具体是什么下个章节讨论。
  2. webpack的编译产出是 chunk,是一个或多个module产出一个chunk。所以我们看到 {moduleId: wrappedModuleCode} 的形式:

    1. 就是上面的 {67: (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 代码 })}
  3. 最终chunk的形式是 (self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push(参数)

    1. 参数是数组,self["webpackChunkwebpack_demo"].push 保证了数组里面如果有代码需要运行是可以运行的,而不是静态的数组。
    2. 参数的形式是 [[chunkId], moreModules, runtime]runtime 就是在 push 时可以运行的代码。

看看entry point编译出的 initial chunk 会是什么样?

(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[179],{

/***/ 573:
/***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => {

"use strict";

;// CONCATENATED MODULE: ./src/js/title.js
var getTitle = function getTitle() {
  return '柴门闻犬吠,风雪夜归人。';
};
// EXTERNAL MODULE: ./src/js/log.js
var log = __webpack_require__(967);
var log_default = /*#__PURE__*/__webpack_require__.n(log);
;// CONCATENATED MODULE: ./src/images/logo.svg
const logo_namespaceObject = "data:image/svg+xml;base64,...........";
;// CONCATENATED MODULE: ./src/index.js
// Test import of a JavaScript function



// Test import of an asset


// Test import of styles

var logo = document.createElement('img');
logo.src = logo_namespaceObject;
var heading = document.createElement('h1');
heading.textContent = getTitle();
var app = document.querySelector('#root');
app.append(logo, heading);
setTimeout(function () {
  __webpack_require__.e(/* import() | info */ 996).then(__webpack_require__.bind(__webpack_require__, 67)).then(function (v) {
    var footer = document.createElement('footer');
    footer.textContent = v.text;
    app.append(footer);
    log_default()();
  });
}, 100);
__webpack_require__.e(/* import() */ 232).then(__webpack_require__.bind(__webpack_require__, 232)).then(function (v) {
  var div = document.createElement('div');
  div.textContent = v.author;
  app.append(div);
});

/***/ }),

/***/ 967:
/***/ ((module) => {

module.exports = function () {
  console.log('xxxxxxxxx');
};

/***/ })

},
/******/ __webpack_require__ => { // webpackRuntimeModules
/******/ /* webpack/runtime/startup prefetch */
/******/ (() => {
/******/ 	__webpack_require__.O(0, [179], () => {
/******/ 		__webpack_require__.E(996);
/******/ 	}, 5);
/******/ })();
/******/ 
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
/******/ var __webpack_exports__ = (__webpack_exec__(573));
/******/ }
]);

(三)打包产物——webpack runtime 有哪些功能

一切魔法都在 webpack runtime 脚本和模块代码的包装中,从 webpack runtime 开始分析编译出的代码,看看一个简单的项目是怎么跑起来的。而我们写的 import/export 这些模块化的代码,是怎么成功跑在浏览器里的。

这里首先明确一个概念:

  • chunk:分为 initial/non-initial 两种类型。
    • 其中 initial 是指 main chunk for entry point,包括entry point涉及的所有模块和依赖;
    • non-initial 是指可能懒加载的 chunk,由import()或者SplitChunksPlugin产生。
  • module/(webpack module): 组成程序的功能块(chunks of functionality),可能是js/css/img等等,可通过import/require/<img src=...>等等。

一定程度上,中文“模块”一词可能存在混用以上概念,注意分辨。

然后我们正式来分析 webpack runtime。webpack runtime 代码根据配置/是否有异步模块等不同,稍有不同,但总共不超过几百行代码。

webpack runtime 结构

整个webpack runtime是个IIFE(Immediately Invoked Function Expression),整体结构如下:

(() => { // webpackBootstrap
	"use strict";
	var __webpack_modules__ = ({});
/************************************************************************/
	// 缓存,缓存了所有模块的 exports
	var __webpack_module_cache__ = {};
	
	// The require function
	function __webpack_require__(moduleId) {
		// 检查是不是在缓存里?在直接返回即可。
		var cachedModule = __webpack_module_cache__[moduleId];
		if (cachedModule !== undefined) {
			return cachedModule.exports;
		}
		// 否则创建 module,并放到缓存。
		var module = __webpack_module_cache__[moduleId] = {
			// no module.id needed
			// no module.loaded needed
			exports: {}
		};
	
		// 执行对应的模块,执行完成后 epxorts 就有值了
		__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__;

    // 下面是一堆自执行函数(IIFE):
    // 1. 为__webpack_require__添加各种方法;
    // 2. 最终往window上添加 webpackChunkwebpack_demo 数组(最终是否叫webpackChunkwebpack_demo取决于config)。
})()
  1. 定义了 __webpack_modules__ 来存储所有模块,定义了 __webpack_require__ 来作为模块中用到的 require 方法。

  2. 通过一堆 IIFE 在 __webpack_require__ 上定义了一堆工具函数,这些函数可以被编译的模块去使用。

  3. 最后一个IIFE中定义了一个唯一全局变量 'webpackChunkwebpack_demo',该变量为数组,提供 push 方法来加载 chunk。

    • 变量名由 output.chunkLoadingGlobal 等配置确定(默认值是'webpackChunkwebpack');
    • 所有的模块最终通过 webpackChunkwebpack_demo.push 来执行,或者说所有的模块被它包装: (self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[id],{id:code},runtime])

最终我们的入口 chunk 执行时,通过webpackChunkwebpack_demo.push来最终执行业务代码。

webpack runtime 中的工具函数解析

核心的 'webpackChunkwebpack_demo'(即webpackJsonpCallback

webpackChunkwebpack_demo.push 是什么?webpackChunkwebpack_demo.push 最终调用 webpackJsonpCallback

// self["webpackChunkwebpack_demo"]一般为undefined,初始化为空数组;如果不为空则保留。
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];

// 假如 self["webpackChunkwebpack_demo"] 之前非空,那么对其每项调用 webpackJsonpCallback,
// 把相关 chunks & modules 存入缓存(module未执行,后面的 startup 真正执行)。
// 这里 0 作为参数传入,即 parentChunkLoadingFunction 是0,防止了 webpackJsonpCallback 执行时元素重复压入chunkLoadingGlobal。
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));

// 设置 chunkLoadingGlobal数组的push方法(这是其它模块的wrapper function);
// webpackJsonpCallback 绑定 parentChunkLoadingFunction 为 chunkLoadingGlobal.push, 元素被压入chunkLoadingGlobal
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

来具体看看 webpackJsonpCallback 作用是什么。请结合上面的 initial chunk 代码来看(看调用chunkLoadingGlobal.push的传参)。

var __webpack_modules__ = ({});
// expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;

// 缓存所有加载完成/加载中的模块,0代表加载完成
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
var installedChunks = {
	666: 0
};

/**
 * install a JSONP callback for chunk loading
 * @params {function|0} parentChunkLoadingFunction,可能是数组的push方法,也可能是0
 * @params {Array} data 格式是 [[moduleId], {moduleId: moduleCode}, runtime]
 * 
 * moduleId: 数字;moduleCode:wrapper function包裹的模块代码;runtime: (__webpack_require__)=>any
 **/
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
    // "moreModules" 就是当前加载的chunk自带的模块,包括自身代码的模块、CONCATENATED过来的模块、其它依赖模块。
	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;
    // 当前加载的chunk一般不会是0,所以执行这个if内逻辑,把moreModules添加到__webpack_modules__
	if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
		for(moduleId in moreModules) {
            // moreModules 形式为 {id: wrappedCode},我们直接把它们挂载到 __webpack_modules__
            // 方便之后__webpack_require__(id) 可以正确执行对应模块。
			if(__webpack_require__.o(moreModules, moduleId)) {
				__webpack_require__.m[moduleId] = moreModules[moduleId];
			}
		}
        // 然后看有需要执行的代码就直接执行
		if(runtime) var result = runtime(__webpack_require__);
	}
    // push 到 chunkLoadingGlobal 数组
	if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
	for(;i < chunkIds.length; i++) {
		chunkId = chunkIds[i];
        // 如果这个chunk是loading状态,则resolve掉它,方便通知之前等待该chunk的代码继续执行。
		if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
			installedChunks[chunkId][0]();
		}
        // 标记这个chunk加载完成
		installedChunks[chunkId] = 0;
	}
    // 那么__webpack_require__.O在干嘛?下一小节看
	return __webpack_require__.O(result);
}
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// chunkLoadingGlobal.push.bind(chunkLoadingGlobal) 作为 parentChunkLoadingFunction,此时是 chunkLoadingGlobal 的数组push方法
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

webpackJsonpCallback 作为真正的业务模块调用入口,主要就是把 push 进来的 chunk 指定的模块加载到 __webpack_modules__,并执行 chunk 指定的业务代码(模块)。

__webpack_require__.O 在干什么?

webpackJsonpCallback 中只剩__webpack_require__.O没搞明白作用,这一小节来解释下。

/* webpack/runtime/chunk loaded */
(() => {
	var deferred = [];
	__webpack_require__.O = (result, chunkIds, fn, priority) => {
		if(chunkIds) {
			priority = priority || 0;
			for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
			deferred[i] = [chunkIds, fn, priority];
			return;
		}
        // 以我们下面的传参为例,这里deferred是 [[179], () => {
        //  __webpack_require__.E(996);
        // }, 5]
		var notFulfilled = Infinity;
		for (var i = 0; i < deferred.length; i++) {
			var [chunkIds, fn, priority] = deferred[i];
			var fulfilled = true;
			for (var j = 0; j < chunkIds.length; j++) {
                // 5 & 1 是1,但是 Infinity >= 5 成立,
                // __webpack_require__.O 上只有 j,是检查chunkId对应的chunk是否已加载,这里第一次不成立而第二次成立
				if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
					chunkIds.splice(j--, 1);
				} else {
					fulfilled = false;
					if(priority < notFulfilled) notFulfilled = priority;
				}
			}
            // 第一次不成立,而第二次可以执行
			if(fulfilled) {
				deferred.splice(i--, 1)
				var r = fn(); // 第二次其实是执行 __webpack_require__.E(996),即prefetch 996
				if (r !== undefined) result = r;
			}
		}
		return result;
	};

    __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
})();

多数时候 __webpack_require__.O 接收的参数全部是 undefined,即什么都不干。所以略过它多数时候也不影响理解webpack流程。但我们的入口 chunk 其实会传入不一样的参数,我们来看下:

__webpack_require__ => { // webpackRuntimeModules
/* webpack/runtime/startup prefetch */
(() => {
	__webpack_require__.O(0, [179], () => {
		__webpack_require__.E(996);
	}, 5);
})();

var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
var __webpack_exports__ = (__webpack_exec__(573));
}

这是initial chunk的runtime函数执行时,会调用__webpack_require__.O,并且是在业务代码执行前。然后等webpackJsonpCallback执行时,__webpack_require__.O会执行第二次,此时会开始 prefetch id 为 996 的 chunk。

这符合预期:prefetch 在父 chunk 加载完成后开始。

__webpack_require__.F__webpack_require__.E 怎么配合完成 prefetch/preload

/* webpack/runtime/chunk prefetch function */
(() => {
	__webpack_require__.F = {};
	__webpack_require__.E = (chunkId) => {
		Object.keys(__webpack_require__.F).map((key) => {
			__webpack_require__.F[key](chunkId);
		});
	}
})();
__webpack_require__.F.j = (chunkId) => {
    // 如果 chunkId 不是 666(runtime chunk),也没有加载过
	if((!__webpack_require__.o(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && 666 != chunkId) {
        // null 代表 chunk preloaded/prefetched
		installedChunks[chunkId] = null;
		var link = document.createElement('link');

		if (__webpack_require__.nc) {
			link.setAttribute("nonce", __webpack_require__.nc);
		}
		link.rel = "prefetch";
		link.as = "script";
		link.href = __webpack_require__.p + __webpack_require__.u(chunkId);
		document.head.appendChild(link);
	}
};

代码一目了然。

重要的jsonp加载chunk相关 __webpack_require__.l & __webpack_require__.f.j & __webpack_require__.e

这里讲讲 webpack 最基本的怎么加载 chunk 的(jsonp)。

/* webpack/runtime/load script */
(() => {
	var inProgress = {};
	var dataWebpackPrefix = "webpack-demo:";
	// 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) => {
			// avoid mem leaks in IE.
			script.onerror = script.onload = null;
			clearTimeout(timeout);
			var doneFns = inProgress[url];
            // 加载完成就删除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);
		needAttach && document.head.appendChild(script);
	};
})();

/* 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;
		}, []));
	};
})();
// import()|require.ensure 的核心实现代码
	__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) {
                    // 正在加载,那么取出promise放到 promises 即可
					promises.push(installedChunkData[2]);
				} else {
					if(666 != chunkId) {
						// setup Promise in chunk cache
						var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
						promises.push(installedChunkData[2] = promise);
	
						// start chunk loading 构造异步模块的正确加载路径(publicPath + filename)
						var url = __webpack_require__.p + __webpack_require__.u(chunkId);
						// create error before stack unwound to get useful stacktrace later
						var error = new Error();
                        // 只需要处理加载失败的问题(因为chunk代码执行会更新installedChunks[chunkId]为0)
						var loadingEnded = (event) => {
							if(__webpack_require__.o(installedChunks, chunkId)) {
								installedChunkData = installedChunks[chunkId];
                                // 加入没有加载成功(chunk代码执行会更新installedChunks[chunkId]为0),
                                // 则置为 undefined(chunk not loaded)
								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;
				}
			}
	};

显而易见的 __webpack_require__.o & __webpack_require__.r & __webpack_require__.d

1. __webpack_require__.ohasOwnProperty

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

2. __webpack_require__.r

exports 对象加上 __esModule=true 等标记。

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

3. __webpack_require__.d

exports 对象加上我们导出的那些属性/方法(其它模块通过 import 来使用)。注意,这里是通过 getter 来导出。

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

creeperyang avatar Mar 14 '23 06:03 creeperyang