1. 程式人生 > 其它 >05.webpack的模組化原理

05.webpack的模組化原理

webpack中mode配置

在使用webpack打包的過程中,如果不設定mode屬性,那麼每次執行npm run build的時候總會丟擲一個警告,用來提示我們設定mode屬性:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. 
Learn more: https://webpack.js.org/configuration/mode/

以上這段話的意思是:在webpack.config.js配置檔案中你還沒有對mode也就是打包的模式做一個配置,但是webpack會預設應用當然的mode為production生產模式,除了production之外你還可以設定模式為development開發模式或者none不設定。

1. production 生產模式

mode設定為production生產模式之後,webpack會自動為我們開啟很多預設的優化選項,並且會將DefinePlugin也就是配置全域性常量的外掛中process.env.NODE_ENV的值設定為production,同時為模組和chunk開啟確定性的混淆名稱,也就是會將程式碼在打包的時候進行混淆和壓縮。設定之後預設開啟的優化選項如下:

// webpack.production.config.js
module.exports = {
+  mode: 'production',  // 開啟此選項等於設定了下面這些配置
- performance: {
-   hints: 'warning'
- },
- output: {
-   pathinfo: false
- },
- optimization: {
-   namedModules: false,
-   namedChunks: false,
-   nodeEnv: 'production',
-   flagIncludedChunks: true,
-   occurrenceOrder: true,
-   sideEffects: true,
-   usedExports: true,
-   concatenateModules: true,
-   splitChunks: {
-     hidePathInfo: true,
-     minSize: 30000,
-     maxAsyncRequests: 5,
-     maxInitialRequests: 3,
-   },
-   noEmitOnErrors: true,
-   checkWasmTypes: true,
-   minimize: true,
- },
- plugins: [
-   new TerserPlugin(/* ... */),
-   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
-   new webpack.optimize.ModuleConcatenationPlugin(),
-   new webpack.NoEmitOnErrorsPlugin()
- ]
}

2. development 開發模式

mode設定為development開發模式之後,webpack也會為我們開啟很多預設的優化配置選項,並且會將DefinePlugin也就是配置全域性常量的外掛中process.env.NODE_ENV的值設定為development,但是打包後的程式碼中的變數及函式名稱都是有效名稱。

// webpack.development.config.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- cache: true,
- performance: {
-   hints: false
- },
- output: {
-   pathinfo: true
- },
- optimization: {
-   namedModules: true,
-   namedChunks: true,
-   nodeEnv: 'development',
-   flagIncludedChunks: false,
-   occurrenceOrder: false,
-   sideEffects: false,
-   usedExports: false,
-   concatenateModules: false,
-   splitChunks: {
-     hidePathInfo: false,
-     minSize: 10000,
-     maxAsyncRequests: Infinity,
-     maxInitialRequests: Infinity,
-   },
-   noEmitOnErrors: false,
-   checkWasmTypes: false,
-   minimize: false,
- },
- plugins: [
-   new webpack.NamedModulesPlugin(),
-   new webpack.NamedChunksPlugin(),
-   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}

3. none 不使用任何預設優化選項

// webpack.custom.config.js
module.exports = {
+ mode: 'none',
- performance: {
-  hints: false
- },
- optimization: {
-   flagIncludedChunks: false,
-   occurrenceOrder: false,
-   sideEffects: false,
-   usedExports: false,
-   concatenateModules: false,
-   splitChunks: {
-     hidePathInfo: false,
-     minSize: 10000,
-     maxAsyncRequests: Infinity,
-     maxInitialRequests: Infinity,
-   },
-   noEmitOnErrors: false,
-   checkWasmTypes: false,
-   minimize: false,
- },
- plugins: []
}

webpack的模組化原理

webpack在打包程式碼的時候允許我們寫的程式碼裡面使用各種型別的模組化,最常用的是ES6 Module和CommonJS兩個模組化標準,瀏覽器首先是不支援CommonJS標準的,並且低版本的瀏覽器也是不支援ES6 Module的,在高版本的瀏覽器中要支援ES6 Module也需要配置script標籤的type=module才可以,那麼思考一個問題:webpack是如何做到將打包之前瀏覽器不支援的模組化寫法經過打包之後可以讓大多數瀏覽器都支援的呢?所以就需要探討下webpack的模組化實現原理:

  1. webpack是如何實現CommonJs模組化的?
  2. webpack是如何實現ES Module模組化的?
  3. webpack是如何實現在CommonJs模組中載入ES Module的?
  4. webpack是如何實現在ES Module中載入CommonJs模組的?

