歡迎關注個人公衆號睿Talk
,獲取我最新的文章:
javascript
雖然天天都在用webpack,但一直以爲隔着一層神祕的面紗,對它的工做原理一直似懂非懂。它是如何用原生JS實現模塊間的依賴管理的呢?對於按需加載的模塊,它是經過什麼方式動態獲取的?打包完成後那一堆/******/
開頭的代碼是用來幹什麼的?本文將圍繞以上3個問題,對照着源碼給出解答。java
若是你對webpack的配置調優感興趣,能夠看看我以前寫的這篇文章:webpack調優總結webpack
先寫一個簡單的JS文件,看看webpack打包後會是什麼樣子:web
// main.js console.log('Hello Dickens'); // webpack.config.js const path = require('path'); module.exports = { entry: './main.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } };
在當前目錄下運行webpack
,會在dist目錄下面生成打包好的bundle.js文件。去掉沒必要要的干擾後,核心代碼以下:segmentfault
// webpack啓動代碼 (function (modules) { // 模塊緩存對象 var installedModules = {}; // webpack實現的require函數 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; // 返回export的內容 return module.exports; } ... // 加載入口模塊 return __webpack_require__(__webpack_require__.s = 0); }) ([ /* 0 */ (function (module, exports) { console.log('Hello Dickens'); }) ]);
代碼是一個當即執行函數,參數modules
是由各個模塊組成的數組,本例子只有一個編號爲0的模塊,由一個函數包裹着,注入了module
和exports
2個變量(本例沒用到)。數組
核心代碼是__webpack_require__
這個函數,它的功能是根據傳入的模塊id,返回模塊export的內容。模塊id由webpack根據文件的依賴關係自動生成,是一個從0開始遞增的數字,入口文件的id爲0。全部的模塊都會被webpack用一個函數包裹,按照順序存入上面提到的數組實參當中。promise
模塊export的內容會被緩存在installedModules
中。當獲取模塊內容的時候,若是已經加載過,則直接從緩存返回,不然根據id從modules
形參中取出模塊內容並執行,同時將結果保存到緩存對象當中。緩存對象數據結構以下:緩存
咱們再添加一個文件,在入口文件處導入,再來看看生成的啓動文件是怎樣的。數據結構
// main.js import logger from './logger'; console.log('Hello Dickens'); logger(); //logger.js export default function log() { console.log('Log from logger'); }
啓動文件的模塊數組:app
[ /* 0 */ (function (module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__logger__ = __webpack_require__(1); console.log('Hello Dickens'); Object(__WEBPACK_IMPORTED_MODULE_0__logger__["a" /* default */ ])(); }), /* 1 */ (function (module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony export (immutable) */ __webpack_exports__["a"] = log; function log() { console.log('Log from logger'); } }) ]
能夠看到如今有2個模塊,每一個模塊的包裹函數都傳入了module, __webpack_exports__, __webpack_require__
三個參數,它們是經過上文提到的__webpack_require__
注入的:
// 執行模塊代碼 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
執行的結果也保存在緩存對象中了。
執行流程以下圖所示:
再對代碼進行改造,來研究webpack是如何實現動態加載的:
// main.js console.log('Hello Dickens'); import('./logger').then(logger => { logger.default(); });
logger文件保持不變,編譯後比以前多出了1個chunk。
bundle_asy的內容以下:
(function (modules) { // 加載成功後的JSONP回調函數 var parentJsonpFunction = window["webpackJsonp"]; // 加載成功後的JSONP回調函數 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { var moduleId, chunkId, i = 0, resolves = [], result; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; // installedChunks[chunkId]不爲0且不爲undefined,將其放入加載成功數組 if (installedChunks[chunkId]) { // promise的resolve resolves.push(installedChunks[chunkId][0]); } // 標記模塊加載完成 installedChunks[chunkId] = 0; } // 將動態加載的模塊添加到modules數組中,以供後續的require使用 for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); while (resolves.length) { resolves.shift()(); } }; // 模塊緩存對象 var installedModules = {}; // 記錄正在加載和已經加載的chunk的對象,0表示已經加載成功 // 1是當前模塊的編號,已加載完成 var installedChunks = { 1: 0 }; // require函數,跟上面的同樣 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; } // 按需加載,經過動態添加script標籤實現 __webpack_require__.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; // chunk已經加載成功 if (installedChunkData === 0) { return new Promise(function (resolve) { resolve(); }); } // 加載中,返回以前建立的promise(數組下標爲2) if (installedChunkData) { return installedChunkData[2]; } // 將promise相關函數保持到installedChunks中方便後續resolve或reject var promise = new Promise(function (resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // 啓動chunk的異步加載 var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.charset = 'utf-8'; script.async = true; script.timeout = 120000; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = __webpack_require__.p + "" + chunkId + ".bundle_async.js"; script.onerror = script.onload = onScriptComplete; var timeout = setTimeout(onScriptComplete, 120000); function onScriptComplete() { script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; // 正常的流程,模塊加載完後會調用webpackJsonp方法,將chunk置爲0 // 若是不爲0,則多是加載失敗或者超時 if (chunk !== 0) { if (chunk) { // 調用promise的reject chunk[1](new Error('Loading chunk ' + chunkId + ' failed.')); } installedChunks[chunkId] = undefined; } }; head.appendChild(script); return promise; }; ... // 加載入口模塊 return __webpack_require__(__webpack_require__.s = 0); }) ([ /* 0 */ (function (module, exports, __webpack_require__) { console.log('Hello Dickens'); // promise resolve後,會指定加載哪一個模塊 __webpack_require__.e /* import() */(0) .then(__webpack_require__.bind(null, 1)) .then(logger => { logger.default(); }); }) ]);
這裏用戶記錄異步模塊加載狀態的對象installedChunks
的數據結構以下:
當chunk加載完成後,對應的值是0。在加載過程當中,對應的值是一個數組,數組內保存了promise的相關信息。
掛在到window下面的webpackJsonp函數是動態加載模塊代碼下載後的回調,它會通知webpack模塊下載完成並將模塊加入到modules
當中。
__webpack_require__.e
函數是動態加載的核心實現,它經過動態建立一個script標籤來實現代碼的異步加載。加載開始前會建立一個promise存到installedChunks對象當中,加載成功則調用resolve,失敗則調用reject。resolve後不會傳入模塊自己,而是經過__webpack_require__
來加載模塊內容,require的模塊id由webpack來生成:
__webpack_require__.e /* import() */(0) .then(__webpack_require__.bind(null, 1)) .then(logger => { logger.default(); });
這裏之因此要加上default是由於遇到按需加載時,若是使用的是ES Module,webpack會將export default
編譯成__webpack_exports__
對象的default
屬性(感謝@MrCanJu的指正)。詳細請看動態加載的chunk的代碼,0.bundle_asy的內容以下:
webpackJsonp([0], [ /* 0 */ , /* 1 */ (function (module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["default"] = log; function log() { console.log('Log from logger'); } }) ]);
代碼很是好理解,加載成功後當即調用上文提到的webpackJsonp
方法,將chunkId和模塊內容傳入。這裏要分清2個概念,一個是chunkId,一個moduleId。這個chunk的chunkId是0,裏面只包含一個module,moduleId是1。一個chunk裏面能夠包含多個module。
執行流程以下圖所示:
本文經過分析webpack生成的啓動代碼,講解了webpack是如何實現模塊管理和動態加載的,但願對你有所幫助。
若是你對webpack的配置調優感興趣,能夠看看我以前寫的這篇文章:webpack調優總結