webpack源碼執行過程分析,loader+plugins

webpack運行於node js之上,瞭解源碼的執行,不僅可以讓我們對webpack的使用更爲熟悉,更會增強我們對應用代碼的組織能力,

本篇文章重點從webpack核心的兩個特性loader,plugin,進行深入分析,

我們從一個例子出發來分析webpack執行過程,地址

我們使用 vscode 調試工具來對webpack進行調試,

首先我們從入口出發

"build":"webpack --config entry.js"

示例項目通過npm run build 進行啓動,npm run 會新建一個shell,並將 node_modules/.bin 下的所有內容加入環境變量,我們查看下.bin 文件夾下內容

webpack
webpack-cli
webpack-dev-server

可以看到webpack便在其中,
打開文件,可以看到文件頭部

#!/usr/bin/env node

使用node執行此文件內容,webpack 文件的主要內容是判斷webpack-cli或者webpack-command有沒有安裝,如果有安裝則執行對應文件內容,本例安裝了webpack-cli,所以通過對目標cli的require,進入到對應cli的執行,

webpack-cli
webpack-cli是一個自執行函數,對我們在命令行傳入的一些參數進行了解析判斷,核心內容是把webpack入口文件作爲參數,執行webpack,生成compiler

       try {
                compiler = webpack(options);
            } catch (err) {
                if (err.name === "WebpackOptionsValidationError") {
                    if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
                    else console.error(err.message);
                    // eslint-disable-next-line no-process-exit
                    process.exit(1);
                }

                throw err;
            }

生成compiler後,執行compiler.run()或者compiler.watch(),
本例未啓動熱更新所以執行的是 compiler.run()

            if (firstOptions.watch || options.watch) {
                const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
                if (watchOptions.stdin) {
                    process.stdin.on("end", function(_) {
                        process.exit(); // eslint-disable-line
                    });
                    process.stdin.resume();
                }
                compiler.watch(watchOptions, compilerCallback);
                if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
                if (compiler.close) compiler.close(compilerCallback);
            } else {
                compiler.run(compilerCallback);
                if (compiler.close) compiler.close(compilerCallback);
            }

既然已經知道核心是這兩個參數的執行,我們即可模擬一個webpack的執行過程,本例中,我們創建一個debug.js

const webpack = require('webpack');
const options = require('./entry.js');

const compiler = webpack(options);

我們在webpack()函數前面加上斷點,即可通過vscode開始debug
我們先對生成compiler過程進行分析,

webpack函數

const webpack = (options, callback) => {
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }
    let compiler;
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        options = new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin().apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        if (typeof callback !== "function") {
            throw new Error("Invalid argument: callback");
        }
        if (
            options.watch === true ||
            (Array.isArray(options) && options.some(o => o.watch))
        ) {
            const watchOptions = Array.isArray(options)
                ? options.map(o => o.watchOptions || {})
                : options.watchOptions || {};
            return compiler.watch(watchOptions, callback);
        }
        compiler.run(callback);
    }
    return compiler;
};

我們可以看到,有對options參數的驗證validateSchema(webpackOptionsSchema,options);
有對默認配置的合併 options = new WebpackOptionsDefaulter().process(options);
合併內容
然後對所有的plugins配置進行註冊操作

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

關於這裏的註冊,我們可以通過寫一個plugin來描述執行過程,
本例中我們新建一個testplugin文件,

testplugin

module.exports = class testPlugin{
    apply(compiler){
        console.log('註冊')
        compiler.hooks.run.tapAsync("testPlugin",(compilation,callback)=>{
            console.log("test plugin")
            callback()
        })
    }
}

關於插件的編寫,我們只需要提供一個類,prototype上含有apply函數,同時擁有一個compiler參數,之後通過tap註冊compiler上的hook,使得webpack執行到指定時機執行回調函數,具體編寫方法參考寫一個插件
本示例插件中,我們在compiler的run hook上註冊了testplugin插件,回調的內容爲打印 “test plugin”,並且,在註冊的時候我們會打印 ”註冊“,來跟蹤plugin的註冊執行流程,

回到webpack 函數,可以看到,進行完插件的註冊,就會執行兩個hook的回調,

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

這時,就會執行我們註冊在environment,afterEnvironment上的plugin的回調,其他插件的回調執行也是通過call或者callAsync 來觸發執行,webpack整個源碼執行過程中會在不同的階段執行不同的hook的call函數,所以,在我們編寫插件的過程中要對流程有些瞭解,從而將插件註冊在合適的hook上,

webpack函數的最後,就是執行compiler.run函數,我們在這裏加上斷點,進入compiler.run函數,

 this.hooks.beforeRun.callAsync(this, err => {
            if (err) return finalCallback(err);

            this.hooks.run.callAsync(this, err => {
                if (err) return finalCallback(err);

                this.readRecords(err => {
                    if (err) return finalCallback(err);

                    this.compile(onCompiled);
                });
            });
        });

compiler.run 函數中也是執行了一系列的hook,我們編寫的testplugin就會在this.hooks.run.callAsync處執行,關於plugin的註冊和運行具體細節,本篇先不講,只需知道註冊通過tap,運行通過call即可,,
到了這裏,基本的plugin的運行過程我們已經瞭解,接下來我們通過幾個目標來對loader的執行過程進行分析,

  1. 模塊如何匹配到的loader
  2. 模塊是如何遞歸的解析當前模塊引用模塊的
  3. loader是在哪裏執行的

回到源代碼,執行完一些hooks後,進入到compile,

compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            if (err) return callback(err);

            this.hooks.compile.call(params);

            const compilation = this.newCompilation(params);

            this.hooks.make.callAsync(compilation, err => {
                if (err) return callback(err);

                compilation.finish();

                compilation.seal(err => {
                    if (err) return callback(err);

                    this.hooks.afterCompile.callAsync(compilation, err => {
                        if (err) return callback(err);

                        return callback(null, compilation);
                    });
                });
            });
        });
    }

依舊是一些hooks的執行,重點是make 的hook,我們進入,make hook通過htmlWebpackPlugin註冊了一個回調,回調中又註冊了一個SingleEntryPlugin,然後又重新執行了make.callAsync,進入了SingleEntryPlugin的回調

compiler.hooks.make.tapAsync(
            "SingleEntryPlugin",
            (compilation, callback) => {
                const { entry, name, context } = this;

                const dep = SingleEntryPlugin.createDependency(entry, name);
                compilation.addEntry(context, dep, name, callback);
            }
        );

可以看到,主要執行了addEntry方法,addEntry中執行addEntry hook,然後調用_addModuleChain,

addEntry(context, entry, name, callback) {
        this.hooks.addEntry.call(entry, name);

        const slot = {
            name: name,
            // TODO webpack 5 remove `request`
            request: null,
            module: null
        };

        if (entry instanceof ModuleDependency) {
            slot.request = entry.request;
        }

        // TODO webpack 5: merge modules instead when multiple entry modules are supported
        const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
        if (idx >= 0) {
            // Overwrite existing entrypoint
            this._preparedEntrypoints[idx] = slot;
        } else {
            this._preparedEntrypoints.push(slot);
        }
        this._addModuleChain(
            context,
            entry,
            module => {
                this.entries.push(module);
            },
            (err, module) => {
                if (err) {
                    this.hooks.failedEntry.call(entry, name, err);
                    return callback(err);
                }

                if (module) {
                    slot.module = module;
                } else {
                    const idx = this._preparedEntrypoints.indexOf(slot);
                    if (idx >= 0) {
                        this._preparedEntrypoints.splice(idx, 1);
                    }
                }
                this.hooks.succeedEntry.call(entry, name, module);
                return callback(null, module);
            }
        );
    }

然後_addModuleChain中通過moduleFactory.create 創建modeuleFactory對象,然後執行buildModule

this.buildModule(module, false, null, null, err => {
                            if (err) {
                                this.semaphore.release();
                                return errorAndCallback(err);
                            }

                            if (currentProfile) {
                                const afterBuilding = Date.now();
                                currentProfile.building = afterBuilding - afterFactory;
                            }

                            this.semaphore.release();
                            afterBuild();
                        });

對於loader的匹配,發生於moduleFactory.create()中,其中執行beforeResolve hook,執行完的回調函數中執行factory,factory中執行resolver,resolver是 resolver hook的回調函數,其中通過this.ruleSet.exec和request的分割分別完成loader的匹配,對module匹配到的loader的生成即在這裏完成,之後注入到module對象中,接下來我們回到moduleFactory.create的回調函數
此時生成的module對象中有幾個顯著的屬性,

userRequest:
loaders

即當前模塊的路徑和匹配到的loader,本例中index.js模塊即匹配到了testloader,我們編寫的測試loader,

testloader

module.exports = function(source){
    console.log("test loader")
    return source+";console.log(123)"
}

關於loader的編寫本篇也不細講,借用一句文檔的描述

A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader. The given function will have access to the Loader API using the thiscontext provided to it.

如何寫一個loader

我們回到源碼,moduleFactory.create回調函數中,執行了buildModule,
buildModule中執行了module.build(),build中執行doBuild,doBuild中執行runloaders,自此開始即爲對loader的執行,runloaders中執行iteratePitchingLoaders,然後執行loadLoader,通過import或者require等模塊化方法加載loader資源,這裏分爲幾種loaders,根據不同情況,最終執行runSyncOrAsync,runSyncOrAsync中

var result = (function LOADER_EXECUTION() {
            return fn.apply(context, args);
        }());

通過LOADER_EXECUTION()方法對loader進行,執行,返回執行結果,繼續執行其他loader,loader的執行即爲此處,
loader執行完成之後,buildModule執行完成,進行callback的執行,其中執行了moduleFactory.create中定義的afterBuild函數,afterBuild函數執行了processModuleDependencies函數,processModuleDependencies函數中通過內部定義的addDependency和addDependenciesBlock方法,生成當前module所依賴的module,執行addModuleDependencies

this.addModuleDependencies(
            module,
            sortedDependencies,
            this.bail,
            null,
            true,
            callback
        );

傳入此模塊的依賴,addModuleDependencies中循環對sortedDependencies進行了factory.create,factory.create中又執行了beforeResolve hook,從而又執行上面流程,匹配loader,執行loader,對依賴進行遍歷等步驟,所以,通過這個深度優先遍歷,即可對所有模塊及其依賴模塊進行loade的匹配和處理,自此,loader學習的三個目標已經達成

make hook主要內容即是這些,之後又執行了seal,afterCopile等等等hook,這些即爲一些關於代碼分割,抽離等等插件的執行時機,爲我們插件的編寫提供了一些入口,compiler和compilation執行過程中的所有hook可以查看文檔,一共有九十多個(汗顏💧)compiler hookcompilation hook

至此,loader的執行過程和plugin的執行過程已經非常清晰,本篇文章目的也已達到,如果大家對某些hook的執行位置感興趣或者對某些插件某些loader感興趣,即可使用debugger根據此流程進行跟蹤,從而對插件,loader的使用更加得心應手,

本篇文章示例代碼github地址

如果本篇文章對你瞭解webpack有一定的幫助,順便留個star ><

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章