分析原始碼前的配置

  1. 設定mode為development避免混淆名稱
    在分析webpack打包後的檔案bundle.js之前我們先將mode模式設定為development,因為這樣可以保證打包之後的程式碼中模組名和變數名不會經過混淆醜化,便於我們對比打包前後的程式碼。

  2. 設定devtool為source-map避免將原始碼轉化為eval函式執行的字串

mode設定為development代表著開啟了很多優化配置選項,而其中有一條就是將devtool的值設定為eval,該項配置的作用就是將打包前的原始碼在打包之後轉化為一個程式碼字串被eval()函式執行,而這對於除錯和閱讀原始碼是非常不友好的,所以我們需要將devtool的值先設定為source-map。

module.exports = {
	mode:"development",
	devtool:"source-map",
}

webpack實現CommonJs模組化原始碼分析

在utils.js中基於CommonJs語法匯出兩個函式:

function sum (a,b) {
	return a+b;
}

function mul (a,b){
	return a*b;
}

module.exports = {
	sum,
	mul
}

在專案入口檔案main.js中匯入:

const {sum,mul} = require('./js/CommonJS.js');

console.log(sum(10,20));
console.log(mul(10,20));

執行npm run build打包,雖然瀏覽器不支援require和module.exports語法,但是打包之後的程式碼是可以在瀏覽器中正確執行的,webpack在實現CommonJs模組化的時候,主要內部做了以下工作:

1. 定義__webpack_modules__物件

var __webpack_modules__ = {
 	"./src/js/CommonJS.js": (function(module) {
 		function sum(a, b) {
 			return a + b;
 		}

 		function mul(a, b) {
 			return a * b;
 		}
 		module.exports = {
 			sum,
 			mul
 		}
 	})
 };

要點1:立即執行函式IEEF

bundle.js檔案中最外層是一個立即執行函式,代表此檔案只要被瀏覽器載入之後就會立即執行裡面的程式碼,webpack在實現模組化原理的時候在很多地方使用了立即執行函式,只不過寫法不同,主要有三種寫法:

// 第一種寫法:兩個括號包裹
(function(...args){})(arg1,arg2); 

// 第二種寫法:一個大括號包裹
(function(...args){}(arg1,arg2));

// 第三種寫法:將函式變為一個表示式,js引擎也會直接將該函式執行
!function(...args){}(arg1,arg2);

要點2:將要打包的模組分別要鍵值對進行對映

以上程式碼表示以模組的相對於根目錄的路徑為物件key值,以一個函式為value,這個函式接收一個module物件作為引數,函式體就是當前模組要匯出的變數、函式等,最後在函式的最底部給module物件上添加了一個exports屬性,並將要匯出的變數依次新增在exports屬性指向的物件中。

2. 定義快取物件__webpack_module_cache__

模組快取物件__webpack_module_cache__最主要的作用就是將已經通過下面的__webpack_require__函式載入過的模組返回的值新增到自己物件中,下次再通過__webpack_require__函式載入模組的時候就直接返回結果,避免模組的重複載入。

3. 定義用於載入模組核心函式__webpack_require__

 var __webpack_module_cache__ = {}; 
 
 function __webpack_require__(moduleId) {
	 /* 
		判斷模組快取物件中是否存在當前要載入的模組:
		如果已經載入,則直接從__webpack_module_cache__物件中取出值返回
		如果值為undefined表示沒有載入,則繼續執行後面程式碼
	*/
 	var cachedModule = __webpack_module_cache__[moduleId];
 	if (cachedModule !== undefined) {
 		return cachedModule.exports;
 	}
	
	/* 
		核心步驟:物件的連續賦值
		1. 宣告module變數並賦值為{exports: {}}
		2. 給快取物件中新增一個屬性,屬性名為唯一的模組ID也就是模組路徑,屬性值為{exports: {}}
		重點在於將module和__webpack_module_cache__[moduleId]指向了同一個物件,也就是同一個記憶體地址,所以其中任意一個操作改變了物件中exports屬性的值,另外一個會感知到。
	 */
 	var module = __webpack_module_cache__[moduleId] = {
 		exports: {}
 	};

	/* 
		核心步驟:載入和執行模組中程式碼
		1. 讀取模組程式碼:通過__webpack_modules__[moduleId]可以讀取到一個函式,這個函式中包裹著模組中的程式碼。
		2. 執行模組程式碼:執行上一步中讀取到的函式並執行,執行的同時傳入三個引數module, module.exports, __webpack_require__,這裡暫時只用到第一個module物件,其餘兩個涉及到模組的交叉引用的時候才會用到。
		3. 執行模組程式碼完成之後,就會為module物件中的exports引數指定一個物件作為值,物件裡面存放著模組要匯出的變數名或者說介面名。
	 */
 	__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
	
	/* 將上一步執行後的module.exports = {sum:fn,mul:fn}匯出 */
 	return module.exports;
 }

