webpack打包原理解析

webpack打包是如何運行的

  • 也可以稱爲,webpack是如何實現模塊化的
  • CommonJS是同步加載模塊,一般用於node。因爲node應用程序運行在服務器上,程序通過文件系統可以直接讀取到各個模塊的文件,特點是響應快速,不會因爲同步而阻塞了程序的運行;
  • AMD是異步加載模塊,所以普遍用於前端。而前端項目運行在瀏覽器中,每個模塊都要通過http請求加載js模塊文件,受到網絡等因素的影響如果同步的話就會使瀏覽器出現“假死”(卡死)的情況,影響到了用戶體驗。
  • ESModule 旨在實現前後端模塊化的統一。而webpack就是把ES6的模塊化代碼轉碼成CommonJS的形式,從而兼容瀏覽器的。
  • 爲什麼webpack打包後的文件,可以用在瀏覽器:此時webpack會將所有的js模塊打包到bundle.js中(異步加載的模塊除外,異步模塊後面會講),讀取到了內存裏,就不會再分模塊加載了。
  • webpack將代碼打包成什麼樣子

webpack對CommonJS的模塊化處理

  • 舉例:
    • index.js文件,引入foo.js文件
    const foo = require('./foo');
    
    console.log(foo);
    console.log('我是高級前端工程師~');
    
    • foo.js文件
    module.exports = {
      name: 'quanquan',
      job: 'fe',
    };
    
  • 當我們執行webpack之後,打包完成,可以看到bundle.js內的代碼
// modules 即爲存放所有模塊的數組,數組中的每一個元素都是一個函數
(function(modules) {
	// 安裝過的模塊都存放在這裏面
    // 作用是把已經加載過的模塊緩存在內存中,提升性能
	var installedModules = {};
	// 去數組中加載一個模塊,moduleId 爲要加載模塊在數組中的 index
    // __webpack_require__作用和 Node.js 中 require 語句相似
	function __webpack_require__(moduleId) {
		// require 模塊時先判斷是否已經緩存, 已經緩存的模塊直接返回
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		// 如果緩存中不存在需要加載的模塊,就新建一個模塊,並把它存在緩存中
		var module = installedModules[moduleId] = {
            // 模塊在數組中的index
            i: moduleId,
            // 該模塊是否已加載完畢
            l: false,
            // 該模塊的導出值,也叫模塊主體內容, 會被重寫
			exports: {}
		};
		// 從 modules 中獲取 index 爲 moduleId 的模塊對應的函數
        // 再調用這個函數,同時把函數需要的參數傳入,this指向模塊的主體內容
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		// 將模塊標記爲已加載
		module.l = true;
		// 返回模塊的導出值,即模塊主體內容
		return module.exports;
	}
    // 向外暴露所有的模塊
	__webpack_require__.m = modules;
	// 向外暴露已緩存的模塊
    __webpack_require__.c = installedModules;
	
	...
	...
	
    // Webpack 配置中的 publicPath,用於加載被分割出去的異步代碼,這個暫時還沒有用到
	__webpack_require__.p = "";
    // Load entry module and return exports
    // 準備工作做完了, require 一下入口模塊, 讓項目跑起來
    // 使用 __webpack_require__ 去加載 index 爲 0 的模塊,並且返回該模塊導出的內容
    // index 爲 0 的模塊就是 index.js文件,也就是執行入口模塊
    // __webpack_require__.s 的含義是啓動模塊對應的 index
	return __webpack_require__(__webpack_require__.s = 0);
})
/***** 華麗的分割線 上邊時 webpack 初始化代碼, 下邊是我們寫的模塊代碼 *******/
// 所有的模塊都存放在了一個數組裏,根據每個模塊在數組的 index 來區分和定位模塊
([
	/* 模塊 0 對應 index.js */
	(function(module, exports, __webpack_require__) {
		// 通過 __webpack_require__ 規範導入 foo 函數,foo.js 對應的模塊 index 爲 1
		const foo = __webpack_require__(1);
		
		console.log(foo);
		console.log('我是高級前端工程師~');
	}),
	/* 模塊 1 對應 foo.js */
	(function(module, exports) {
		// 通過 CommonJS 規範導出對象
		module.exports = {
		  name: 'quanquan',
		  job: 'fe',
		};
	})
]);
  • 上面是一個立即執行函數,簡單點寫:
