bundle分析
无splitChunk时的main.bundle.js
如果不splitChunk,也不用dll提前构建,name打包出来的就只有一个文件,就叫main.bundle.js吧,这个文件干啥呢?VScode command + K + 0折叠所有代码:
哦 , 原来是个自执行函数,看看参数:
一大坨object, 第一个是入口文件,其他貌似是依赖。接着看看函数拿这些参数要干啥
前面都是方法的定义,走到最后一行:
return __webpack_require__(__webpack_require__.s = "./app.js");
webpack_require
执行__webpack_require__("./app.js),这个方法比较简单:
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // installedModules缓存中有moduleId模块直接取出来用
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // installedModules缓存中没有的话就以moduleId为key给自己加一个
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // modules就是那一大坨object,就是执行那一大坨object中key:"./app.js"对应的value,是个方法
/******/ // 模块中一般都会module.exports要返回的结果,这不就写到installedModules[moduleId].exports中了嘛
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // 把moduleId的加载状态记为已加载
/******/ module.l = true;
/******/
/******/ // Return installedModules[moduleId].exports
/******/ return module.exports;
/******/ }
哦,就是一个带缓存及模块加载状态的webpack require嘛,,接着看看"./app.js"对应的回调方法要干啥:
一大坨eval,里面还是字符串,晕,为啥?不清楚
定睛一看,是要require vue的运行时代码和vue模板文件,这里应该就会构建DOM树吧
optimization
webpack配置
现在给webpack配置文件添加:
optimization: {
splitChunks: {
chunks: 'all', // 依赖都分离出来
name: 'vendor', // 分离出来的bundle名称,命名即output中的name
minChunks: 1,
// minChunks: 2, // 被共享的最小次数,如loadash只被引用了一次是不是将其抽离的
},
// runtimeChunk: true
},
就是分离依赖的意思
build结果及main.bundle.js
build agian,看看dist文件输出,除过main.bundle.js还有一个vendor.bundle.js,
先看看main.bundle.js,还是一个自执行函数,参数还是那一大坨object,不过方法体中貌似多了很多东西,捡最重要的说
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 就是下面这句最重要,在jsonpArray.push时执行webpackJsonpCallback,而在jsonpArray就是window["webpackJsonp"],
// 所以当先加载main.bundle.js后加载vendor.bundle.js时也会执行到webpackJsonpCallback
/******/ jsonpArray.push = webpackJsonpCallback;
// 再将jsonpArray还原为自己本来的样子,去掉push方法
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/ // add entry module to deferred list,splitChunk出来的bundle也是一个entry
/******/ deferredModules.push(["./app.js","vendor"]);
这段代码非常关键,也说明为啥main.bundle.js和vendor.bundle.js引入顺序不同也能运行成功的原因。
window[“webpackJsonp”]是啥?容我打开vendor.bundle.js,手动格式化后给大家看看:
正是给window[“webpackJsonp”] push一个数组(arr),arr0又是一个数组,arr1结果又是那一大坨object,原来是分离出的依赖
看到这里就能看懂main.bundle.js截取的代码注释了
webpackJsonpCallback window[“webpackJsonp”] push时标记vendor已加载
而webpackJsonpCallback干啥呢
// window["webpackJsonp"] push的东西是一个数组,往上第二个找图,就是参数data
function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0]; // ["vendor"]
/******/ var moreModules = data[1]; // 一大坨object
/******/ var executeModules = data[2];
/******/
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i]; // chunkId为'vendor'
/******/ // some code here and installedChunks初始值为{ "main": 0 }
/******/ installedChunks[chunkId] = 0;
/******/ }
// modules中加入vendor.bundle.js中的那一大坨object,当然main.bundle.js自执行方法的入参对应的那一大坨object也在其中,至此,所有的依赖都加载进来了
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ // some code here
/******/
/******/ // run deferred modules when all chunks ready,英文解释已经很清晰了
/******/ return checkDeferredModules();
/******/ };
checkDeferredModules bundle加载完后执行app.js回调
现在看看这个checkDeferredModules,它才是最终执行代码
// deferredModules.push(["./app.js","vendor"]);
/******/ function checkDeferredModules() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i]; // deferredModule为["./app.js","vendor"]
/******/ var fulfilled = true;
/******/ for(var j = 1; j < deferredModule.length; j++) { // 从1开始
/******/ var depId = deferredModule[j]; // depId为vendor
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
// 如果vendor加载后执行__webpack_require__("./app.js");即执行main.bundle.js自执行函数入参中"./app.js"对应的value回调
/******/ if(fulfilled) {
/******/ deferredModules.splice(i--, 1);
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/
/******/ return result;
/******/ }
最开始的时候已经解释了"./app.js"回调是干啥的
运行时代码抽离
splitChunk后发现main.bundle.js自执行函数不纯洁,如果把他的入参也取出来,那main.bundle.js不就是真正的runtime了
optimize帮你解决,配置为:optimize.runtimeChunk: true
build again,发现这时有3个文件,分别是:
这时runtime~main.bundle.js中就是运行时的代码了,而main.bundle.js和vendor.bundle.js都是给window[“webpackJsonp”] push了
DllPlugin (dynamic link libray)动态链接库
终于说到DllPlugin了,它的目的就是提前构建,然后vendor中需要moduleId时只需从提前构建的那一大坨object里取就行,既然是提前构建,那必须不能和webpack.config.js一起运行啊,那就新建一个webpack.dll.js吧
const webpack = require('webpack');
const path = require('path');
module.exports = {
mode: "development",
entry: {
commonLib: ['vue']
},
output: {
path: path.resolve(__dirname, './dllJs'),
filename: 'dll.[name].js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: './[name].manifest.json',
name: '[name]_library',
context: __dirname
})
]
}
和webpack普通的配置一样,entry中包含的依赖即需要提前构建的,这里以vue举例,webpack.DllPlugin里的name要和output中的library对应,后面会说到
webpack --config webpack.dll.js
执行后发现dllJs文件下有一个dll.commonLib.js文件,根目录下有一个commonLib.manifest.json文件,看看commonLib.manifest.json文件,包含了vue的运行时代码路径。
"./node_modules/vue/dist/vue.runtime.esm.js": {
"id": "./node_modules/vue/dist/vue.runtime.esm.js",
"buildMeta": {
"exportsType": "namespace",
"providedExports": ["default"]
}
},
而dll.commonLib.js呢?
将一个很熟悉的自执行函数赋值给了commonLib_library,这个名字就是 webpack.DllPlugin中的name。
打开看看,自执行函数的入参最重要的是包含了vue的运行时代码,很符合预期,这就是我要提前构建的代码呀
此方法中关键的一步
return __webpack_require__(__webpack_require__.s = 0);
0就是那一大坨object中
看样子是将此文件中的__webpack_require__写到入参module的exports中
全局来看这个文件就是:
var commonLib_librarycommonLib_library = __webpack_require__
和webpack.config.js结合
那提前构建好了后怎么用呢,这就需要在webpack.config.js中添加配置了
optimization: {
splitChunks: {
chunks: 'all',
name: 'vendor',
minChunks: 1
},
runtimeChunk: true
},
plugins: [
new VueLoaderPlugin(),
new webpack.DllReferencePlugin({
context: __dirname, // 我猜是node_modules的路径。。。,可省略
manifest: require('./commonLib.manifest.json') // 引入dll构建好的json
})
]
build webpack.config.js again
生成的文件是
先看看 main.bundle.js,仍然是window[“webpackJsonp”] push ,找找vue.runtime.esm.js
/***/ "./node_modules/vue/dist/vue.runtime.esm.js":
/*!*************************************************************************************************!*\
!*** delegated ./node_modules/vue/dist/vue.runtime.esm.js from dll-reference commonLib_library ***!
\*************************************************************************************************/
/*! exports provided: default */
/***/ (function(module, exports, __webpack_require__) {
eval("module.exports = (__webpack_require__(/*! dll-reference commonLib_library */ \"dll-reference commonLib_library\"))(\"./node_modules/vue/dist/vue.runtime.esm.js\");\n\n//# sourceURL=webpack:///delegated_./node_modules/vue/dist/vue.runtime.esm.js_from_dll-reference_commonLib_library?");
/***/ }),
恶心的eval,果然evil
上面最关键的是执行
module.exports = (__webpack_require__("dll-reference commonLib_library"))("./node_modules/vue/dist/vue.runtime.esm.js")
webpack_require(“dll-reference commonLib_library”)又是什么鬼,command + f后输入 “dll-reference commonLib_library”:
原来是把commonLib_library写到module.exports上,commonLib_library就是dll.commonLib.js执行结果,及dll.commonLib.js里边的__webpack_require__
所以使是用dll.commonLib.js里边的__webpack_require__("./node_modules/vue/dist/vue.runtime.esm.js"),哦哦,原理是就是这么动态链接的!!!