本文基于webpack5,分析打包后的文件,参考了网上的资料,同时加上了个人的理解,欢迎讨论。
代码已经关联到github: 链接地址 觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:
简单打包
文件介绍
//index.js 入口
import { cc } from './Test'
cc()
//Test.js
function cc(){}
export {cc}
运行时分析
webpack执行打包后生成的js文件是一个立即执行函数,其参数modules为一个对象{},包含我们所有要打包的文件。
这个立即执行函数主要分为以下部分:
- webpack_modules:保存webpack已经注册的模块,一个键值对的对象
- webpack_require:对应import(es6),实现模块加载和缓存,模块管理核心
- webpack_module_cache:模块缓存
- webpack_require.o:工具函数,判断是否有某属性
- webpack_require.d:对应export,用来定义导出变量对象
- webpack_require.r:区分是否es模块,给导出导出变量对象添加__esModule:true属性,用来兼容es和commonJS等模块的
模块加载执行流程:
- 模块使用
__webpack_require__
加载模块,接受的参数是moduleId
(文件路径),返回的是模块的exports
- 首先会判断是否存在缓存,存在则返回模块的
exports
,不存在则创建一个模块并缓存 - 接着按照文件路径执行
__webpack_modules__
中模块函数 - 执行完毕后返回模块的
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)
}
运行时分析
异步打包的模块,多出了以下方法
- installedChunks:缓存异步加载的模块键值对集合,内部存的模块有未加载,加载中,加载完毕几种形式。
- webpack_require.e:加载异步模块的入口方法,使用
__webpack_require__.f.j
方法加载模块。 - webpack_require.f:异步加载方法集,方便后续添加其他异步加载方法。
- webpack_require.f.j :异步加载js的方法,主要是将js异步的Promise设置到
installedChunks
中(webpackJsonpCallback会调用),还处理其他拦截相同文件加载、加载文件的url、加载异常回调的等,最后会调用__webpack_require__.l
加载js文件。 - webpack_require.l::动态创建script标签去加载js文件。
- webpackJsonpCallback:异步js文件内部执行方法,主要是 1.将
installedChunks
中的对应模块的Promise fullfill掉,执行引入异步加载import().then的代码 2.将异步模块存到__webpack_modules__
这个全局注册模块集合中。 - webpack_require.p:获得公共路径
- 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()//执行异步函数
}
);
- 异步加载时,从入口文件出发,发现有异步加载的js则调用
__webpack_require__.e
->__webpack_require__.f.j
->__webpack_require__.l
, 使用动态创建script标签的形式 下载模块文件,下载完毕后最终__webpack_require__.e
的会返回一个的promise的实例(Promise.all) - 下载完毕的js会自动执行
self["webpackChunkwebpack_step1"].push
方法,该方法已经在入口文件加载时重置成为了webpackJsonpCallback
函数,用来触发引入该异步模块的回调(将第一步的Promise.all状态修改成fullfilled)和缓存该异步模块。 - 如果在我们的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));