(function(modules) {

  // 模擬 require 語句
  function __webpack_require__(index) {
	  return [/*存放所有模塊的數組中,第index個模塊暴露的東西*/]
  }

  // 執行存放所有模塊數組中的第0個模塊,並且返回該模塊導出的內容
  return __webpack_require__(0);

})([/*存放所有模塊的數組*/])
  • bundle.js 能直接運行在瀏覽器中的原因在於:
    • webpack通過 _webpack_require_ 函數(該函數定義了一個可以在瀏覽器中執行的加載函數)模擬了模塊的加載(類似於Node.js 中的 require 語句),把定義的模塊內容掛載到module.exports上;
    • 同時__webpack_require__函數中也對模塊緩存做了優化,執行加載過的模塊不會再執行第二次,執行結果會緩存在內存中,當某個模塊第二次被訪問時會直接去內存中讀取被緩存的返回值。
  • 原來一個個獨立的模塊文件被合併到了一個單獨的 bundle.js 的原因在於,瀏覽器不能像 Node.js 那樣快速地去本地加載一個個模塊文件,而必須通過網絡請求去加載還未得到的文件。 如果模塊數量很多,加載時間會很長,因此把所有模塊都存放在了數組中,執行一次網絡加載。

webpack對es6 Module模塊化的處理

  • 舉例
    • index.js文件,引入foo.js文件
    const foo = require('./foo');❎
    import foo from './foo';✅
    
    console.log(foo);
    console.log('我是高級前端工程師~');
    
    • foo.js文件
    module.exports = {❎
    export default {✅
      name: 'quanquan',
      job: 'fe',
    };
    
  • 打包完後bundle.js代碼如下
(function(modules) {
	var installedModules = {};
	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;
	}
	__webpack_require__.m = modules;
	__webpack_require__.c = installedModules;
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, {
				configurable: false,
				enumerable: true,
				get: getter
			});
		}
	};
	__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__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
	__webpack_require__.p = "";
	return __webpack_require__(__webpack_require__.s = 0);
})([相關模塊]);
  • 打包好的內容和commonjs模塊化方法差不多
function(module, __webpack_exports__, __webpack_require__) {
	"use strict";
	// 在__webpack_exports__上定義__esModule爲true,表明是一個模塊對象
	Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
	var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);
	console.log(__WEBPACK_IMPORTED_MODULE_0__foo__["a"]);
	console.log('我是高級前端工程師~');
},
function(module, __webpack_exports__, __webpack_require__) {
	"use strict";
	__webpack_exports__["a"] = ({
		name: 'quanquan',
		job: 'fe',
	});
}
  • 和 commonjs 不同的地方
    • 首先, 包裝函數的參數之前的 module.exports 變成了_webpack_exports_
    • 其次, 在使用了 es6 模塊導入語法(import)的地方, 給__webpack_exports__添加了屬性__esModule
    • 其餘的部分和 commonjs 類似

