源码分析(十二):打包后文件解析
以 前言及总流程概览
里的 demo
为例, 前十一张章分析了打包过程,现在来分析它打包后的文件。
demo
//src/a.js
import { add } from 'Src/b';
import('./c.js').then((m) => m.sub(2, 1));
const a = 1;
add(3, 2 + a);
//src/b.js
import { mul } from '@fe_korey/test-loader?number=20!Src/e';
export function add(a, b) {
return a + b + mul(10, 5);
}
export function addddd(a, b) {
return a + b * b;
}
//src/c.js
import { mul } from 'Src/d';
import('./b.js').then((m) => m.add(200, 100)); //require.ensure() 是 webpack 特有的,已经被 import() 取代。
export function sub(a, b) {
return a - b + mul(100, 50);
}
//src/d.js
export function mul(a, b) {
const d = 10000;
return a * b + d;
}
//webpack.config.js
var path = require('path');
module.exports = {
entry: {
bundle: './src/a.js',
},
devtool: 'none',
output: {
path: __dirname + '/dist',
filename: '[name].[chunkhash:4].js',
chunkFilename: '[name].[chunkhash:8].js',
},
mode: 'development',
resolve: {
alias: {
Src: path.resolve(__dirname, 'src/'),
},
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
},
],
},
],
},
};
//babel.config.js
module.exports = {
presets: ['@babel/env'],
};
@fe_korey/test-loader
是一个测试 loader
,该 loader
作用为代码里的字符串 10000
替换为传入的 number
。
打包结果文件
根据项目配置及同步异步的关系,打包后一共生成两个文件:
bundle.xxxx.js
总代码:见 github
入口文件,该文件名根据配置:entry
及 output.filename
生成,里面包含 webpack runtime
代码和同步模块代码。
如若配置了 html-webpack-plugin
,那么在生成的 html
里将只会引入此 js
文件。
0.xxxxxxxx.js
总代码:见 github
非入口文件,本例为异步 chunk
文件,该文件名根据配置: output.chunkFilename
生成,里面包含异步模块代码。
代码执行流程
根据代码执行顺序来分析,html
文件只需引入了 bundle.xxxx.js
文件,则从该文件开始执行,如果有其他 import
后,会先跳到对应的 module
进行处理,即先序深度优先遍历算法递归该依赖树。
bundle 主体结构
(function (modules) {
//runtime代码
})({
'./node_modules/@fe_korey/test-loader/loader.js?number=20!./src/d.js': function (module, __webpack_exports__, __webpack_require__) {
//...模块代码d
},
'./src/a.js': function (module, __webpack_exports__, __webpack_require__) {
//...模块代码a
},
'./src/b.js': function (module, __webpack_exports__, __webpack_require__) {
//...模块代码b
},
});
主体结构为一个自执行函数,函数体为 runtime
函数,参数为 modules
对象,各模块以 key-value
的形式一起存在该 modules
对象里。当前 key
为模块的路径,value
为包裹模块代码的一个函数。
runtime 函数
runtime
指的是 webpack
的运行环境(具体作用就是模块解析, 加载) 和 模块信息清单(表现在 jsonpScriptSrc
方法里)。
配置项 optimization.runtimeChunk
可以设置 webpack
将 runtime
这部分代码单独打包。
runtime 函数主体结构
function(modules){
function webpackJsonpCallback(data){
//...
}
// 设置 script src __webpack_require__.p 即为 output.publicPath 配置
function jsonpScriptSrc(chunkId){
return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + "." + {"0":"d680ffbe"}[chunkId] + ".js"
}
function __webpack_require__(moduleId){
//...
}
var installedModules = {};
var installedChunks = {"bundle": 0};
// 定义一堆挂载在__webpack_require__上的属性
//...
// jsonp 初始化
// ...
return __webpack_require__(__webpack_require__.s = "./src/a.js");
}
开始执行
代码开始执行:
var installedModules = {};
初始化 installedModules
,保存所有创建过的 module
,用于缓存判断。
// undefined:chunk未加载, null: chunk通过prefetch/preload提前获取过
// Promise:chunk正在加载, 0:chunk加载完毕
// 数组: 结构为 [resolve Function, reject Function, Promise] 的数组, 代表 chunk 在处于加载中
var installedChunks = {
bundle: 0,
};
installedChunks
以 key-value
的形式,用于收集保存所有的 chunk
,这里 bundle
就是指的当前 chunk
,自然是已经加载好了的。
__webpack_require__
属性
然后定义了一堆 __webpack_require__
的属性:
// 异步处理
__webpack_require__.e = function requireEnsure(chunkId) {
//后文单独分析
};
// 即为传入的modules:各模块组成的对象
__webpack_require__.m = modules;
// 即为installedModules:已经缓存的对象
__webpack_require__.c = installedModules;
// 在exports对象上添加属性,即 增加导出
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 在exports对象上添加 __esModule 属性,用于标识 es6 模块
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 创建一个伪命名空间对象
__webpack_require__.t = function (value, mode) {
//没用上,解释暂时略过
};
// 得到 getDefaultExport,即通过 __esModule 属性判断是否是 es6 来确定对应的默认导出方法
__webpack_require__.n = function (module) {
var getter =
module && module.__esModule
? function getDefault() {
return module['default'];
}
: function getModuleExports() {
return module;
};
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// 调用 hasOwnProperty,即判断对象上是否有某一属性
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// 即为 publicPath,在output.publicPath配置而来
__webpack_require__.p = '';
// 错误处理
__webpack_require__.oe = function (err) {
console.error(err);
throw err;
};
每个属性的作用已经写在注释上面。
jsonp
初始化
然后执行 jsonp
初始化:
var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []); //初始化 window['webpackJsonp']对象
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 暂存 push 方法
jsonpArray.push = webpackJsonpCallback; //重写 jsonpArray 的 push 方法为 webpackJsonpCallback
jsonpArray = jsonpArray.slice(); //拷贝 jsonpArray(不带 push 方法)
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); //若入口文件加载前,chunks文件先加载了,遍历 jsonpArray 用 webpackJsonpCallback 执行
var parentJsonpFunction = oldJsonpFunction; //旧的 push 方法存入 parentJsonpFunction
jsonp
初始化的主要作用就是给 window['webpackJsonp']
重写了 push
方法为 webpackJsonpCallback
。接着执行:
return __webpack_require__((__webpack_require__.s = './src/a.js'));
由入口文件 a
开始,传入 moduleID : "./src/a.js"
,执行方法 __webpack_require__
。
__webpack_require__
function __webpack_require__(moduleId) {
// 判断该module是否已经被缓存到installedModules,如果有,则直接返回它的导出exports
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 定义module并缓存
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
});
// 执行module代码
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标志module已经读取完成
module.l = true;
return module.exports;
}
__webpack_require__
方法的主要作用就是创建缓存 module
后,执行该 module
的代码。其中 modules
即为上文所解释的各模块组成的对象。
执行各同步模块代码
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
执行模块 a
的代码。
// 模块 a
'use strict';
__webpack_require__.r(__webpack_exports__);
var Src_b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./src/b.js');
__webpack_require__
.e(0)
.then(__webpack_require__.bind(null, './src/c.js'))
.then(function (m) {
return m.sub(2, 1);
});
var a = 1;
Object(Src_b__WEBPACK_IMPORTED_MODULE_0__['add'])(3, 2 + a);
代码里 __webpack_require__('./src/b.js')
会去执行模块 b
的代码:
//模块 b
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'add', function () {
return add;
});
__webpack_require__.d(__webpack_exports__, 'addddd', function () {
return addddd;
});
var _fe_korey_test_loader_number_20_Src_d__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./node_modules/@fe_korey/test-loader/loader.js?number=20!./src/d.js');
function add(a, b) {
return a + b + Object(_fe_korey_test_loader_number_20_Src_d__WEBPACK_IMPORTED_MODULE_0__['mul'])(10, 5);
}
function addddd(a, b) {
return a + b * b;
}
代码里在导出了两个方法后,去执行模块 d
的代码:
//模块 d
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'mul', function () {
return mul;
});
function mul(a, b) {
var d = 20;
return a * b + d;
}
模块 d
代码导出了 mul
。
import() 的处理
各自模块执行完后,回到模块 a
里执行:
__webpack_require__
.e(0)
.then(__webpack_require__.bind(null, './src/c.js'))
.then(function (m) {
return m.sub(2, 1);
});
该打包后的代码为异步动态加载,源代码为:
import('./c.js').then((m) => m.sub(2, 1));
__webpack_require__.e
__webpack_require__.e
实现异步加载模块,方法为:
// promise队列,等待多个异步 chunk都加载完成才执行回调
var promises = [];
// 先判断是否加载过该 chunk
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
// 0 means "already installed".
// a Promise means "currently loading". 目标 chunk 正在加载,则将 promise push到 promises 数组
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 新建一个Promise去异步加载目标chunk
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject]; //这里设置 installedChunks[chunkId]
});
promises.push((installedChunkData[2] = promise)); // installedChunks[chunkId] = [resolve, reject, promise]
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute('nonce', __webpack_require__.nc);
}
// 设置src
script.src = jsonpScriptSrc(chunkId);
var error = new Error();
// 设置加载完成或错误的回调
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标签的onload事件都是在外部js文件被加载完成并执行完成后(异步不算)才被触发
script.onerror = script.onload = onScriptComplete;
// script标签加入文档
document.head.appendChild(script);
}
}
return Promise.all(promises);
参数 0
为 chunkId
,在方法 __webpack_require__.e
里,主要功能就是模拟 jsonp
去异步加载目标 chunk
文件 0
,返回一个 promise
对象。
然后加载异步文件 0.e3296d88.js
并执行。
加载非入口文件0.e3296d88.js
非入口文件主体结构
(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
[0],
{
'./src/c.js': function (module, __webpack_exports__, __webpack_require__) {
//模块 c
},
'./src/d.js': function (module, __webpack_exports__, __webpack_require__) {
//模块 d
},
},
]);
在模块加载后,就会立即执行的 window['webpackJsonp'].push()
。由 jsonp
初始化可知, 即执行 bundle
文件里的 webpackJsonpCallback
方法。
webpackJsonpCallback
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1]; //异步 chunk 的各模块组成的对象
var moduleId,
chunkId,
i = 0,
resolves = [];
// 这里收集 resolve 并将所有 chunkIds 标记为已加载
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]); //将 resolve push 到 resolves 数组中
}
installedChunks[chunkId] = 0; //标记为已加载
}
// 遍历各模块组成的对象,将每个模块都加到 modules
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// 执行保存的旧 push 方法,可能是 array.push (即 push 到 window.webpackJsonp),也可能是前一个并行执行了 runtime 的 bundle 的 webpackJsonpCallback,即递归执行 webpackJsonpCallback,如多入口同时 import 同一个 module 的情况。
if (parentJsonpFunction) parentJsonpFunction(data);
//循环触发 resolve 回调
while (resolves.length) {
resolves.shift()();
}
}
webpackJsonpCallback
方法主要将异步的 chunk
里的所有模块都加到 modules
后,改变 installedChunks[chunkId]
的状态为 0
(即已加载),然后执行之前创建的 promise
的 resolve()
。
执行 resolve 的回调 then 方法
回到模块 a
根据 promise
的定义,执行 promises
队列里所有的 resolve
后,然后去执行对应的 then
方法:
//...
then(__webpack_require__.bind(null, './src/c.js'));
//...
即执行模块 c
:
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'sub', function () {
return sub;
});
var Src_d__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./src/d.js');
Promise.resolve()
.then(__webpack_require__.bind(null, './src/b.js'))
.then(function (m) {
return m.add(200, 100);
});
function sub(a, b) {
return a - b + Object(Src_d__WEBPACK_IMPORTED_MODULE_0__['mul'])(100, 50);
}
console.log('c');
模块 c
里引入了模块 d
,这里的模块 d
与前文的模块 d
虽然是一样的,但由于用的 loader
不一样,所以会认为是两个不同的模块,故会再次加载,互不影响。这里模块 d
不在累述。
然后执行:
Promise.resolve()
.then(__webpack_require__.bind(null, './src/b.js'))
.then(function (m) {
return m.add(200, 100);
});
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的 Promise
对象。即执行 then 方法,即 __webpack_require__.bind(null, './src/b.js')
。然后在 __webpack_require__
方法里判断缓存有模块 b
,则直接返回模块 b
对应的 exports
。到此异步加载完成。
根据微任务队列的先后顺序,先执行模块 a
的第二个 then
回调,然后执行模块 c
的第二个 then
回调,都执行完成后,执行加载完成回调 onScriptComplete
。到此代码运行完成。
异步加载小结
再次梳理下异步加载的关键思路:
- 通过
__webpack_require__
加载运行入口module
- 模块代码里遇到
import()
即执行__webpack_require__.e
加载异步chunk
__webpack_require__.e
使用模拟jsonp
的方式及创建script
标签来加载异步chunk
,并为每个chunk
创建一个promise
- 等到异步
chunk
被加载后,会执行window['webpackJsonp'].push
,即webpackJsonpCallback
方法 webpackJsonpCallback
里将异步chunk
里的module
加入到modules
, 并触发前面创建promise
的resolve
回调,然后执行其then
方法即__webpack_require__
去加载新的module
。
扩展 使用 splitChunks 切割后的文件解析
demo
// a.js
import { mul } from './d';
// b.js
import { mul } from './d';
// d.js为普通同步文件
//webpack.config.js
{
"entry": {
"bundle1": "./src/e.js",
"bundle2": "./src/f.js"
},
"plugins": [new HtmlWebpackPlugin()],
//...
"optimization": {
"splitChunks": {
"chunks": "all",
"minSize": 0, //当模块小于这个值时,就不拆
"maxSize": 0, //当模块大于这个值时,尝试拆分
"minChunks": 1, //重复一次就打包
"name": true, //是否以cacheGroups中的filename作为文件名
"automaticNameDelimiter": "~", //打包的chunk名字连接符
"cacheGroups": {
"default": {
"chunks": "all",
"minChunks": 2,
"priority": -10
}
}
}
}
}
引入插件 HtmlWebpackPlugin
辅助分析。
打包后代码见 github,以下只做关键点记录:
html
会引入每个入口bundle
生成的js
和公共部分的js
:
<script type="text/javascript" src="default~bundle1~bundle2.183bf5f4.js"></script><script type="text/javascript" src="bundle1.10ad.js"></script><script type="text/javascript" src="bundle2.c333.js"></script></body>
- 在
jsonp
初始化阶段,执行:
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
和在 webpackJsonpCallback
方法里执行:
if (parentJsonpFunction) parentJsonpFunction(data);
可以保证无论页面先加载入口文件还是非入口文件,都可以将依赖 module
同步到各自的 chunk
里。
- 两个入口文件
bundle1.xxxx.js bundle2.xxxx.js
的runtime
代码里会多出一个新的变量deferredModules
:
var deferredModules = [];
//...
deferredModules.push(['./src/e.js', 'default~bundle1~bundle2']);
return checkDeferredModules();
该变量为一个数组,第一个变量是需要加载的 module
,后面的变量就是要加载本 module
所需的其他依赖 module
。然后在 runtime
的末尾执行:return checkDeferredModules();
checkDeferredModules
function checkDeferredModules() {
var result;
for (var i = 0; i < deferredModules.length; i++) {
var deferredModule = deferredModules[i];
var fulfilled = true;
for (var j = 1; j < deferredModule.length; j++) {
var depId = deferredModule[j];
if (installedChunks[depId] !== 0) fulfilled = false; //判断依赖模块有没有加载过
if (fulfilled) {
deferredModules.splice(i--, 1);
result = __webpack_require__((__webpack_require__.s = deferredModule[0])); //如果所有依赖模块都加载了(即modules里有依赖模块),则就可以读取目标的module了
}
}
return result;
}
该方法主要检查依赖的 module
是否加载过,若都加载了则加载目标 module
。
webpackJsonpCallback
格外代码
//...
deferredModules.push.apply(deferredModules, executeModules || []);
return checkDeferredModules();
该方法增加了这两句代码,用于在调用 webpackJsonpCallback
时(即 window["webpackJsonp"].push
或 webpackJsonpCallback(jsonpArray[i])
),有其他依赖的时候可以再去调用 checkDeferredModules
进行依赖检查。
splitChunks 切割后加载小结
梳理下 splitChunks
切割后的关键思路:
- 根据
script
标签先后顺序,html
先加载公共依赖default~bundle1~bundle2.xx.js
,即在window["webpackJsonp"]
里push
了该module
。 html
加载bundle1.js
,在jsonp
初始化里调用webpackJsonpCallback(jsonpArray[i])
将公共依赖模块加到modules
里并改变其状态为已加载后,调用checkDeferredModules()
,但deferredModules
为空,所以没有任何操作。- 然后回到
runtime
里继续执行,将当前module
和依赖module
push
到deferredModules
里,再次调用checkDeferredModules
,此时判断各依赖模块状态均为已加载后,加载当前module
。 html
加载bundle2
文件,此后逻辑跟bundle1
一致。