看看webpack都打出了些什麼

鼓延發表於2018-04-19

最近在看webpack的原理,覺得可以分為兩個方面來完成:

  • 瞭解webpack打包出來的檔案。
  • 瞭解webpack流程並且自己寫loader和plugin。

當然看原始碼是可以的,但是有點事倍功半並且沒有必要,個人覺得完成以上兩部分就可以對webpack有不錯的瞭解了。本文主要關於webpack打包出來的檔案的內容【希望能夠提出不對或者可以補充的地方,感覺說的不是很清晰,歡迎指tu正cao】。

配置以及待打包檔案如下:

// webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        bundle1: path.resolve(__dirname, 'src/index1.js'),
        bundle2: path.resolve(__dirname, 'src/index2.js')
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        })
    ]
};
複製程式碼
// index1.js
const test1 = require('./test1');
const test3 = require('./test3')
console.log(test1);
console.log(test3);

// test1.js
const str = 'test1 is loaded';
module.exports = str;

// test3.js
const str = 'test3 is loaded';
module.exports = str;

// index2.js
setTimeout(function() {
    require.ensure([], function() {
        const test2 = require('./test2');
        console.log(test2);
    });
}, 5000);

// test2.js

const str = 'test2 is async loaded';
module.exports = str;
複製程式碼

module和chunk

首先了解module和chunk的概念:

  • module 其實就是打包前,import 或者 require 的js 檔案,如test1.js 與 index1.js。
  • chunk 是打包後的檔案,即 bundle1.js、bundle2.js、0.js和manifest.js檔案,這裡需要注意 一個 chunk 可能包含若干 module

三個核心的方法

打包結果檔案(簡化版本):

// manifest.js
(function(modules) { // webpackBootstrap
	// install a JSONP callback for chunk loading
	var parentJsonpFunction = window["webpackJsonp"];
	window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
		// add "moreModules" to the modules object,
		// then flag all "chunkIds" as loaded and fire callback
		var moduleId, chunkId, i = 0, resolves = [], result;
		for(;i < chunkIds.length; i++) {
			chunkId = chunkIds[i];
			if(installedChunks[chunkId]) {
				resolves.push(installedChunks[chunkId][0]);
			}
			installedChunks[chunkId] = 0;
		}
		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()();
		}
		if(executeModules) {
			for(i=0; i < executeModules.length; i++) {
				result = __webpack_require__(__webpack_require__.s = executeModules[i]);
			}
		}
		return result;
	};
	// The module cache
	var installedModules = {};
	// objects to store loaded and loading chunks
	var installedChunks = {
		3: 0
	};
	// 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,
			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;
	}
	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__webpack_require__.e = function requireEnsure(chunkId) {
		var installedChunkData = installedChunks[chunkId];
		if(installedChunkData === 0) {
			return new Promise(function(resolve) { resolve(); });
		}
		// a Promise means "currently loading".
		if(installedChunkData) {
			return installedChunkData[2];
		}
		// setup Promise in chunk cache
		var promise = new Promise(function(resolve, reject) {
			installedChunkData = installedChunks[chunkId] = [resolve, reject];
		});
		installedChunkData[2] = promise;
		// start chunk loading
		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 + ".js";
		var timeout = setTimeout(onScriptComplete, 120000);
		script.onerror = script.onload = onScriptComplete;
		function onScriptComplete() {
			// avoid mem leaks in IE.
			script.onerror = script.onload = null;
			clearTimeout(timeout);
			var chunk = installedChunks[chunkId];
			if(chunk !== 0) {
				if(chunk) {
					chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
				}
				installedChunks[chunkId] = undefined;
			}
		};
		head.appendChild(script);
		return promise;
	};
})
([]);
複製程式碼

manifest.js 先執行注入了一些方法,下面三個是最核心的方法:

  • webpackJsonp
  • webpack_require
  • webpack_require.e

這裡只大致的說下大致的作用以及重點的部分。

webpackJsonp方法,接受三個引數chunkIds, moreModules, executeModules。這裡要分清楚chunk id和module id,chunk id指的是一個打包後檔案的標示,而module id是每個打包前的module的唯一標示也就是id。這裡需要分別用來表示各個chunk和module以及在之後的快取過程中使用到。

chunkIds指的是這個chunk檔案載入後需要被載入的chunk id的陣列,所以預設會有自身chunk的id。如果有這個chunk會用到的module打包到的chunk需要被預載入的話,對應的chunk的id也會在chunkIds中。

moreModules指的是這個chunk載入後帶來的module的陣列,其中的每個module被以函式的形式包裹實現作用域上的隔離,其實和node的模組載入的機制很像。

以bundle1.js為例

webpackJsonp([1],[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

const test1 = __webpack_require__(1);
const test3 = __webpack_require__(2)

console.log(test1);
console.log(test3);

/***/ }),
/* 1 */
/***/ (function(module, exports) {

const str = 'test1 is loaded';

module.exports = str;

/***/ }),
/* 2 */
/***/ (function(module, exports) {

const str = 'test3 is loaded';

module.exports = str;

/***/ })
],[0]);
複製程式碼