webpack文件的按需加載

  • 以上webpack把所有模塊打包到主文件中,所以模塊加載方式都是同步方式。但在開發應用過程中,按需加載(也叫懶加載)也是經常使用的優化技巧之一。
  • 按需加載,通俗講就是代碼執行到異步模塊(模塊內容在另外一個js文件中),通過網絡請求即時加載對應的異步模塊代碼,再繼續接下去的流程。
  • 在給單頁應用做按需加載優化時,一般採用以下原則:
    • 把整個網站劃分成一個個小功能,再按照每個功能的相關程度把它們分成幾類。
    • 把每一類合併爲一個 Chunk,按需加載對應的 Chunk。
    • 對於用戶首次打開你的網站時需要看到的畫面所對應的功能,不要對它們做按需加載,而是放到執行入口所在的 Chunk 中,以降低用戶能感知的網頁加載時間。
    • 對於個別依賴大量代碼的功能點,例如依賴 Chart.js 去畫圖表、依賴 flv.js 去播放視頻的功能點,可再對其進行按需加載。
  • 被分割出去的代碼的加載需要一定的時機去觸發,也就是當用戶操作到了或者即將操作到對應的功能時再去加載對應的代碼。 被分割出去的代碼的加載時機需要開發者自己去根據網頁的需求去衡量和確定。
  • 由於被分割出去進行按需加載的代碼在加載的過程中也需要耗時,你可以預言用戶接下來可能會進行的操作,並提前加載好對應的代碼,從而讓用戶感知不到網絡加載時間。
  • 舉個例子
    • 網頁首次加載時只加載 main.js 文件,網頁會展示一個按鈕,main.js 文件中只包含監聽按鈕事件和加載按需加載的代碼。當按鈕被點擊時纔去加載被分割出去的 show.js 文件,加載成功後再執行 show.js 裏的函數。
    • main.js 文件
    window.document.getElementById('btn').addEventListener('click', function () {
     // 當按鈕被點擊後纔去加載 show.js 文件,文件加載成功後執行文件導出的函數
     import(/* webpackChunkName: "show" */ './show').then((show) => {
       show('Webpack');
     })
    });
    
    • show.js 文件
    module.exports = function (content) {
      window.alert('Hello ' + content);
    };
    
    • 代碼中最關鍵的一句是 import(/* webpackChunkName: “show” / ‘./show’),Webpack 內置了對 import() 語句的支持,當 Webpack 遇到了類似的語句時會這樣處理:
      • 以 ./show.js 爲入口新生成一個 Chunk;
      • 當代碼執行到 import 所在語句時纔會去加載由 Chunk 對應生成的文件。
      • import 返回一個 Promise,當文件加載成功時可以在 Promise 的 then 方法中獲取到 show.js 導出的內容。
  • webpack有個require.ensure api語法來標記爲異步加載模塊,最新的webpack4推薦使用新的import() api(需要配合@babel/plugin-syntax-dynamic-import插件)。

  • 因爲require.ensure是通過回調函數執行接下來的流程,而import()返回promise,這意味着可以使用最新的ES8 async/await語法,使得可以像書寫同步代碼一樣,執行異步流程。

按需加載輸出代碼分析

  • 舉例
    • main.js
    // main.js
    import Add from './add'
    console.log(Add, Add(1, 2), 123)
    
    // 按需加載
    // 方式1: require.ensure
    // require.ensure([], function(require){
    //     var asyncModule = require('./async')
    //     console.log(asyncModule.default, 234)
    // })
    
    // 方式2: webpack4新的import語法
    // 需要加@babel/plugin-syntax-dynamic-import插件
    let asyncModuleWarp = async () => await import('./async')
    console.log(asyncModuleWarp().default, 234)
    
    • async.js
    // async.js
    export default function() {
        return 'hello, aysnc module'
    }
    
  • 打包後會生成兩個chunk文件,分別是主文件執行入口文件 bundle.js 和 異步加載文件 0.bundle.js。
// 0.bundle.js
// 異步模塊
// window["webpackJsonp"]是連接多個chunk文件的橋樑
// window["webpackJsonp"].push = 主chunk文件.webpackJsonpCallback
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  [0], // 異步模塊標識chunkId,可判斷異步代碼是否加載成功
  // 跟同步模塊一樣,存放了{模塊路徑:模塊內容}
  {
  "./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {
      __webpack_require__.r(__webpack_exports__);
      __webpack_exports__["default"] = (function () {
        return 'hello, aysnc module';
      });
    })
  }
]);
  • 異步模塊打包後的文件中保存着異步模塊源代碼,同時爲了區分不同的異步模塊,還保存着該異步模塊對應的標識:chunkId。以上代碼主動調用window[“webpackJsonp”].push函數,該函數是連接異步模塊與主模塊的關鍵函數,該函數定義在主文件中,實際上window[“webpackJsonp”].push = webpackJsonpCallback,詳細源碼咱們看看主文件打包後的代碼bundle.js:
(function(modules) {
	// 獲取到異步chunk代碼後的回調函數
	// 連接兩個模塊文件的關鍵函數
	function webpackJsonpCallback(data) {
	  var chunkIds = data[0]; //data[0]存放了異步模塊對應的chunkId
	  var moreModules = data[1]; // data[1]存放了異步模塊代碼
	
	  // 標記異步模塊已加載成功
	  var moduleId, chunkId, i = 0, resolves = [];
	  for(;i < chunkIds.length; i++) {
	    chunkId = chunkIds[i];
	    if(installedChunks[chunkId]) {
	      resolves.push(installedChunks[chunkId][0]);
	    }
	    installedChunks[chunkId] = 0;
	  }
	
	  // 把異步模塊代碼都存放到modules中
	  // 此時萬事俱備,異步代碼都已經同步加載到主模塊中
	  for(moduleId in moreModules) {
	    modules[moduleId] = moreModules[moduleId];
	  }
	
	  // 重點:執行resolve() = installedChunks[chunkId][0]()返回promise
	  while(resolves.length) {
	    resolves.shift()();
	  }
	};
	
	// 記錄哪些chunk已加載完成
	var installedChunks = {
	  "main": 0
	};
	
	// __webpack_require__依然是同步讀取模塊代碼作用
	function __webpack_require__(moduleId) {
	  ...
	}
	
	// 加載異步模塊
	__webpack_require__.e = function requireEnsure(chunkId) {
	  // 創建promise
	  // 把resolve保存到installedChunks[chunkId]中,等待代碼加載好再執行resolve()以返回promise
	  var promise = new Promise(function(resolve, reject) {
	    installedChunks[chunkId] = [resolve, reject];
	  });
	
	  // 通過往head頭部插入script標籤異步加載到chunk代碼
	  var script = document.createElement('script');
	  script.charset = 'utf-8';
	  script.timeout = 120;
	  script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js"
	  var onScriptComplete = function (event) {
	    var chunk = installedChunks[chunkId];
	  };
	  script.onerror = script.onload = onScriptComplete;
	  document.head.appendChild(script);
	
	  return promise;
	};
	
	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
	// 關鍵代碼: window["webpackJsonp"].push = webpackJsonpCallback
	jsonpArray.push = webpackJsonpCallback;
	
	// 入口執行
	return __webpack_require__(__webpack_require__.s = "./src/main.js");
	})
	({
	"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {...}),
	
	"./src/main.js": (function(module, exports, __webpack_require__) {
	  // 同步方式
	  var Add = __webpack_require__("./src/add.js").default;
	  console.log(Add, Add(1, 2), 123);
	
	  // 異步方式
	  var asyncModuleWarp =function () {
	    var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() {
	      return regeneratorRuntime.wrap(function _callee$(_context) {
	        // 執行到異步代碼時,會去執行__webpack_require__.e方法
	        // __webpack_require__.e其返回promise,表示異步代碼都已經加載到主模塊了
	        // 接下來像同步一樣,直接加載模塊
	        return __webpack_require__.e(0)
	              .then(__webpack_require__.bind(null, "./src/async.js"))
	      }, _callee);
	    }));
	
	    return function asyncModuleWarp() {
	      return _ref.apply(this, arguments);
	    };
	  }();
	  console.log(asyncModuleWarp().default, 234)
	})
});
  • webpack實現模塊的異步加載有點像jsonp的流程。

    • 在主js文件中通過在head中構建script標籤方式,異步加載模塊信息;
    • 再使用回調函數webpackJsonpCallback,把異步的模塊源碼同步到主文件中,所以後續操作異步模塊可以像同步模塊一樣。
  • 源碼具體實現流程:

    • 遇到異步模塊時,使用_webpack_require_.e函數去把異步代碼加載進來。該函數會在html的head中動態增加script標籤,src指向指定的異步模塊存放的文件。
    • 加載的異步模塊文件會執行webpackJsonpCallback函數,把異步模塊加載到主文件中。
    • 所以後續可以像同步模塊一樣,直接使用_webpack_require_("./src/async.js")加載異步模塊。
  • 這裏的 bundle.js 和上面所講的 bundle.js 非常相似,區別在於:

    • 多了一個 webpack_require.e 用於加載被分割出去的,需要異步加載的 Chunk 對應的文件;
    • 多了一個 webpackJsonp 函數用於從異步加載的文件中安裝模塊。
    • 在使用了 CommonsChunkPlugin 去提取公共代碼時輸出的文件和使用了異步加載時輸出的文件是一樣的,都會有 webpack_require.e 和 webpackJsonp。 原因在於提取公共代碼和異步加載本質上都是代碼分割。

總結

  • webpack對於ES模塊/CommonJS模塊的實現,是基於自己實現的webpack_require,所以代碼能跑在瀏覽器中。
  • 從 webpack2 開始,已經內置了對 ES6、CommonJS、AMD 模塊化語句的支持。但不包括新的ES6語法轉爲ES5代碼,這部分工作還是留給了babel及其插件。
  • 在webpack中可以同時使用ES6模塊和CommonJS模塊。因爲 module.exports很像export default,所以ES6模塊可以很方便兼容 CommonJS:import XXX from ‘commonjs-module’。反過來CommonJS兼容ES6模塊,需要額外加上default:require(‘es-module’).default。
  • webpack異步加載模塊實現流程跟jsonp基本一致。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章