webpack的編譯&構建

騰訊IVWEB團隊發表於2019-03-02

上一篇文章webpack詳解中介紹了webpack基於事件流程式設計,是個高度的外掛集合,整體介紹了webpack 的編譯流程。本文將單獨聊一聊最核心的部分,編譯&構建。

webpack的編譯

重要的構建節點

webpack的構建中總會經歷如下幾個事件節點。

  • before-run 清除快取
  • run 註冊快取資料鉤子
  • compile 開始編譯
  • make 從入口分析依賴以及間接依賴模組,建立模組物件
  • build-module 模組構建
  • seal 構建結果封裝, 不可再更改
  • after-compile 完成構建,快取資料
  • emit 輸出到dist目錄

其中make是整個構建中最核心的部分編譯,通過模組工廠函式建立模組,然後對模組進行編譯。

在make鉤子的編譯

Alt text
上圖中提到的*ModuleFactory是指模組工廠函式,之所以會有模組工廠這樣的函式,還要從webpack中entry的配置說起,在webpack的配置項中entry支援如下型別:

  • 字元型別string
  • 字元陣列型別[string]
  • 多頁面物件key-value型別object { <key>: string | [string] }
  • 也支援一個函式,返回構建的入口(function: () => string | [string] | object { <key>: string | [string] })

為了處理以後不同型別的入口模組,所以就需要個模組工廠來處理不同的入口模組型別。

  • singleEntry: string|object { <key>: string }
  • multiEntry: [string]|object { <key>: [string] }
  • dynamicEntry: (function: () => string | [string] | object { <key>: string | [string] })

上圖中為了簡單說明構建的流程,就以最直接的singleEntry型別說起,對於此類入口模組,webpack均使用NormalModuleFactory來建立模組,這個建立的模組的型別叫NormalModule,在NormalModule中實現了模組的構建方法build,使用runLoaders對模組進行載入,然後利用進行解析,分析模組依賴,遞迴構建。

構建封裝seal

到構建封裝階段時候,程式碼構建已經完畢,但是如何將這些程式碼按照依賴引用邏輯組織起來,當瀏覽器將你構建出來的程式碼載入到瀏覽器的時候,仍然能夠正確執行。在webpack中通過Manifest記錄各個模組的詳細要點,通過Runtime來引導,載入執行模組程式碼,特別是非同步載入。

###Runtime 如上所述,我們這裡只簡略地介紹一下。runtime,以及伴隨的 manifest 資料,主要是指:在瀏覽器執行時,webpack 用來連線模組化的應用程式的所有程式碼。runtime 包含:在模組互動時,連線模組所需的載入和解析邏輯。包括瀏覽器中的已載入模組的連線,以及懶載入模組的執行邏輯。

###Manifest 那麼,一旦你的應用程式中,形如 index.html 檔案、一些 bundle 和各種資源載入到瀏覽器中,會發生什麼?你精心安排的 /src 目錄的檔案結構現在已經不存在,所以 webpack 如何管理所有模組之間的互動呢?這就是 manifest 資料用途的由來…… 當編譯器(compiler)開始執行、解析和對映應用程式時,它會保留所有模組的詳細要點。這個資料集合稱為 "Manifest",當完成打包併傳送到瀏覽器時,會在執行時通過 Manifest 來解析和載入模組。無論你選擇哪種模組語法,那些 import 或 require 語句現在都已經轉換為 webpack_require 方法,此方法指向模組識別符號(module identifier)。通過使用 manifest 中的資料,runtime 將能夠查詢模組識別符號,檢索出背後對應的模組。

定義了一個立即執行函式,宣告瞭__webpack_require__,對各種模組進行載入。

(function(modules) { // webpackBootstrap
    var installedModules = {}; // cache module
    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;
    }

    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;

    // expose the module cache
    __webpack_require__.c = installedModules;

    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
        if (!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter
            });
        }
    };

    // 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;
    };

    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

    // __webpack_public_path__
    __webpack_require__.p = "";

    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})([/**modules*/])
複製程式碼

上面提到的程式碼片段便是webpack構建後在瀏覽器中執行的引導程式碼。也就是上面提到的runtime。它是個立即執行函式,那麼入參modules便是上面的Manifest,組織各個模組的依賴邏輯。

(function(modules){
	// ...
	// runtime
	function __webpack_require__(moduleId) {
		// 載入邏輯
	}
	// ...
})([function (module, exports, __webpack_require__) {

    var chunk1 = __webpack_require__(1);
    var chunk2 = __webpack_require__(2);
   
}, function (module, exports, __webpack_require__) {

    __webpack_require__(2);
    var chunk1 = 1;
    exports.chunk1 = chunk1;
}, function (module, exports) {

    var chunk2 = 1;
    exports.chunk2 = chunk2;
}])
複製程式碼

上面說到了runtimemanifest就是在seal階段注入的

class Compilation extends Tapable {
   
    seal(callback) {
        this.hooks.seal.call();
        // ...
        if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
            this.hooks.beforeChunkAssets.call();
            this.createChunkAssets();
        }
        // ...
    }
  
    createChunkAssets() {
	    // ...
		for (let i = 0; i < this.chunks.length; i++) {
			const chunk = this.chunks[i];
			// ...
				const template = chunk.hasRuntime() 
					? this.mainTemplate
					: this.chunkTemplate; // 根據是否有runTime選擇模組,入口檔案是true, 需要非同步載入的檔案則沒有
				const manifest = template.getRenderManifest({ 
					// 生成manifest
					chunk,
					hash: this.hash,
					fullHash: this.fullHash,
					outputOptions,
					moduleTemplates: this.moduleTemplates,
					dependencyTemplates: this.dependencyTemplates
				}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
				// ...
    }
}
複製程式碼

通過template最後將程式碼組織起來,上面看到的構建後的程式碼就是mainTemplate生成的。

寫在最後

通過template生成最後程式碼,構建已經完成,接下來就是將程式碼輸出到dist 目錄。

最後

騰訊IVWEB團隊的工程化解決方案feflow已經開源:Github主頁:https://github.com/feflow/feflow

相關文章