本文主要說明Webpack懶加載構建和加載的原理,對構建後的源碼進行分析。javascript
本文以一個簡單的示例,經過對構建好的bundle.js源碼進行分析,說明Webpack懶加載構建原理。html
本文使用的Webpack版本是4.32.2版本。java
注意:以前也分析過Webpack3.10.0版本構建出來的bundle.js,經過和此次的Webpack 4.32.2版本對比,核心的構建原理基本一致,只是將模塊索引id改成文件路徑和名字、模塊代碼改成了eval(moduleString)執行的方式等一些優化改造。webpack
1)Webpack.config.js文件內容:web
1 const path = require('path'); 2 const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 5 module.exports = { 6 entry: { 7 app: './src/index.js' 8 }, 9 output: { 10 filename: '[name].bundle.js', 11 chunkFilename: '[name].bundle.js', 12 path: path.resolve(__dirname, 'dist') 13 }, 14 plugins: [ 15 new CleanWebpackPlugin(['dist']), 16 new HtmlWebpackPlugin({ 17 title: 'Output Management' 18 }) 19 ], 20 mode: 'development' // 'production' 用於配置開發仍是發佈模式 21 };
2)建立src文件夾,添加入口文件index.js:npm
1 function component() { 2 var element = document.createElement('div'); 3 var button = document.createElement('button'); 4 var br = document.createElement('br'); 5 6 button.innerHTML = 'Click me and look at the console!'; 7 element.innerHTML = 'Hello webpack'; // _.join(['Hello', 'webpack'], ' '); 8 element.appendChild(br); 9 element.appendChild(button); 10 11 button.onclick = ( 12 e => { 13 // 注意:下邊的註釋不寫的話,打包出來的print文件包名就不是print.bundle.js,而是0.bundle.js 14 import(/* webpackChunkName: "print" */'./print').then( 15 module => { 16 var print = module.default; 17 print(); 18 } 19 ) 20 } 21 ); 22 23 return element; 24 } 25 26 document.body.appendChild(component());
3)在src目錄下建立print.js文件:json
1 export default () => { 2 console.log('Button Clicked: Here\'s "some text"!'); 3 }
4)package.json文件內容:數組
{ "name": "webpack-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "webpack": "webpack", }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "clean-webpack-plugin": "^0.1.18", "html-webpack-plugin": "^3.2.0", "webpack": "^4.32.2", "webpack-cli": "^3.3.2" }, "dependencies": { "lodash": "^4.17.4" } }
執行構建命令:npm run webpackpromise
在dist目錄下生成了兩個文件:app.bundle.js和print.bundle.js。緩存
app.bundle.js源碼以下(下邊代碼是將註釋去掉、壓縮的代碼還原後的代碼):
1 (function (modules) { 2 function webpackJsonpCallback(data) { 3 var chunkIds = data[0]; 4 var moreModules = data[1]; 5 6 // add "moreModules" to the modules object, 7 // then flag all "chunkIds" as loaded and fire callback 8 var moduleId, chunkId, i = 0, resolves = []; 9 for (; i < chunkIds.length; i++) { 10 chunkId = chunkIds[i]; 11 if (installedChunks[chunkId]) { 12 resolves.push(installedChunks[chunkId][0]); 13 } 14 installedChunks[chunkId] = 0; 15 } 16 for (moduleId in moreModules) { 17 if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { 18 modules[moduleId] = moreModules[moduleId]; 19 } 20 } 21 if (parentJsonpFunction) parentJsonpFunction(data); 22 23 while (resolves.length) { 24 resolves.shift()(); 25 } 26 }; 27 28 // The module cache 29 var installedModules = {}; 30 31 // object to store loaded and loading chunks 32 // undefined = chunk not loaded, null = chunk preloaded/prefetched 33 // Promise = chunk loading, 0 = chunk loaded 34 var installedChunks = { 35 "app": 0 36 }; 37 38 // script path function 39 function jsonpScriptSrc(chunkId) { 40 return __webpack_require__.p + "" + ({"print": "print"}[chunkId] || chunkId) + ".bundle.js" 41 } 42 43 // The require function 44 function __webpack_require__(moduleId) { 45 // Check if module is in cache 46 if (installedModules[moduleId]) { 47 return installedModules[moduleId].exports; 48 } 49 // Create a new module (and put it into the cache) 50 var module = installedModules[moduleId] = { 51 i: moduleId, 52 l: false, 53 exports: {} 54 }; 55 56 // Execute the module function 57 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 58 59 // Flag the module as loaded 60 module.l = true; 61 62 // Return the exports of the module 63 return module.exports; 64 } 65 66 // This file contains only the entry chunk. 67 // The chunk loading function for additional chunks 68 __webpack_require__.e = function requireEnsure(chunkId) { 69 var promises = []; 70 71 // JSONP chunk loading for javascript 72 73 var installedChunkData = installedChunks[chunkId]; 74 if (installedChunkData !== 0) { // 0 means "already installed". 75 76 // a Promise means "currently loading". 77 if (installedChunkData) { 78 promises.push(installedChunkData[2]); 79 } else { 80 // setup Promise in chunk cache 81 var promise = new Promise(function (resolve, reject) { 82 installedChunkData = installedChunks[chunkId] = [resolve, reject]; 83 }); 84 promises.push(installedChunkData[2] = promise); 85 86 // start chunk loading 87 var script = document.createElement('script'); 88 var onScriptComplete; 89 90 script.charset = 'utf-8'; 91 script.timeout = 120; 92 if (__webpack_require__.nc) { 93 script.setAttribute("nonce", __webpack_require__.nc); 94 } 95 script.src = jsonpScriptSrc(chunkId); 96 97 // create error before stack unwound to get useful stacktrace later 98 var error = new Error(); 99 onScriptComplete = function (event) { 100 // avoid mem leaks in IE. 101 script.onerror = script.onload = null; 102 clearTimeout(timeout); 103 var chunk = installedChunks[chunkId]; 104 if (chunk !== 0) { 105 if (chunk) { 106 var errorType = event && (event.type === 'load' ? 'missing' : event.type); 107 var realSrc = event && event.target && event.target.src; 108 error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; 109 error.type = errorType; 110 error.request = realSrc; 111 chunk[1](error); 112 } 113 installedChunks[chunkId] = undefined; 114 } 115 }; 116 var timeout = setTimeout(function () { 117 onScriptComplete({type: 'timeout', target: script}); 118 }, 120000); 119 script.onerror = script.onload = onScriptComplete; 120 document.head.appendChild(script); 121 } 122 } 123 return Promise.all(promises); 124 }; 125 126 // expose the modules object (__webpack_modules__) 127 __webpack_require__.m = modules; 128 129 // expose the module cache 130 __webpack_require__.c = installedModules; 131 132 // define getter function for harmony exports 133 __webpack_require__.d = function (exports, name, getter) { 134 if (!__webpack_require__.o(exports, name)) { 135 Object.defineProperty(exports, name, {enumerable: true, get: getter}); 136 } 137 }; 138 139 // define __esModule on exports 140 __webpack_require__.r = function (exports) { 141 if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { 142 Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'}); 143 } 144 Object.defineProperty(exports, '__esModule', {value: true}); 145 }; 146 147 // create a fake namespace object 148 // mode & 1: value is a module id, require it 149 // mode & 2: merge all properties of value into the ns 150 // mode & 4: return value when already ns object 151 // mode & 8|1: behave like require 152 __webpack_require__.t = function (value, mode) { 153 if (mode & 1) value = __webpack_require__(value); 154 if (mode & 8) return value; 155 if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 156 var ns = Object.create(null); 157 __webpack_require__.r(ns); 158 Object.defineProperty(ns, 'default', {enumerable: true, value: value}); 159 if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { 160 return value[key]; 161 }.bind(null, key)); 162 return ns; 163 }; 164 165 // getDefaultExport function for compatibility with non-harmony modules 166 __webpack_require__.n = function (module) { 167 var getter = module && module.__esModule ? 168 function getDefault() { 169 return module['default']; 170 } : 171 function getModuleExports() { 172 return module; 173 }; 174 __webpack_require__.d(getter, 'a', getter); 175 return getter; 176 }; 177 178 // Object.prototype.hasOwnProperty.call 179 __webpack_require__.o = function (object, property) { 180 return Object.prototype.hasOwnProperty.call(object, property); 181 }; 182 183 // __webpack_public_path__ 184 __webpack_require__.p = ""; 185 186 // on error function for async loading 187 __webpack_require__.oe = function (err) { 188 console.error(err); 189 throw err; 190 }; 191 192 var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; 193 var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 複製一個數組的push方法,這個方法的this是jsonpArray 194 jsonpArray.push = webpackJsonpCallback; // TODO: 爲何要複寫push,而不是直接增長一個新方法名? 195 jsonpArray = jsonpArray.slice(); // 拷貝一個新數組 196 for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); 197 var parentJsonpFunction = oldJsonpFunction; 198 199 // Load entry module and return exports 200 return __webpack_require__(__webpack_require__.s = "./src/index.js"); 201 }) 202 /************************************************************************/ 203 ({ 204 "./src/index.js": (function (module, exports, __webpack_require__) { 205 function component() { 206 var element = document.createElement('div'); 207 var button = document.createElement('button'); 208 var br = document.createElement('br'); 209 210 button.innerHTML = 'Click me and look at the console!'; 211 element.innerHTML = 'Hello webpack'; // _.join(['Hello', 'webpack'], ' '); 212 element.appendChild(br); 213 element.appendChild(button); 214 215 button.onclick = ( 216 e => { 217 __webpack_require__.e("print") 218 .then(__webpack_require__.bind(null, "./src/print.js")) 219 .then( 220 module => { 221 var print = module.default; 222 print(); 223 } 224 ) 225 } 226 ); 227 228 return element; 229 } 230 231 document.body.appendChild(component()); 232 }) 233 });
print.bundle.js的源碼以下:
1 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([ // 注意:這個push實際是webpackJsonpCallback方法 2 ["print"], 3 { 4 "./src/print.js": (function(module, __webpack_exports__, __webpack_require__) { 5 "use strict"; 6 __webpack_require__.r(__webpack_exports__); 7 __webpack_exports__["default"] = (() => { 8 console.log('Button Clicked: Here\'s "some text"!'); 9 }); 10 }) 11 } 12 ]);
說明:懶加載構建和和上一篇的基礎構建原理中有不少相同的代碼,這裏再也不重複說明,本文主要詳細說明其中增長的懶加載方面的內容。
app.bundle.js是構建好的入口文件,裏邊就是一個自執行函數,基本結構和上一篇基礎構建源碼中一致,這裏再也不詳細說明。下邊是使用懶加載模塊構建後,增長的內容,這裏詳細說明這些內容:
1 (function (modules) { 2 function webpackJsonpCallback(data) {...}; 3 4 // The module cache 5 var installedModules = {}; 6 7 // object to store loaded and loading chunks 8 // undefined = chunk not loaded, null = chunk preloaded/prefetched 9 // Promise = chunk loading, 0 = chunk loaded 10 var installedChunks = { 11 "app": 0 12 }; 13 14 // script path function 15 function jsonpScriptSrc(chunkId) {...} 16 17 // The require function 18 function __webpack_require__(moduleId) {...} 19 20 // This file contains only the entry chunk. 21 // The chunk loading function for additional chunks 22 __webpack_require__.e = function requireEnsure(chunkId) {...}; 23 24 // .... 25 26 // on error function for async loading 27 __webpack_require__.oe = function (err) { 28 console.error(err); 29 throw err; 30 }; 31 32 var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; 33 var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 複製一個數組的push方法,這個方法的this是jsonpArray 34 jsonpArray.push = webpackJsonpCallback; // TODO: 爲何要複寫push,而不是直接增長一個新方法名? 35 jsonpArray = jsonpArray.slice(); // 拷貝一個新數組 36 for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); 37 var parentJsonpFunction = oldJsonpFunction; 38 39 // Load entry module and return exports 40 return __webpack_require__(__webpack_require__.s = "./src/index.js"); 41 }) 42 /************************************************************************/ 43 ({ 44 "./src/index.js": (function (module, exports, __webpack_require__) {...}) 45 });
咱們詳細分析下新增的這些代碼。
根據註釋,該對象變量主要緩存各個獨立的js文件模塊的加載狀態。
該對象的key就是chunkId,而chunkId實際就是文件名去掉.bundle.js後剩餘的內容,例如:print.bundle.js的chunkId就是print。
根據值的不一樣標誌着key對應的文件加載狀態主要有如下幾種:
undefined:key對應的文件未加載;
null:key對應的文件延遲加載;
數組:正在加載(注意,這裏的註釋有點不許確,這個數組實際存儲的是一個promise的實例,以及對應的reject和resolve);
0:已經加載過了。
這個變量的核心做用:當一個懶加載模塊被多個文件依賴時,若是該模塊已經被加載過了,就不會被其它模塊加載了。判斷方法就是經過該緩存變量判斷的。具體源碼能夠在__webpack_require__.e函數中看到:
1 __webpack_require__.e = function requireEnsure(chunkId) { 2 var promises = []; 3 4 // JSONP chunk loading for javascript 5 var installedChunkData = installedChunks[chunkId]; 6 if (installedChunkData !== 0) { // 0 means "already installed". 7 8 // a Promise means "currently loading". 9 if (installedChunkData) { 10 promises.push(installedChunkData[2]); 11 } else { 12 // ... 13 // 建立一個<script>標籤,將路徑設置爲懶加載文件路徑,並插入HTML,實現該懶加載文件的加載。 14 } 15 } 16 return Promise.all(promises); 17 };
該函數主要做用就是建立一個<script>標籤,而後將chunkId對應的文件經過該標籤加載。
源代碼以下:
1 __webpack_require__.e = function requireEnsure(chunkId) { 2 var promises = []; 3 4 // JSONP chunk loading for javascript 5 6 var installedChunkData = installedChunks[chunkId]; 7 if (installedChunkData !== 0) { // 0 means "already installed". 8 9 // a Promise means "currently loading". 10 if (installedChunkData) { 11 promises.push(installedChunkData[2]); 12 } else { 13 // setup Promise in chunk cache 14 var promise = new Promise(function (resolve, reject) { 15 installedChunkData = installedChunks[chunkId] = [resolve, reject]; 16 }); 17 promises.push(installedChunkData[2] = promise); 18 19 // start chunk loading 20 var script = document.createElement('script'); 21 var onScriptComplete; 22 23 script.charset = 'utf-8'; 24 script.timeout = 120; 25 if (__webpack_require__.nc) { 26 script.setAttribute("nonce", __webpack_require__.nc); 27 } 28 script.src = jsonpScriptSrc(chunkId); 29 30 // create error before stack unwound to get useful stacktrace later 31 var error = new Error(); 32 onScriptComplete = function (event) { 33 // avoid mem leaks in IE. 34 script.onerror = script.onload = null; 35 clearTimeout(timeout); 36 var chunk = installedChunks[chunkId]; 37 if (chunk !== 0) { 38 if (chunk) { 39 var errorType = event && (event.type === 'load' ? 'missing' : event.type); 40 var realSrc = event && event.target && event.target.src; 41 error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; 42 error.type = errorType; 43 error.request = realSrc; 44 chunk[1](error); 45 } 46 installedChunks[chunkId] = undefined; 47 } 48 }; 49 var timeout = setTimeout(function () { 50 onScriptComplete({type: 'timeout', target: script}); 51 }, 120000); 52 script.onerror = script.onload = onScriptComplete; 53 document.head.appendChild(script); 54 } 55 } 56 return Promise.all(promises); 57 };
主要作了以下幾個事情:
1)判斷chunkId對應的模塊是否已經加載了,若是已經加載了,就再也不從新加載;
2)若是模塊沒有被加載過,但模塊處於正在被加載的過程,再也不重複加載,直接將加載模塊的promise返回。
爲何會出現這種狀況?
例如:咱們將index.js中加載print.js文件的地方改造爲下邊屢次經過ES6的import加載print.js文件:
1 button.onclick = ( 2 e => { 3 4 import('./print').then( 5 module => { 6 var print = module.default; 7 print(); 8 } 9 ); 10 11 import('./print').then( 12 module => { 13 var print = module.default; 14 print(); 15 } 16 ) 17 } 18 );
從上邊代碼能夠看出,當第一import加載print.js文件時,尚未resolve,就又執行第二個import文件了,而爲了不重複加載該文件,就經過將這裏的判斷,避免了重複加載。
3)若是模塊沒有被加載過,也不處於加載過程,就建立一個promise,並將resolve、reject、promise構成的數組存儲在上邊說過的installedChunks緩存對象屬性中。而後建立一個script標籤加載對應的文件,加載超時時間是2分鐘。若是script文件加載失敗,觸發reject(對應源碼中:chunk[1](error),chunk[1]就是上邊緩存的數組的第二個元素reject),並將installedChunks緩存對象中對應key的值設置爲undefined,標識其沒有被加載。
4)最後返回promise
注意:源碼中,這裏返回的是Promise.all(promises),分析代碼發現promises好像只可能有一個元素。可能還沒遇到多個promises的場景吧。留待後續研究。
整個app.bundle.js文件是一個自執行函數,該函數中執行的代碼以下:
1 var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
2 var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 複製一個數組的push方法,這個方法的this是jsonpArray
3 jsonpArray.push = webpackJsonpCallback; // TODO: 爲何要複寫push,而不是直接增長一個新方法名?
4 jsonpArray = jsonpArray.slice(); // 拷貝一個新數組
5 for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); 6 var parentJsonpFunction = oldJsonpFunction;
這段代碼主要作了以下幾個事情:
1)定義了一個全局變量webpackJsonp,改變量是一個數組,該數組變量的原生push方法被複寫爲webpackJsonpCallback方法,該方法是懶加載實現的一個核心方法,具體代碼會在下邊分析。
該全局變量在懶加載文件中被用到。在print.bundle.js中:
1 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([ // 注意:這個push實際是webpackJsonpCallback方法
2 ["print"],
3 { 4 "./src/print.js": (function(module, __webpack_exports__, __webpack_require__) {...}) 5 } 6 ]);
2)將數組的原生push方法備份,賦值給parentJsonpFunction變量保存。
注意:該方法的this是全局變量webpackJsonp,也就是說parentJsonpFunction('111')後,全局數組變量webpackJsonp就增長了一個'111'元素。
該方法在webpackJsonpCallback中會用到,是將懶加載文件的內容保存到全局變量webpackJsonp中。
3)上邊第一步中複寫push的緣由?
多是由於在懶加載文件中,調用了複寫後的push,執行了原生push的功能,所以,爲了更形象的表達該意思,所以直接複寫了push。
但我的認爲這個不太好,不易讀。直接新增一個_push或者extendPush,這樣是否是讀起來就很簡單了。
該函數是懶加載的一個比較核心代碼。其代碼以下:
1 function webpackJsonpCallback(data) {
2 var chunkIds = data[0]; 3 var moreModules = data[1]; 4 5 // add "moreModules" to the modules object, 6 // then flag all "chunkIds" as loaded and fire callback 7 var moduleId, chunkId, i = 0, resolves = []; 8 for (; i < chunkIds.length; i++) { 9 chunkId = chunkIds[i]; 10 if (installedChunks[chunkId]) { 11 resolves.push(installedChunks[chunkId][0]); 12 } 13 installedChunks[chunkId] = 0; 14 } 15 for (moduleId in moreModules) { 16 if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { 17 modules[moduleId] = moreModules[moduleId]; 18 } 19 } 20 if (parentJsonpFunction) parentJsonpFunction(data); 21 22 while (resolves.length) { 23 resolves.shift()(); 24 } 25 };
參數說明:
參數是一個數組。有兩個元素:第一個元素是要懶加載文件中全部模塊的chunkId組成的數組;第二個參數是一個對象,對象的屬性和值分別是要加載模塊的moduleId和模塊代碼函數。
該函數主要作的事情以下:
1)遍歷參數中的chunkId:
判斷installedChunks緩存變量中對應chunkId的屬性值:若是是真,說明模塊正在加載,由於從上邊分析中能夠知道,installedChunks[chunkId]只有一種狀況是真,那就是在對應的模塊正在加載時,會將加載模塊建立的promise的三個信息搞成一個數組[resolve, reject, proimise]賦值給installedChunks[chunkId]。將resolve存入resolves變量中。
將installedChunks中對應的chunkId置爲0,標識該模塊已經被加載過了。
2)遍歷參數中模塊對象全部屬性:
將模塊代碼函數存儲到modules中,該modules是入口文件app.bundle.js中自執行函數的參數。
這一步很是關鍵,由於執行模塊加載函數__webpack_require__時,獲取模塊代碼時,就是經過moduleId從modules中查找對應模塊代碼。
3)調用parentJsonpFunction(原生push方法)將整個懶加載文件的數據存入全局數組變量window.webpackJsonp。
4)遍歷resolves,執行全部promise的resolve:
當執行了promise的resolve後,纔會走到promise.then的成功回調中,查看源碼能夠看到:
1 button.onclick = (
2 e => { 3 __webpack_require__.e("print") 4 .then(__webpack_require__.bind(null, "./src/print.js")) 5 .then( 6 module => { 7 var print = module.default; 8 print(); 9 } 10 ) 11 } 12 );
resolve後,執行了兩個then回調:
第一個回調是調用__webpack_require__函數,傳入的參數是懶加載文件中的一個模塊的moduleId,而這個moduleId就是上邊存入到modules變量其中一個。這樣就經過__webpack_require__執行了模塊的代碼。並將模塊的返回值,傳遞給第二個then的回調函數;
第二個回調函數是真正的onclick回調函數的業務代碼。
5)重要思考:
從這個函數能夠看出:
調用__webpack_require__.e('print')方法,實際只是將對應的print.bundle.js文件加載和建立了一個異步的promise(由於並不知道何時這個文件才能執行完,所以須要一個異步promise,而promise的resolve會在對應的文件加載時執行,這樣就能實現異步文件加載了),並無將懶加載文件中保存的模塊代碼執行。
在加載對應print.bundle.js文件代碼時,經過調用webpackJsonpCallback函數,實現觸發加載文件時建立的promise的resolve。
resolve觸發後,會執行promise的then回調,這個回調經過__webpack_require__函數執行了真正須要模塊的代碼(注意:若是print.bundle.js中有不少模塊,只會執行用到的模塊代碼,而不是執行全部模塊的代碼),執行完後將模塊的exports返回給promise的下一個then函數,該函數也就是真正的業務代碼了。
綜上,能夠看出,webpack實際是經過promise,巧妙的實現了模塊的懶加載功能。