4. 啟動執行函式

/* 
	!function(){}()是將函式變為表示式的寫法,等於是一個立即執行函式
	 
	 上面的程式碼都是函式或者變數的定義,這裡才是真正載入模組的邏輯開始的地方,原理很簡單就是執行載入模組核心函式__webpack_require__並將模組的路徑也就是moduleID傳入,並得到__webpack_require__函式的返回結果也就是一個匯出介面的物件,如下:
	 {
		sum,
		mul,
	 }
*/
 ! function() {
	 /* 解構物件*/
 	const {
 		sum,
 		mul
 	} = __webpack_require__( "./src/js/CommonJS.js");

	/* 執行函式 */
 	console.log(sum(10, 20));
 	console.log(mul(10, 20));
 }();
 

webpack實現ES Module模組化原始碼分析

在utils.js中基於ES Module語法匯出兩個函式:

function sum (a,b) {
	return a+b;
}

function mul (a,b){
	return a*b;
}

export {
	sum,
	mul
}

在專案入口檔案main.js中匯入:

import {sum,mul} from "./js/ESModule.js";

console.log(sum(10,20));
console.log(mul(10,20));

1. 開啟嚴格模式

webpack在對ES Module的模組進行打包的時候,在打包之後生成的bundle.js檔案中還是由一個立即執行函式包裹,但是不同的是由於ES Module規定其內部預設開啟嚴格模式,所以打包之後的立即執行函式最頂端會宣告"use strict"代表當前採用嚴格模式。

2. 定義__webpack_modules__物件

同CommonJS處理方法,將模組的路徑當做key,將一個函式當做value,webpack處理ES Module和CommonJS模組的區別就在於這個函式內部的邏輯不一樣:

var __webpack_modules__ = ({
  		"./src/js/ESModule.js":
		/**
		 * @param __unused_webpack_module :對應呼叫時的module,值為{exports:{}}
		 * @param __webpack_exports__  :對應呼叫時的module.exports,值為{}
		 * @param __webpack_require__  :對應載入模組的核心函式,函式也是一個物件,上面掛載o、r、d三個方法
		 * 
		 * */
  			function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
				
				/* 
					呼叫__webpack_require__函式物件上的r方法
					將最終module.exports匯出的{}標記為一個ES Module
				 */
  				__webpack_require__.r(__webpack_exports__);

				/*
					呼叫__webpack_require__函式物件上的d方法
					將最終module.exports匯出的{}做一層代理,代理過後在外部呼叫這個物件上的屬性的時候,就會執行該屬性對應的getter方法,getter方法的返回值才是最終讀取該屬性的值,也就是下面定義好的要匯出的介面sum、mul等。
				 */
  				__webpack_require__.d(__webpack_exports__, {
  					"sum": function() {
  						return sum;
  					},
  					"mul": function() {
  						return mul;
  					},
  				});

				// 這是原本模組中要匯出的介面
  				function sum(a, b) {
  					return a + b;
  				}

  				function mul(a, b) {
  					return a * b;
  				}
  			})
  	};

3. 定義快取物件__webpack_module_cache__

var __webpack_module_cache__ = {};

4. 定義用於載入模組核心函式__webpack_require__

function __webpack_require__(moduleId) {
	var cachedModule = __webpack_module_cache__[moduleId];
	if (cachedModule !== undefined) {
		return cachedModule.exports;
	}
	
	var module = __webpack_module_cache__[moduleId] = {
		exports: {}
	};

	/* 
		執行__webpack_modules__物件中屬性為moduleId對應的函式,並依次傳入三個引數:
		1. module:{exports:{}}
		2. module.exports:空物件{}
		3. __webpack_require__:當前函式本身
		
		執行此函式的過程中做了兩件事:
		1. 為最終匯出物件打一個ES Module的標記
		2. 將要匯出的介面依次新增到匯出的物件上,然後做了一層代理
	 */
	__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

	/* 將經過上一步處理後的 module.exports物件返回 */
	return module.exports;
}

