webpack4外掛及工作流程

嘻嘻xixi發表於2019-05-06

基礎篇可以參考webpack4基礎

webpack作者是c#出身,很多程式碼是OOP模式,可以借鑑下

Tapable

webpack4重寫了Tapable, 是webpack的外掛組織的核心。它提供給各個外掛鉤子,在事件觸發時執行這些掛載的方法。webapck的外掛裡必須有apply()方法,當其被呼叫的時候webpack將鉤子上的方法掛載到各個事件下面有點像nodejsEventEmitter$on

class Car {
	constructor() {
		this.hooks = {
			accelerate: new SyncHook(["newSpeed"]),
			brake: new SyncHook(),
			calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
		};
	},
    setSpeed(newSpeed) {
		this.hooks.accelerate.call(newSpeed);
	}
}
複製程式碼

如上程式碼所示先是在例項化的過程中註冊了三個鉤子函式,在例項上呼叫方法時觸發鉤子函式。 下面介紹webpack裡主要的6個Tapable的例項, 它們都繼承了Tapable,定義了一些自己的hook

Compiler

最高層的例項,初始化配置,提供全域性性的鉤子比如done, compilation。其他的Tapable例項需要通過其訪問,如

    compiler.hooks.compilation.tap(
      "myFirstWebpackPlugin",
      (compilation, params) => {
        compilation.hooks.seal.tap()
      }
    );
複製程式碼

Compilation

Compiler建立,整個構建就在這裡完成,進行依賴圖構建,優化資源,渲染出runtime時的程式碼等。下面的4個例項都是發生在這個階段。

Resolver

當你請求一個模組的時候,你將模組名或者相對地址發給模組解析器,它會去解析出絕對地址去尋找那個模組,看是否存在,如果存在則返回相應的模組資訊,包括上下文等。這裡的請求可以類似網路請求一樣攜帶上查詢引數之類的,Resolver將會返回額外資訊。webpack4裡將Resolver這個例項抽出來單獨發了一個包enhanced-resolve, 抽象出來可以便於使用者實現自己的Resolver

ModuleFactory

模組工廠就是負責構造模組的例項,介紹兩種NormalModuleFactoryContextModuleFactory。兩者不同的地方在於後者用於解析動態import(). 模組工廠主要是用於將Resolver解析成功的請求裡的原始碼從檔案中拿出,在記憶體中建立一個模組物件(NormalModule)

Parser

Parser主要用於將程式碼解析成AST抽象語法?.可以在ast檢視程式碼轉換成AST後的樣子。webpack預設採用acorn解析器,babel是babylonParserModuleFactory返回的物件裡的程式碼字串轉換成AST後進行解析,發現import或者require或者define類似模組引用時會將這些引用資訊也就是依賴新增到當前模組的物件裡,這樣每個模組物件裡不但有自己模組的資訊還包含它的依賴資訊。webpack會在不僅僅會在模組宣告處觸發事件,它甚至會在解析到變數時也觸發事件。如下在webpack/lib/Parser.js裡可以看到如下三個鉤子函式

    varDeclaration: new HookMap(() => new SyncBailHook(["declaration"])),
    varDeclarationLet: new HookMap(() => new SyncBailHook(["declaration"])),
    varDeclarationConst: new HookMap(() => new SyncBailHook(["declaration"])),
複製程式碼

Template

負責生成執行時的程式碼

// 原始碼
// index.js
    var multiply  = require('./multiply')
    var sum = (a,b)=>{
        return a+b;
    }
    module.exports = sum;
// multiply.js
    module.exports = (a, b) => a*b

// 生成的runtime
[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
        var multiply  = __webpack_require__(1)
        var sum = (a,b)=>{
            return a+b;
        }
        module.exports = sum;
/***/ }),
/* 1 */
/***/ (function(module, exports) {
        module.exports = (a, b) => a*b
/***/ })
];
複製程式碼

如上面程式碼所示,裡面包含三個模板,分別負責chunk, module, dependency. chunk是包含多個模組的陣列,就是外面陣列的形式;module就是裡面用立即執行函式包圍的部分;dependency就是將原先import, require等引用模組部分轉換成 __webpack_require__.

工作流程

目前只看到模組構建那部分,後續再補充╮(╯_╰)╭。。。心得就是利用好vscode的除錯工具多打斷點~~

介紹完了這六個例項,下面大致講下webpack的工作流程,webpack做的工作非常多,這裡只挑主要的講下。括號裡的是原始碼所在的檔案位置,上下文是node_modules/webpack. 本流程基於webpack4.30.0版本。

  1. 首先是將配置檔案讀入,webpack4有預設配置options = new WebpackOptionsDefaulter().process(options);會以使用者的配置為先。Compiler進行建立compiler = new Compiler(options.context);將配置裡的plugin部分進行繫結呼叫
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
複製程式碼

進行其他配置設定compiler.options = new WebpackOptionsApply().process(options, compiler); (lib/webpack.js) 2. 接著根據打包的目標(web, node, electron等)生成不同的打包模板

    switch (options.target) {
        case "web":
            JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
            ...
            break;
        case "webworker":
            ...
複製程式碼

因為瀏覽器端請求非同步載入的模組會類似jsonp插入dom中<script>標籤,而比如node端是沒有dom結構的。

    new EntryOptionPlugin().apply(compiler);
    compiler.hooks.entryOption.call(options.context, options.entry);
複製程式碼

這部分是將入口處配置新增呼叫entryOption鉤子。 (lib/WebpackOptionsApply.js) 3. 根據不同介面型別呼叫不同的類,webpack裡到處都是類

// lib/EntryOptionPlugin.js
    if (typeof entry === "string" || Array.isArray(entry)) {
        itemToPlugin(context, entry, "main").apply(compiler);
    } else if (typeof entry === "object") {
        for (const name of Object.keys(entry)) {
            itemToPlugin(context, entry[name], name).apply(compiler);
        }
    } else if (typeof entry === "function") {
        new DynamicEntryPlugin(context, entry).apply(compiler);
    }
複製程式碼

這裡舉例是單檔案入口, 在compilation鉤子上繫結(即Compiler建立compilation後呼叫)回撥,指定當前依賴的模組生成方法。

    compiler.hooks.compilation.tap(
        "SingleEntryPlugin",
        (compilation, { normalModuleFactory }) => {
            compilation.dependencyFactories.set(
                SingleEntryDependency,
                normalModuleFactory
            );
        }
    );
複製程式碼

(lib/SingleEntryPlugin.js) 4. 建立compilation (lib/Compiler.js).

    const compilation = this.createCompilation();
複製程式碼
// lib/SingleEntryPlugin.js
    compiler.hooks.make.tapAsync(
        "SingleEntryPlugin",
        (compilation, callback) => {
            const { entry, name, context } = this;
            const dep = SingleEntryPlugin.createDependency(entry, name);
            compilation.addEntry(context, dep, name, callback);
        }
    );
複製程式碼

上面這段是之前註冊的,但是會在compilation建立完成前呼叫,是個非同步鉤子。compilation建立好後傳入,它會將入口建立一個依賴。 5. 開始執行addEntry()方法,在addEntry方法裡呼叫_addModuleChain,將當前入口檔案建立模組moduleFactory.create,模組建立好後處理當前模組的依賴項this.processModuleDependencies. 將依賴建立模組後再依次解析模組的依賴。(lib/Compilation.js)

參考資料

主要還是Sean的課程 webpack-plugins

contribute-to-webpack

相關文章