webpack 流程解析(2):引數初始化完成

csywweb 發表於 2021-10-14
Webpack

前言

上文說到 webpack 準備好了引數,要建立 compiler物件了。
建立完之後,則會執行 compiler.run 來開始編譯,本文將闡述 new Compilercompiler.run()中間的過程。
整體過程都發生在createCompiler這個函式體內。

/**
 * @param {WebpackOptions} rawOptions options object
 * @returns {Compiler} a compiler
 */
const createCompiler = rawOptions => {

new Compiler

  • 在new之前,webpack會完成一次基礎引數的初始化,這裡只給日誌輸出格式和context進行了賦值

    applyWebpackOptionsBaseDefaults(options);
  • webpack/lib/Compiler.js 是整個webpack的編譯核心流程。
  • new Compiler 的時候先在Tapable註冊了一堆鉤子,例如常見的watch-run,run, before-run, 等等。更多的鉤子可以在這裡檢視

初始化檔案操作

new NodeEnvironmentPlugin({
    infrastructureLogging: options.infrastructureLogging
}).apply(compiler);

這裡是在擴充compiler物件,增加對檔案的一些操作,例如輸入,輸出,監聽,快取等方法。同時還註冊了一個beforeRun鉤子的回撥。

apply(compiler) {
        const { infrastructureLogging } = this.options;
        compiler.infrastructureLogger = createConsoleLogger({
            level: infrastructureLogging.level || "info",
            debug: infrastructureLogging.debug || false,
            console:
                infrastructureLogging.console ||
                nodeConsole({
                    colors: infrastructureLogging.colors,
                    appendOnly: infrastructureLogging.appendOnly,
                    stream: infrastructureLogging.stream
                })
        });
        compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);
        const inputFileSystem = compiler.inputFileSystem;
        compiler.outputFileSystem = fs;
        compiler.intermediateFileSystem = fs;
        compiler.watchFileSystem = new NodeWatchFileSystem(
            compiler.inputFileSystem
        );
        compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
            if (compiler.inputFileSystem === inputFileSystem) {
                compiler.fsStartTime = Date.now();
                inputFileSystem.purge();
            }
        });
    }
這裡的fs,不是nodejs的 file system,是用了一個第三方包graceful-fs

註冊外掛

if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
}

接下來webpack會把options註冊的外掛,都註冊一遍。傳入compiler物件給外掛內部使用,外掛通過 compiler 提供的hook,可以在編譯全流程註冊鉤子的回撥函式。同時有些compiler鉤子又傳入了compilation物件,又可以在資源構建的時候註冊 compilation 鉤子回撥。

如何編寫一個webpack外掛

environment ready

外掛註冊完之後,webpack 又一次給options賦值了一次預設引數。為什麼和前面的applyWebpackOptionsBaseDefaults一起呢。
這裡呼叫了

applyWebpackOptionsDefaults(options);

又加了一波預設值。
加完之後呼叫了environmentafterEnvironment兩個鉤子。

compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

註冊內建外掛

環境初始化之後,webpack還需要執行一下它自己內部的預設外掛。

new WebpackOptionsApply().process(options, compiler);

這裡會根據你的配置,執行對應的外掛。
挑幾個和鉤子有關係的講講,

解析entry

new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry); 

這裡就是生成構建所需的entry資料結構

/** @type {EntryOptions} */
        const options = {
            name,
            filename: desc.filename,
            runtime: desc.runtime,
            layer: desc.layer,
            dependOn: desc.dependOn,
            publicPath: desc.publicPath,
            chunkLoading: desc.chunkLoading,
            wasmLoading: desc.wasmLoading,
            library: desc.library
        };

然後再呼叫EntryPluginapplay方法裡註冊Compiler.hooks:compilation, make 這兩個鉤子函式。將來等compiler物件裡面,觸發make鉤子的時候,在EntryPlugin註冊的回撥會觸發complition.addEntry(context, dep, options)開始編譯
這裡是重點,不然找不到開始的入口

compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) =>        {
        compilation.addEntry(context, dep, options, err => {
            callback(err);
        });
    }
);

註冊resloverFactory鉤子

webpack本身的一些外掛呼叫完成之後,會呼叫afterPlugin這個鉤子。

compiler.hooks.afterPlugins.call(compiler);

接下來 webpack 在 compiler.resolverFactory 上註冊了resolveOptions鉤子

compiler.resolverFactory.hooks.resolveOptions
    .for("normal")
    .tap("WebpackOptionsApply", resolveOptions => {
        resolveOptions = cleverMerge(options.resolve, resolveOptions);
        resolveOptions.fileSystem = compiler.inputFileSystem;
        return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
    .for("context")
    .tap("WebpackOptionsApply", resolveOptions => {
        resolveOptions = cleverMerge(options.resolve, resolveOptions);
        resolveOptions.fileSystem = compiler.inputFileSystem;
        resolveOptions.resolveToContext = true;
        return resolveOptions;
    });
compiler.resolverFactory.hooks.resolveOptions
        .for("loader")
        .tap("WebpackOptionsApply", resolveOptions => {
            resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
                resolveOptions.fileSystem = compiler.inputFileSystem;
                return resolveOptions;
        });

這裡的目的是為 Factory.createResolver 提供預設的引數物件(含有相關的 resolve 專案配置項)。
然後再呼叫afterResolvers鉤子

compiler.hooks.afterResolvers.call(compiler);

初始化完成

到目前為止,compiler 物件上已經有了足夠的東西開始我們的編譯,就告訴外界初始化完成,以webpack的調性,必然還有一個鉤子的觸發。

compiler.hooks.initialize.call();

結語

webpack 編譯前的所有事情已經交待清楚,下一篇將開始啟動編譯。
文章中提到的各種鉤子的註冊,煩請讀者記下來,後續在整個編譯過程中,前面註冊的一些鉤子,經常會在你遺漏的地方觸發,這也是除錯webpack過程中的一個痛點。