5. 給__webpack_require__函式物件新增d方法

d方法的作用是對

/* 立即執行函式的表示式寫法 */
! function() {
	
	/**
	 * @param exports     是最終module.exports匯出的{}
	 * @param definition  是一個物件,物件中的每一個key都是模組中需要匯出的變數名,變數值就是對應的變數值
	 * 
	 * */
	__webpack_require__.d = function(exports, definition) {
		for (var key in definition) {
			/* 
				o函式就是用來判斷當前物件是否存在某個屬性的
				如果definition物件中存在key並且最終module.exports匯出的物件不包含key
				那麼就對definition物件中的所有key值做一層代理
			 */
			if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
				
				/* 依次將匯出的介面新增到最終要匯出的物件exports上,這是比較核心的程式碼 */
				Object.defineProperty(exports, key, {
					// 呼叫defineProperty方法定義屬性時候不顯式宣告就預設為false,就會導致無法被迭代
					enumerable: true, 
					// 在訪問key屬性的時候,呼叫getter函式,對應的值就是一個個的函式或者js值
					get: definition[key]
				});
			}
		}
	};
}();

6. 給__webpack_require__函式物件新增r方法

r方法的本質是webpack對當前載入的模組做一個標記,記錄當前載入的模組是一個ES Module。

r方法不返回任何值,它只是將傳入的exports物件做一層標記,經過這個方法處理後的物件會被標記為一個ES Module,具體的實現就是呼叫toString的時候返回Module或者訪問物件的__esModule屬性會返回true。

! function() {
	/**
	 * @param exports 執行載入函式__webpack_require__時傳入的空物件,這個物件最終經過處理之後存放的就是要匯出給外部的變數
	 * 
	 * */
	__webpack_require__.r = function(exports) {
		
		/* 
			如果執行此程式碼的環境支援Symbol,就將exports物件上的Symbol.toStringTag的內建屬性值定義為'Module',這樣做的意義在於將一個物件呼叫toString方法的時候就會優先返回Module告訴這是一個ES Module
		 */
		if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, {
				value: 'Module'
			});
		}
		
		/* 
			如果執行此程式碼的環境不支援Symbol,那麼就直接將exports物件新增一個'__esModule'屬性並且將其值設定為true,作用一樣都是記錄這是一個ES Module
		*/
		Object.defineProperty(exports, '__esModule', {
			value: true
		});
	};
}();

7. 給__webpack_require__函式物件新增o方法

o方法的作用是一個輔助函式,用於檢測物件中是否存在某個屬性,如果存在返回true,否則返回false。
其實Object.prototype.hasOwnProperty.call(obj, prop)這種寫法的另外一個寫法就是:obj.hasOwnProperty(prop);本質都是一樣用來檢測當前物件是否包含屬性prop的。

! function() {
	/**
	 * 
	 * @param obj 要檢測的物件
	 * @param prop 要檢測的屬性
	 * 
	 * */
	__webpack_require__.o = function(obj, prop) {
		return Object.prototype.hasOwnProperty.call(obj, prop);
	}
}();

8. 入口啟動函式

var __webpack_exports__ = {};
	
! function() {
	/* 給當前要載入的模組標記為ES Module */
	__webpack_require__.r(__webpack_exports__); 
		
	/* 執行模組載入函式__webpack_require__ */
	var _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/js/ESModule.js");
		
	/* 
		(0, _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.sum)(10, 20)
		這種寫法就等於:
		_js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.sum(10, 20) 
	*/
	console.log((0, _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.sum)(10, 20));
	console.log((0, _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.mul)(10, 20));
}();

9. 最終呈現

經過上述操作之後,一個ES Module經過webpack處理之後最終的呈現如下:

import * as demo from "./utils.js";
console.log(demo);
{
	c: 100
	mul: ƒ mul(a,b)
	sum: ƒ sum(a,b)
	__esModule: true  // 經過r方法打上的標記
	Symbol(Symbol.toStringTag): "Module" // 經過r方法打上的標記
	get c: ƒ ()  // 經過d方法實現的getter代理
	get mul: ƒ ()  // 經過d方法實現的getter代理
	get sum: ƒ ()  // 經過d方法實現的getter代理
}

webpack是實現在CommonJs模組中載入ES Module

  1. 搞清楚互相載入的到底是如何實現的
  2. CommonJS語法到底是怎樣的?
  3. ES Module和CommonJS的比較
  4. 前端模組化
  5. 再看看Vue Press部落格是如何搭建的