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"),哦哦,原理是就是這麼動態鏈接的!!!