chunkIds是[1],moreModules是中間的陣列引數,executeModules是[0](這裡要分清楚,[1]中的1是chunk id,而[0]中的0是module id 指的是需要被執行的module的id這裡指的就是打包之前的index.js檔案)這裡執行了bundle1.js相當於執行了打包之前的index1.js檔案。

moreModules陣列中的元素舉個例子:

/* 0 */
/***/ (function(module, exports, __webpack_require__) {

const test1 = __webpack_require__(1);
const test3 = __webpack_require__(2)

console.log(test1);
console.log(test3);

/***/ })
複製程式碼

每個module接收三個引數,第三個引數可選(取決於該module是否依賴其他module,稍後說明)其中的module和exports是在開發時的模組匯出中經常遇到的。我們在匯出一個模組的時候的操作:

module.exports = balabala;
複製程式碼

Javascript的函式的引數傳遞是按值來傳遞的。在函式執行的stack中當變數的型別是物件(引用型別值)時儲存的是這個物件的地址,真正的物件儲存在這個地址指向的堆(heap)中。函式的引數在按值傳遞情況下當引數型別是物件的時候傳遞的是物件的地址,這樣操作的時候指向的是heap中的同一個物件。

所以也就是變相的將module中export的內容掛載到了module.export物件上了。在打包後的程式碼可以看到,我們只要執行moreModules陣列中對應的元素的函式,就能夠變相的將這個module想要export的內容掛載到輸入到函式的module的export物件上。

在介紹完module的接收的三個引數後。我們可以看到函式內部當需要引用某個module的時候,會呼叫__webpack_require__方法參入對應的module的id。

// 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,
		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;
}
複製程式碼

__webpack_require__方法比較簡單,其實就是傳入moduleId首先判斷下installedModules中是否有快取(也就是之前載入過),有的話直接返回輸出的內容,沒有的話,就執行modules[moduleId].call(module.exports, module, module.exports, webpack_require);將module輸出的內容掛載到module.exports物件上,同時快取到installedModules中,結果就是:

每個module只會在最開始依賴到的時候載入一次,之後會從installedModules直接獲取不在載入。如果module依賴的module繼續依賴其他module的話,上述的過程會遞迴的執行下去,但是載入過的依賴值會載入一次。

這裡可以看到如果依賴的module被打包到獨立的chunk,並且這個chunk還沒有被執行的話,這個時候modules[moduleId]就是undefined了。我覺得這個時候就和webpackJsonp的第一個引數有關了,也就是這個chunk依賴的chunk的id需要在chunkIds引數中。這裡牽扯到另一個概念“迴圈依賴”的處理,這個打算之後再另一篇文章中專門介紹。

簡單來說__webpack_require__的作用就是載入執行對應的module,並且快取起來。

在瞭解了__webpack_require__後,回頭看下每個chunk都有的IIFE的對應的webpackJsonp方法,webpackJsonp做的事情其實簡單來說:

  • 標記了每個chunk是否載入過了。
  • 快取了每個載入的chunk帶來的module到modules物件中。
  • 將需要非同步載入的chunk的回撥(promise的resolve)統一收集並且執行。(這部分具體的過程感覺理解的還不是很透徹)
  • 按順序載入executeModules中的module,載入的過程就已經執行了對應的module,返回最後的module的執行結果。

這裡還需要注意**webpack_require.e**方法,這個方法對應的是require.ensure方法,這個方法的作用是載入chunk,也就是對應的module會被打包成獨立的chunk。在執行require.ensure回撥中的方法的時候之前會下載對應的chunk,從而實現chunk的按需載入。

從__webpack_require__.e的程式碼中可以看到,ta大致的思想是:判斷對應的chunk是否已經載入過了,如果已經載入過了,就return一個resolve了的promise,然後執行對應的回撥函式的內容。如果chunk沒有載入過則用動態新增script標籤的方式載入對應的chunk(所以這裡的方法名叫做webpackJsonp也是有道理的,非同步載入的方式和jsonp的方法有類似的地方)。然後標註chunk已載入,這裡還對如果chunk下載失敗的情況下拋錯警告chunk載入失敗。

總結

至此算是大致的介紹了webpack打包後檔案的內容,可以看到webpack打包後的檔案,自己實現了一套模組載入的機制,這樣方便實現比如程式碼分割等功能。

在瞭解了webpack的打包之後檔案的結構,我們並知道webpack的打包過程依靠不同 loader 和 plugin 組合來進行。loader負責將不同型別的檔案進行轉換成js型別的資料,plugin在打包過程中註冊對應的事件,webpack在對應的階段執行不同外掛來實現對檔案的處理。

在知道了這些之後,實現loader和plugin來真正的使用webpack。

內部分享的時候的 Slide

參考資料:

相關文章