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 runtime 源码分析 #59

Open
creeperyang opened this issue Mar 14, 2023 · 0 comments
Open

webpack runtime 源码分析 #59

creeperyang opened this issue Mar 14, 2023 · 0 comments

Comments

@creeperyang
Copy link
Owner

creeperyang commented Mar 14, 2023

项目基本配置可参考基于 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 creeperyang changed the title Webpack概念与性能优化 Webpack之runtime、配置和性能优化 Mar 14, 2023
@creeperyang creeperyang changed the title Webpack之runtime、配置和性能优化 webpack runtime 源码分析 Mar 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant