Webpack編譯結果淺析
如今Webpack已經是一個不可或缺的前端構建工具,藉助這個構建工具,我們可以使用比較新的技術(瀏覽器不能直接支援)來開發。
你是否好奇你寫的程式碼經過Webpack構建之後會生成什麼東西?是否有時除錯遇到莫名其妙的問題?
本文不講如何進行配置,只是基於幾個基礎的例子,簡要分析一下 [email protected] 構建後的程式碼結構,當然了,並不全面,時間問題能力問題還不能理解到位。
程式碼比較長,生成的程式碼也比較晦澀比較繞,也可能條理不順,客官坐好咧~
一、Webpack的執行機制
Webpack的執行過程實際上可以歸納為這個步驟
讀取配置引數 -> 相關事件繫結(外掛參與) -> 識別各入口Entry模組 -> 編譯檔案(loader參與)-> 生成檔案
首先讀取我們的配置檔案如 webpack.config.js,然後事件流就參與進來繫結相關的事件,Webpack中的事件使用 Tapable 來管理,在這一階段,除了繫結webpack內建的一大堆事件之外,還支援自定義的一些事件處理。
配置中的 plugins部分,實際上也可以看作是一些自定義的事件處理,因為外掛將在定義的”相關時刻“插入到編譯過程中處理資源,這裡的”相關時刻“指的就是 訂閱-釋出 模式中的釋出環節
webpack支援多個入口模組,所以還需要進行各入口模組的分析(這裡的入口模組只能為JS模組),比如以下兩個入口模組
分析完入口模組,接下來分析該模組的依賴,並使用相關loader進行編譯(如果需要loader的話),真正的編譯環節是在這裡。
期間會使用AST抽象語法樹來分析語法,直到編譯完成,輸出到相應的檔案中
二、Webpack編譯結果
由最簡單的例子開始
2.1 無依賴的單個模組
./main.js
console.log('main');
./webpack.config.js
module.exports = { // entry: './main', entry: { main: './main' }, mode: 'none', output: { path: path.resolve(__dirname,'dist'), filename: '[name].js' } };
注意,在webpack4中預設的mode對 development和production進行了一些特殊配置,為了簡化,這裡就設定成none
編譯一個檔案,將在dist目錄中生成
./dist/main.js
1 /******/ (function(modules) { // webpackBootstrap 2 /******/ // The module cache 3 /******/ var installedModules = {}; 4 /******/ 5 /******/ // The require function 6 /******/ function __webpack_require__(moduleId) { 7 /******/ 8 /******/ // Check if module is in cache 9 /******/ if(installedModules[moduleId]) { 10 /******/ return installedModules[moduleId].exports; 11 /******/ } 12 /******/ // Create a new module (and put it into the cache) 13 /******/ var module = installedModules[moduleId] = { 14 /******/ i: moduleId, 15 /******/ l: false, 16 /******/ exports: {} 17 /******/ }; 18 /******/ 19 /******/ // Execute the module function 20 /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 /******/ 22 /******/ // Flag the module as loaded 23 /******/ module.l = true; 24 /******/ 25 /******/ // Return the exports of the module 26 /******/ return module.exports; 27 /******/ } 28 /******/ 29 /******/ 30 /******/ // expose the modules object (__webpack_modules__) 31 /******/ __webpack_require__.m = modules; 32 /******/ 33 /******/ // expose the module cache 34 /******/ __webpack_require__.c = installedModules; 35 /******/ 36 /******/ // define getter function for harmony exports 37 /******/ __webpack_require__.d = function(exports, name, getter) { 38 /******/ if(!__webpack_require__.o(exports, name)) { 39 /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 /******/ } 41 /******/ }; 42 /******/ 43 /******/ // define __esModule on exports 44 /******/ __webpack_require__.r = function(exports) { 45 /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 /******/ } 48 /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 /******/ }; 50 /******/ 51 /******/ // create a fake namespace object 52 /******/ // mode & 1: value is a module id, require it 53 /******/ // mode & 2: merge all properties of value into the ns 54 /******/ // mode & 4: return value when already ns object 55 /******/ // mode & 8|1: behave like require 56 /******/ __webpack_require__.t = function(value, mode) { 57 /******/ if(mode & 1) value = __webpack_require__(value); 58 /******/ if(mode & 8) return value; 59 /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 /******/ var ns = Object.create(null); 61 /******/ __webpack_require__.r(ns); 62 /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 /******/ return ns; 65 /******/ }; 66 /******/ 67 /******/ // getDefaultExport function for compatibility with non-harmony modules 68 /******/ __webpack_require__.n = function(module) { 69 /******/ var getter = module && module.__esModule ? 70 /******/ function getDefault() { return module['default']; } : 71 /******/ function getModuleExports() { return module; }; 72 /******/ __webpack_require__.d(getter, 'a', getter); 73 /******/ return getter; 74 /******/ }; 75 /******/ 76 /******/ // Object.prototype.hasOwnProperty.call 77 /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 /******/ 79 /******/ // __webpack_public_path__ 80 /******/ __webpack_require__.p = ""; 81 /******/ 82 /******/ 83 /******/ // Load entry module and return exports 84 /******/ return __webpack_require__(__webpack_require__.s = 0); 85 /******/ }) 86 /************************************************************************/ 87 /******/ ([ 88 /* 0 */ 89 /***/ (function(module, exports) { 90 91 92 console.log('main'); 93 94 95 /***/ }) 96 /******/ ]);
可以看到首先是一個匿名函式,在87行時自執行傳入
[ /* 0 */ /***/ (function(module, exports) { console.log('main'); /***/ }) /******/ ]
這個是modules,表示有一個模組需要載入
第3行使用 installedModules 來快取已經載入的模組
webpack由最初支援 commonjs模組規範,到後來要支援es6的模組等,為了相容不同的模組機制,定義了一個 __webpack_require__ 函式作為webpack內部的require
/******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache // 如果模組已經載入則直接使用 /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, // 模組ID /******/ l: false, // 模組是否已載入 /******/ exports: {} // 模組的匯出項 /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; // 標記已經載入 /******/ /******/ // Return the exports of the module /******/ return module.exports; // 返回模組的匯出專案 /******/ }
其中,這個呼叫非常重要
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
結合匿名函式傳入的引數來看,modules[moduleId] 其實就是這個
(function(module, exports) { console.log('main'); /***/ })
第一個引數 module.exports 實際上就是上面模組的匯出項,是為了保證this能正確地指向module,第二第三個引數按著順序來,第四個引數一般用於依賴
因為這裡 main.js沒有依賴其他模組,所以沒有傳進來
最後 return module.exports; 實際上就是返回了模組的匯出項,在上面的84行中,入口模組被引入 。從而自動地載入第一個模組並執行
return __webpack_require__(__webpack_require__.s = 0); // __webpack_require__.s為入口檔案,此處引用模組ID
另外再看其它程式碼,
/******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; // 將模組存起來 /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; // 將已經載入的模組存起來 /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; // 設定的 publicPath
這裡沒什麼可說的,這裡的publicPath對應於 output中的配置,如
output: { publicPath: './dist/', path: path.resolve(__dirname, 'dist'), filename: '[name].js' },
另外
/******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
這裡 __webpack_require__.o 這裡只是hasOwnProperty的包裝
__webpack_require__.d 這裡是對exports定義一個屬性(當前模組未用到,暫且如此,理解不到位)
__webpack_require__.r 這裡是對es6模組中的export的支援(當前模組未用到,暫且如此,理解不到位)
還有這個,這個就更難理解了
/******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __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__.t 暫時不說明了,還看不懂怎麼呼叫的..
__webpack_require__.n 這個主要也是為 es6模組服務的,也沒能理解好,知道的可以在評論區留言哈~
2. 有依賴的單個模組
先使用最基礎的commonjs模組規範 require, exports ,module.exports 有助於理解上面那個模組的匯出專案
./main.js
let number = require('./number');
console.log('main', number);
./number.js
let n = 10;
exports.n = n;
編譯後,生成的檔案變化的只是匿名函式傳入的部分
./dist/main.js
// 省略 /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { let number = __webpack_require__(1); console.log('main', number); /***/ }), /* 1 */ /***/ (function(module, exports) { let n = 10; exports.n = n; /***/ }) /******/ ]);
注意到前面的數字即是模組的ID,也可圖中的一致
這裡__webpack_require__引數被傳進來,main.js中引入number這個模組 __webpack_require__(1);
number模組中 exports.n = n,注意這裡的 exports即是呼叫時的第二個引數
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
所以此時 n屬性被存入module的export匯出項中,從而__webpack_require__(1) 就能獲取這個匯出項
換種方式,使用es6的模組匯出
更改 ./number.js
let n = 10;
export {
n
};
編譯後 ./dist/main.js
/******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { let number = __webpack_require__(1); console.log('main', number); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "n", function() { return n; }); let n = 10; /***/ }) /******/ ]);
可以看到模組1變了,為了相容 export ,使用 __webpack_require__.r 定義了它為es6模組,再使用__webpack_require__.d 將 n儲存到模組的匯出項中
__webpack_require__.d 函式中的 getter即為 這裡的 function() { return n; },通過設定為物件的get屬性,可以獲取到 n這個返回值
var o = {}; Object.defineProperty(o, 'abc', { get: function() { return 123; } }); console.log(o.abc); // 123
所以將 let n = 10 定義在後面也是沒問題的,因為getter是在number模組被呼叫返回之後才使用的
接著,我們把引入依賴檔案改為import
./main.js
import {n} from './number';
console.log('main', n);
編譯後 ./dist/main.js
/******/ ([ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _number__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); console.log('main', _number__WEBPACK_IMPORTED_MODULE_0__["n"]); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "n", function() { return n; }); let n = 10; /***/ }) /******/ ]);
同樣的,這時main模組用到了es6的模組引入方式,所以 __webpack_require__.r(__webpack_exports__);
var _number__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
這個 __webpack_require__(1) 實際上就是 number模組的模組匯出項,自然就能取到屬性 n 了
接下來,著眼那個 default字眼,繼續更換模組的匯入匯出方式
./main.js
import n from './number';
console.log('main', n);
./number.js
let n = 10; export default n;
./dist/main.js
/******/ ([ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _number__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); console.log('main', _number__WEBPACK_IMPORTED_MODULE_0__["default"]); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); let n = 10; /* harmony default export */ __webpack_exports__["default"] = (n); /***/ }) /******/ ]);
可以看到,變化只是屬性變成了default
再來一種 es6的方式
./main.js
import n from './number';
console.log('main', n);
./number.js
import {str as n} from './str'; export default n;
./str.js
export var str = 10;
編譯後
./dist/main.js
/******/ ([ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _number__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); console.log('main', _number__WEBPACK_IMPORTED_MODULE_0__["default"]); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _str__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony default export */ __webpack_exports__["default"] = (_str__WEBPACK_IMPORTED_MODULE_0__["str"]); /***/ }), /* 2 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "str", function() { return str; }); var str = 10; /***/ }) /******/ ]);
可以看到 {str as n} 也是沒什麼影響的,通過上面的例子應該基本能理解模組的依賴了
3. 多個入口模組
如果不提取多模組之間的公共部分,多個入口模組和單個的不同之處就是多了一個檔案而已,它們是獨立的。
所以這裡就不多說了
4. 非同步載入模組
webpack支援使用require.ensure來非同步載入模組
./main.js
console.log('main'); setTimeout(() => { require([], (require) => { let number = require('./number'); console.log(number.n); }); }, 1000);
./number.js
let n = 10;
export {
n
};
webpack.config.js中要加上 publicPath,防止非同步模組載入路徑出錯
output: { publicPath: './dist/',