webpack打包文件分析
学习webpack原理通常都是从打包文件入手,从入口出发再深入探究。
单文件
最简单的情况下,项目中只src/index.js,同时index.js的代码还特别简单:
const hello = 'hello~';console.log(hello);
webpack v4
运行webpack —mode development,从dist中可以看到main.js。删除掉不必要的注释,发现其结构是这样的:
(function (modules) {// 一大堆逻辑})({"./src/index.js":(function (module, exports) {eval("const hello='hello~';\n\n\nconsole.log(hello);\n\n\n//#sourceURL=webpack:///./src/index.js?");})});
可以看到这是一个IIFE(立即执行函数:Immediately-invoked function expression, IIFE)。
该IIFE如参数定义叫modules,实际传参是对象:
{// 对象key"./src/index.js":// 对象value是一个函数(function (module, exports) {eval("const hello='hello~';\n\n\nconsole.log(hello);\n\n\n//#sourceURL=webpack:///./src/index.js?");})}
这个对象的key是文件路径,这个对象的值也是函数,函数体执行了eval,eval中的内容才是真正要执行的代码(为什么要用eval?关于eval )。
继续看这个函数function (modules) { },先看结构:
// 结构说明function (modules) {// 模块缓存var installedModules = {};// 定义__webpack_require__function __webpack_require__(moduleId) { }// 定义 __webpack_require__的各种个样的属性// ... ...// 入口文件是 index.js// 调用__webpack_require__return __webpack_require__(__webpack_require__.s = "./src/index.js");}
从上述结构中,可以看到,核心其实还是在return这个地方,调用了webpack_require,传进去的参数是入口文件的地址”./src/index.js”。中间很大篇幅的代码量是在对webpack_require进行设置,这里暂时用不到,且先不细看(dev模式生成的代码中其实注释也挺明白)。
着重看看webpack_require里面发生了什么:
// The require functionfunction __webpack_require__(moduleId) {// Check if module is in cache// 检查moduleId是不是已经被缓存// 如果缓存了返回缓存if(installedModules[moduleId]) {return installedModules[moduleId].exports;}// Create a new module (and put it into the cache)var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}};// Execute the module functionmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// Flag the module as loadedmodule.l = true;// Return the exports of the modulereturn module.exports;}
- 首先,入参moudleId就是传进来的’./src/index.js’,入口文件路径。
- 检查模块缓存中是不是已经有了此模块,如果有,直接返回moudle.exports属性。如果没有,构造一个moudle对象:
{i: moduleId, // idl: false, // 被加载了吗?exports: {} // 输出}
这个对象作为moudleId的key对应的value放入缓存中,所以这个缓存就是个Object形式的Map。
- 调用moudles中对应moudleId的方法:
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
这个”modules.moduleId”是谁呢?就是IIFE中被作为参数传进来的object,对应的key为moudleId的值,是一个函数:
// IIFE的参数 { "./src/index.js": ... }function (module, exports) {eval("const hello='hello~';\n\n\nconsole.log(hello);\n\n\n//#sourceURL=webpack:///./src/index.js?");}
所以上面函数中参数,在webpack_require中调用的时候分别对应:module对应module,exports对应module.exports。目前还看不出有什么作用,所以暂且不表。
- 调用完成后(调用中函数中eval自然是被执行过了)设置loaded标记为true,表示已经加载了。最终返回了moudle.exports;
整个流程其实很清楚。到目前为止,遗留的问题是:module, moudle.exports怎么用?这个问题后面分析多文件会弄明白。
另外,值得一提的是webpack v5在build单文件的时候可不像v4这么啰嗦
webpack v5
时代在变化。
和上述v4中用的相同的src代码,可以看到,v5版本的简单明了。直接一个IIFE完事:
// v5(() => {eval("const hello = 'hello~';\n\nconsole.log(hello);\n\n//# sourceURL=webpack://y/./src/index.js?");})()
(简直就是: 开局一个IIFE,一刀99级)
多文件
把src改动:增加一个util.js
const sayHello = (to = 'world') => {console.log(`hello ${to} ~`)}export { sayHello };
index.js中使用sayHello:
import { sayHello } from './util'console.log(sayHello());
webpack v4
先看作为IIFE的参数传递那个对象modules:
{"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {"use strict";eval("一堆东西...略");}),"./src/util.js": (function (module, __webpack_exports__, __webpack_require__) {"use strict";eval("一堆东西...略");})}
这个对象可现在有两个key,分别是”./src/index.js”、”./src/util.js”。key对应的值还是函数,函数的参数是和之前单文件稍有不同,另外还是eval中一堆代码。
不过因为设计多文件了,而代码中有多文件的调用关系,所以eval还是得看看的:
index.js中对应的eval:
eval(`__webpack_require__.r(__webpack_exports__);var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(\"./src/util.js\");console.log(Object(_util__WEBPACK_IMPORTED_MODULE_0__[\"sayHello\"])());`);
util.js中对应的eval:
eval(`__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__, \"sayHello\", function() { return sayHello; });const sayHello = (to = 'world') => {console.log(`hello ${to} ~`)}`);
在这些被格式化的eval代码中,都用了webpack_require.r, .d这些方法,看来要进一步理解,还得继续看看这些函数了。另外,这里的webpack_exports记着是这样调用传下来的module.exports,是一个对象:
modules[moduleId].call(module.exports, module, module.exports, webpack_require); 说白了 webpack_require.r, .d这些方法其实就是在给这个对象设置属性。
看到在webpack_require.r 的注释中说道这个函数是在 define __esModule on exports:
// define __esModule on exports__webpack_require__.r = function (exports) {if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});}Object.defineProperty(exports, '__esModule', {value: true});};
上文中写道,最终其实传进来的是模块moudle对象的moudle.exports,这个moudle.exports就是这里的exports,那么这个函数中,对exports进行了属性设置__esModule设成了true。由此可见,这个方法应该不那么重要。
接着看看webpack_require.d:
// define getter function for harmony exports__webpack_require__.d = function (exports, name, getter) {if (!__webpack_require__.o(exports, name)) {Object.defineProperty(exports, name, {enumerable: true,get: getter});}};
.d中也defineProperty了,对象也是exports,定义的是什么呢?原来是对exports定义了一个name属性,属性的get是走getter函数,结合使用看看具体的,比如util.js对应的eval中:
__webpack_require__.d(__webpack_exports__, 'sayHello', function() { return sayHello; })const sayHello = (to = 'world') => {console.log(`hello ${to} ~`)}
这样的话.d函数就相当于在exports对象上面定义了一个属性名sayHello, 当你去读取sayHello的时候,实际执行的是function() { return sayHello; },当然它会返回sayHello这个变量的值。
到此为止,似乎有点清晰了呢~
要彻底拨云见日,还得整个流程梳理下:
- build后,整体是个IIFE。
- IIFE的参数是模块ID对应模块内容,模块ID就是文件路径,内容核心是一个函数,函数中会调用eval代码,函数的传参moudle、webpack_exports和webpack_require。
- IIFE的return中将入口index.js作为webpack_require的参数传递了进去。
- webpack_require执行了moudles中key为./src/index.js的代码,其中eval在执行的过程中遇到了源码是import的地方,对应翻译为webpack_require(“./src/util.js”)
- 继续看webpack_require(“./src/util.js”)的执行:
- 在执行的之后返回的就是”./src/util.js”对应的moudle的exports(这个moudle我们称之为当前模块);
- 执行中遇到源代码为exports的地方,实际调用了webpack_require.d,在这个函数中对当前模块的exports属性进行了设置,这样保证了上面提到的return出去的东西能从exports属性上面拿到导出的结果。
- 再回到./src/index.js对应的eval,调用webpack_require(“./src/util.js”)之后拿到的就是”./src/util.js”模块对应的exports,那么根据代码逻辑,后续使用就好。
- 入口文件执行完,返回入口文件的moudle.exports。
整个过程就是这样,就是从入口文件进去,遇到依赖模块了之后通过webpack_require去取依赖,此处涉及了缓存的逻辑(加载过的自然是不用加载了,直接拿就好了),依赖模块在第一次加载的过程中还定义了模块。
如此一个深度的依赖遍历,一层层深入,再一层层出来的过程,完成了整个程序的调用过程。
另外,在整个过程中,可以看到,webpack在自己实现Common js,我们的代码是用esMoudle写的,但是真正打包的import,export过程走的是commonjs类似,或者说,因为代码是在浏览器中执行,webpack用自己的逻辑webpack_require、webpack_require.d、installedModules等实现了一套在浏览器端可运行的模块化方案。
webpack v5
在v5中,逻辑没有变化,就是上文中分析那样,但是生成的代码看上去清爽了很多:
(() => { // webpackBootstrap"use strict";// 所有模块var __webpack_modules__ = ({"./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {eval("... ...略");}),"./src/util.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {eval("... ...略");})});// The module cachevar __webpack_module_cache__ = {};// The require functionfunction __webpack_require__(moduleId) {// Check if module is in cacheif (__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 neededexports: {}};// Execute the module function__webpack_modules__[moduleId](module, module.exports, __webpack_require__);// Return the exports of the modulereturn module.exports;}/* webpack/runtime/define property getters */(() => {// define getter functions for harmony exports__webpack_require__.d = (exports, definition) => {};})();/* 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) => {};})();// startup// Load entry module__webpack_require__("./src/index.js");// This entry module used 'exports' so it can't be inlined})();
可以看到打包代码主体还是IIFE,但是这次所有模块并不是通过IIFE的参数传递进去,而是直接定义了个变量在IIFE内部叫webpack_modules,这点差不多就是最大的区别了,别的没有什么。
异步import
我们用这样的语法实现异步加载:
import('url').then(data => {});
那么webpack自然就帮我们实现了支持这样语法的逻辑,现在把src中的代码修改如下:
// index.jsimport ('./async').then(data => {console.log('async import:', data)})// async.jsexport const aData = 'i am async data';
webpack v4
在v4版本中,我们发现凡事有异步加载的文件都会拆分出来,而且命名诡异,我们得到了两个文件:main.js 和 0.js。
整体看下结构,main.js:
(function (modules) { // webpackBootstrap// install a JSONP callback for chunk loadingfunction webpackJsonpCallback(data) {// ... ...};// The module cachevar installedModules = {};// object to store loaded and loading chunksvar installedChunks = {"main": 0};// script path functionfunction jsonpScriptSrc(chunkId) {// ... ...}// The require functionfunction __webpack_require__(moduleId) {// ... ...return module.exports;}// This file contains only the entry chunk.// The chunk loading function for additional chunks__webpack_require__.e = function requireEnsure(chunkId) {// ... ...return Promise.all(promises);};// expose the modules object (__webpack_modules__)__webpack_require__.m = modules;// expose the module cache__webpack_require__.c = installedModules;// define getter function for harmony exports__webpack_require__.d = function (exports, name, getter) {// ... ...};// define __esModule on exports__webpack_require__.r = function (exports) {// ... ...};// 省略了点没怎么看的// ... ...var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);jsonpArray.push = webpackJsonpCallback;jsonpArray = jsonpArray.slice();for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);var parentJsonpFunction = oldJsonpFunction;// Load entry module and return exportsreturn __webpack_require__(__webpack_require__.s = "./src/index.js");})({"./src/index.js":(function (module, exports, __webpack_require__) {eval(`__webpack_require__.e(0).then(__webpack_require__.bind(null, \"./src/async_data.js\")).then(data => {console.log('async import:', data)})//# sourceURL=webpack:///./src/index.js?`);})});
光看结构就复杂了不少,但是本质还是IIFE,这个没变,但是看到我们调用IIFE的参数中,这个对象只有一个属性,就是入口文件’./src/index.js’和它对应的代码。
这部分流程和之前是一样的也就是在webpack_reqire中调用:
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
从而先执行入口文件的代码,也就是eval部分:
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/async_data.js")).then(data => {console.log('async import:', data)})
这时候需要去看webpack_require.e的代码了:
// This file contains only the entry chunk.// The chunk loading function for additional chunks__webpack_require__.e = function requireEnsure(chunkId) {var promises = [];// JSONP chunk loading for javascriptvar installedChunkData = installedChunks[chunkId];if (installedChunkData !== 0) { // 0 means "already installed".// a Promise means "currently loading".if (installedChunkData) {promises.push(installedChunkData[2]);} else {// setup Promise in chunk cachevar promise = new Promise(function (resolve, reject) {installedChunkData = installedChunks[chunkId] = [resolve, reject];});promises.push(installedChunkData[2] = promise);// start chunk loading// 加载chunk用的是script标签// 1. 构造标签设置各种属性var script = document.createElement('script');var onScriptComplete;script.charset = 'utf-8';script.timeout = 120;if (__webpack_require__.nc) {script.setAttribute("nonce", __webpack_require__.nc);}script.src = jsonpScriptSrc(chunkId);// create error before stack unwound to get useful stacktrace latervar error = new Error();// 3. 完成回调onScriptComplete = function (event) {// avoid mem leaks in IE.script.onerror = script.onload = null;clearTimeout(timeout);var chunk = installedChunks[chunkId];if (chunk !== 0) {if (chunk) {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;chunk[1](error);}installedChunks[chunkId] = undefined;}};var timeout = setTimeout(function () {onScriptComplete({type: 'timeout',target: script});}, 120000);script.onerror = script.onload = onScriptComplete;// 2. 请求document.head.appendChild(script);}}return Promise.all(promises);};
这是一段很长的代码,但是简单的理解下,就是根据chuckId,也就是0去利用script标签,到这个地方script.src = jsonpScriptSrc(chunkId);去请求脚本;那么可以想象当document.head.appendChild(script);请求下来的时候执行的就是上面的完成回调onScriptComplete,同时写在脚本中的js就会被执行。
所以是时候看看这个script.src是什么了,其实就是0.js:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], {"./src/async_data.js":/*! exports provided: data */(function (module, __webpack_exports__, __webpack_require__) {"use strict";eval(`__webpack_require__.r(__webpack_exports__);/* harmony export (binding) */__webpack_require__.d(__webpack_exports__,\"data\",function() { return data; });const data = 'async get data';//# sourceURL=webpack:///./src/async_data.js?`);})}]);
可以看到,当这个script加载完成,执行的就是这个:
window["webpackJsonp"].push([[0],{// ... ...}]);
看上去是向window.webpackJsonp里面push了个二维数组,那么window.webpackJsonp是个数组吗?这里调用的是数组的push方法??
其实不是的,这个push并不是array.prototype.push,这个push是main中修改过的:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);// here ----->jsonpArray.push = webpackJsonpCallback;jsonpArray = jsonpArray.slice();for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);var parentJsonpFunction = oldJsonpFunction;
这里看到jsonpArray.push = webpackJsonpCallback,也就是说jsonpArray.push是webpackJsonpCallback,那么这个是谁,就是它window.webpackJsonp,所以上面window[“webpackJsonp”].push(二维数组),这个push就是在调用webpackJsonpCallback。所以看看webpackJsonpCallback:
function webpackJsonpCallback(data) {// data就是那个二维数组var chunkIds = data[0];var moreModules = data[1];// add "moreModules" to the modules object,// then flag all "chunkIds" as loaded and fire callbackvar moduleId, chunkId, i = 0,resolves = [];for (; i < chunkIds.length; i++) {chunkId = chunkIds[i];if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {// installedChunks之前被赋值为// 在 __webpack_reqire__.e 中// installedChunkData = installedChunks[chunkId] = [resolve, reject];resolves.push(installedChunks[chunkId][0]);}installedChunks[chunkId] = 0;}for (moduleId in moreModules) { // mouduleID不是0,而是./src/async_data.jsif (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {// 这句话最重要// moudles['./src/async_data.js'] 被赋值了modules[moduleId] = moreModules[moduleId];}}if (parentJsonpFunction) parentJsonpFunction(data);// 调用resolevewhile (resolves.length) {resolves.shift()();}};
这段代码想做的事情就是把data,这个二维数组中的data[1],这个东西扩展到modules里面去:
{"./src/async_data.js":(function (module, __webpack_exports__, __webpack_require__) {eval(/*.......*/)})}
这段代码想做的正是把这个东西扩展到modules里面去。即这样 modules[moduleId] = moreModules[moduleId]; 等到都扩展完了调用下resolves,这个resovle是webpack_reqire.e里面设置installedChunks而来的installedChunks[chunkId] = [resolve, reject];
resolev了之后resovle是webpack_reqire.ereturn的Promise.all就到结束了,下面就又回到这个流程了:
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/async_data.js")).then(data => {console.log('async import:', data)})
这会我们已经把modules模块扩展上了我们新载入的的路径key和代码value,所以,这一步就和同步的流程一样了webpack_require.bind(null, “./src/async_data.js”),相当于webpack_require(“./src/async_data.js”)。
正常加载就好~
到这里v4的异步import算简单的过了下,总结下思路,就是当遇到异步加载的代码,先使用script标签的方式,拿到异步文件最外层的代码,在执行拿到的文件时,执行window.webpackJsonpCallback。这个函数将异步代码作为moudles的新属性扩展,这样webpack_require.e的promise.all都ok后,就可以按照正常的webpack_require加载模块了。
v5的简单对比
学习了v4的流程后,看v5生成的文件将会简单很多,整体思路其实是一致的,没有太多变化。
使用webppack5异步加载的chunk最终生成的文件不再是以0,1这样命名的了,默认是代码文件路径作为文件名,当然具体什么规则是能配置的,这样有利于我们做缓存优化。
不同的地方有:
- 不像v4把模块作为IIFE的参数传入,v5直接在函数内部;
- 异步chunk的加载对象不再是window.webpackJsonp,v5变成了self.webpackChunky; (self虽然和window都一样,实际上和globalThis也是一样的,但是self不见得比window好,而且也能被重写,本质上也没有解决啥问题)
