以 前言及总流程概览
里的 demo
为例, 前十一张章分析了打包过程,现在来分析它打包后的文件。
demo 1 2 3 4 5 import { add } from 'Src/b' ;import ('./c.js' ).then((m ) => m.sub(2 , 1 ));const a = 1 ;add(3 , 2 + a);
1 2 3 4 5 6 7 8 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; }
1 2 3 4 5 6 import { mul } from 'Src/d' ;import ('./b.js' ).then((m ) => m.add(200 , 100 )); export function sub (a, b ) { return a - b + mul(100 , 50 ); }
1 2 3 4 5 export function mul (a, b ) { const d = 10000 ; return a * b + d; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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' , }, ], }, ], }, };
1 2 3 4 module .exports = { presets: ['@babel/env' ], };
@fe_korey/test-loader
是一个测试 loader
,该 loader
作用为代码里的字符串 10000
替换为传入的 number
。
打包结果文件 根据项目配置及同步异步的关系,打包后一共生成两个文件:
总代码:见 github
入口文件,该文件名根据配置:entry
及 output.filename
生成,里面包含 webpack runtime
代码和同步模块代码。
如若配置了 html-webpack-plugin
,那么在生成的 html
里将只会引入此 js
文件。
总代码:见 github
非入口文件,本例为异步 chunk
文件,该文件名根据配置: output.chunkFilename
生成,里面包含异步模块代码。
代码执行流程 根据代码执行顺序来分析,html
文件只需引入了 bundle.xxxx.js
文件,则从该文件开始执行,如果有其他 import
后,会先跳到对应的 module
进行处理,即先序深度优先 遍历算法递归该依赖树。
bundle 主体结构 1 2 3 4 5 6 7 8 9 10 11 12 13 (function (modules ) { })({ './node_modules/@fe_korey/test-loader/loader.js?number=20!./src/d.js' : function (module , __webpack_exports__, __webpack_require__ ) { }, './src/a.js' : function (module , __webpack_exports__, __webpack_require__ ) { }, './src/b.js' : function (module , __webpack_exports__, __webpack_require__ ) { }, });
主体结构为一个自执行函数,函数体为 runtime
函数,参数为 modules
对象,各模块以 key-value
的形式一起存在该 modules
对象里。当前 key
为模块的路径,value
为包裹模块代码的一个函数。
runtime 函数 runtime
指的是 webpack
的运行环境(具体作用就是模块解析, 加载) 和 模块信息清单(表现在 jsonpScriptSrc
方法里)。
配置项 optimization.runtimeChunk
可以设置 webpack
将 runtime
这部分代码单独打包。
runtime 函数主体结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function (modules ) { function webpackJsonpCallback (data ) { } function jsonpScriptSrc (chunkId ) { return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + "." + {"0" :"d680ffbe" }[chunkId] + ".js" } function __webpack_require__ (moduleId ) { } var installedModules = {}; var installedChunks = {"bundle" : 0 }; return __webpack_require__(__webpack_require__.s = "./src/a.js" ); }
开始执行 代码开始执行:
1 var installedModules = {};
初始化 installedModules
,保存所有创建过的 module
,用于缓存判断。
1 2 3 4 5 6 var installedChunks = { bundle: 0 , };
installedChunks
以 key-value
的形式,用于收集保存所有的 chunk
,这里 bundle
就是指的当前 chunk
,自然是已经加载好了的。
__webpack_require__
属性然后定义了一堆 __webpack_require__
的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 __webpack_require__.e = function requireEnsure (chunkId ) { }; __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.d = function (exports , name, getter ) { if (!__webpack_require__.o(exports , name)) { Object .defineProperty(exports , name, { enumerable : true , get : getter }); } }; __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 ) { }; __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; }; __webpack_require__.o = function (object, property ) { return Object .prototype.hasOwnProperty.call(object, property); }; __webpack_require__.p = '' ; __webpack_require__.oe = function (err ) { console .error(err); throw err; };
每个属性的作用已经写在注释上面。
jsonp
初始化然后执行 jsonp
初始化:
1 2 3 4 5 6 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;
jsonp
初始化的主要作用就是给 window['webpackJsonp']
重写了 push
方法为 webpackJsonpCallback
。接着执行:
1 return __webpack_require__((__webpack_require__.s = './src/a.js' ));
由入口文件 a
开始,传入 moduleID : "./src/a.js"
,执行方法 __webpack_require__
。
__webpack_require__
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function __webpack_require__ (moduleId ) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = (installedModules[moduleId] = { i: moduleId, l: false , exports : {}, }); modules[moduleId].call(module .exports, module , module .exports, __webpack_require__); module .l = true ; return module .exports; }
__webpack_require__
方法的主要作用就是创建缓存 module
后,执行该 module
的代码。其中 modules
即为上文所解释的各模块组成的对象。
执行各同步模块代码 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
执行模块 a
的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 '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
的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 '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
的代码:
1 2 3 4 5 6 7 8 9 10 '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
里执行:
1 2 3 4 5 6 __webpack_require__ .e(0 ) .then(__webpack_require__.bind(null , './src/c.js' )) .then(function (m ) { return m.sub(2 , 1 ); });
该打包后的代码为异步动态加载 ,源代码为:
1 import ('./c.js' ).then((m ) => m.sub(2 , 1 ));
__webpack_require__.e
__webpack_require__.e
实现异步加载模块,方法为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 var promises = [];var installedChunkData = installedChunks[chunkId];if (installedChunkData !== 0 ) { if (installedChunkData) { promises.push(installedChunkData[2 ]); } else { var promise = new Promise (function (resolve, reject ) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push((installedChunkData[2 ] = 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); } script.src = jsonpScriptSrc(chunkId); var error = new Error (); onScriptComplete = function (event ) { 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; document .head.appendChild(script); } } return Promise .all(promises);
参数 0
为 chunkId
,在方法 __webpack_require__.e
里,主要功能就是模拟 jsonp
去异步加载目标 chunk
文件 0
,返回一个 promise
对象。
然后加载异步文件 0.e3296d88.js
并执行。
加载非入口文件0.e3296d88.js
非入口文件主体结构 1 2 3 4 5 6 7 8 9 10 11 12 (window ['webpackJsonp' ] = window ['webpackJsonp' ] || []).push([ [0 ], { './src/c.js' : function (module , __webpack_exports__, __webpack_require__ ) { }, './src/d.js' : function (module , __webpack_exports__, __webpack_require__ ) { }, }, ]);
在模块加载后,就会立即执行的 window['webpackJsonp'].push()
。由 jsonp
初始化可知, 即执行 bundle
文件里的 webpackJsonpCallback
方法。
webpackJsonpCallback 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function webpackJsonpCallback (data ) { var chunkIds = data[0 ]; var moreModules = data[1 ]; var moduleId, chunkId, i = 0 , resolves = []; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (Object .prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0 ]); } installedChunks[chunkId] = 0 ; } for (moduleId in moreModules) { if (Object .prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) { resolves.shift()(); } }
webpackJsonpCallback
方法主要将异步的 chunk
里的所有模块都加到 modules
后,改变 installedChunks[chunkId]
的状态为 0
(即已加载),然后执行之前创建的 promise
的 resolve()
。
执行 resolve 的回调 then 方法 回到模块 a
根据 promise
的定义,执行 promises
队列里所有的 resolve
后,然后去执行对应的 then
方法:
1 2 3 then(__webpack_require__.bind(null , './src/c.js' ));
即执行模块 c
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 '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
不在累述。
然后执行:
1 2 3 4 5 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
1 2 3 4 5 import { mul } from './d' ;import { mul } from './d' ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "entry" : { "bundle1" : "./src/e.js" , "bundle2" : "./src/f.js" }, "plugins" : [new HtmlWebpackPlugin()], "optimization" : { "splitChunks" : { "chunks" : "all" , "minSize" : 0 , "maxSize" : 0 , "minChunks" : 1 , "name" : true , "automaticNameDelimiter" : "~" , "cacheGroups" : { "default" : { "chunks" : "all" , "minChunks" : 2 , "priority" : -10 } } } } }
引入插件 HtmlWebpackPlugin
辅助分析。
打包后代码见 github ,以下只做关键点记录:
html
会引入每个入口 bundle
生成的 js
和公共部分的 js
:
1 <script type="text/javascript" src="default~bundle1~bundle2.183bf5f4.js" ></script><script type="text/ javascript" src=" bundle1.10 ad.js"></script><script type=" text/javascript" src=" bundle2.c333.js"></script></body>
1 for (var i = 0 ; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
和在 webpackJsonpCallback
方法里执行:
1 if (parentJsonpFunction) parentJsonpFunction(data);
可以保证无论页面先加载入口文件还是非入口文件,都可以将依赖 module
同步到各自的 chunk
里。
两个入口文件 bundle1.xxxx.js bundle2.xxxx.js
的 runtime
代码里会多出一个新的变量 deferredModules
:
1 2 3 4 var deferredModules = [];deferredModules.push(['./src/e.js' , 'default~bundle1~bundle2' ]); return checkDeferredModules();
该变量为一个数组,第一个变量是需要加载的 module
,后面的变量就是要加载本 module
所需的其他依赖 module
。然后在 runtime
的末尾执行:return checkDeferredModules();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ])); } } return result; }
该方法主要检查依赖的 module
是否加载过,若都加载了则加载目标 module
。
webpackJsonpCallback
格外代码
1 2 3 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
一致。