webpack源碼執(zhí)行過程分析曙强,loader+plugins

webpack運(yùn)行于node js之上,了解源碼的執(zhí)行涎拉,不僅可以讓我們對(duì)webpack的使用更為熟悉瑞侮,更會(huì)增強(qiáng)我們對(duì)應(yīng)用代碼的組織能力,

本篇文章重點(diǎn)從webpack核心的兩個(gè)特性loader鼓拧,plugin半火,進(jìn)行深入分析,

我們從一個(gè)例子出發(fā)來分析webpack執(zhí)行過程季俩,地址

我們使用 vscode 調(diào)試工具來對(duì)webpack進(jìn)行調(diào)試钮糖,

首先我們從入口出發(fā)

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

示例項(xiàng)目通過npm run build 進(jìn)行啟動(dòng),npm run 會(huì)新建一個(gè)shell酌住,并將 node_modules/.bin 下的所有內(nèi)容加入環(huán)境變量店归,我們查看下.bin 文件夾下內(nèi)容

webpack
webpack-cli
webpack-dev-server

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

#!/usr/bin/env node

使用node執(zhí)行此文件內(nèi)容消痛,webpack 文件的主要內(nèi)容是判斷webpack-cli或者webpack-command有沒有安裝,如果有安裝則執(zhí)行對(duì)應(yīng)文件內(nèi)容都哭,本例安裝了webpack-cli肄满,所以通過對(duì)目標(biāo)cli的require谴古,進(jìn)入到對(duì)應(yīng)cli的執(zhí)行,

webpack-cli
webpack-cli是一個(gè)自執(zhí)行函數(shù)稠歉,對(duì)我們?cè)诿钚袀魅氲囊恍﹨?shù)進(jìn)行了解析判斷掰担,核心內(nèi)容是把webpack入口文件作為參數(shù),執(zhí)行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后带饱,執(zhí)行compiler.run()或者compiler.watch(),
本例未啟動(dòng)熱更新所以執(zhí)行的是 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);
            }

既然已經(jīng)知道核心是這兩個(gè)參數(shù)的執(zhí)行阅羹,我們即可模擬一個(gè)webpack的執(zhí)行過程勺疼,本例中,我們創(chuàng)建一個(gè)debug.js

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

const compiler = webpack(options);

我們?cè)趙ebpack()函數(shù)前面加上斷點(diǎn)捏鱼,即可通過vscode開始debug
我們先對(duì)生成compiler過程進(jìn)行分析执庐,

webpack函數(shù)

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

我們可以看到,有對(duì)options參數(shù)的驗(yàn)證validateSchema(webpackOptionsSchema,options);
有對(duì)默認(rèn)配置的合并 options = new WebpackOptionsDefaulter().process(options);
合并內(nèi)容
然后對(duì)所有的plugins配置進(jìn)行注冊(cè)操作

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

關(guān)于這里的注冊(cè)导梆,我們可以通過寫一個(gè)plugin來描述執(zhí)行過程轨淌,
本例中我們新建一個(gè)testplugin文件,

testplugin

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

關(guān)于插件的編寫看尼,我們只需要提供一個(gè)類递鹉,prototype上含有apply函數(shù),同時(shí)擁有一個(gè)compiler參數(shù)藏斩,之后通過tap注冊(cè)compiler上的hook躏结,使得webpack執(zhí)行到指定時(shí)機(jī)執(zhí)行回調(diào)函數(shù),具體編寫方法參考寫一個(gè)插件

本示例插件中狰域,我們?cè)赾ompiler的run hook上注冊(cè)了testplugin插件媳拴,回調(diào)的內(nèi)容為打印 “test plugin”,并且兆览,在注冊(cè)的時(shí)候我們會(huì)打印 ”注冊(cè)“屈溉,來跟蹤plugin的注冊(cè)執(zhí)行流程,

回到webpack 函數(shù)拓颓,可以看到语婴,進(jìn)行完插件的注冊(cè)描孟,就會(huì)執(zhí)行兩個(gè)hook的回調(diào)驶睦,

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

這時(shí),就會(huì)執(zhí)行我們注冊(cè)在environment匿醒,afterEnvironment上的plugin的回調(diào)场航,其他插件的回調(diào)執(zhí)行也是通過call或者callAsync 來觸發(fā)執(zhí)行,webpack整個(gè)源碼執(zhí)行過程中會(huì)在不同的階段執(zhí)行不同的hook的call函數(shù)廉羔,所以溉痢,在我們編寫插件的過程中要對(duì)流程有些了解,從而將插件注冊(cè)在合適的hook上,

webpack函數(shù)的最后孩饼,就是執(zhí)行compiler.run函數(shù)髓削,我們?cè)谶@里加上斷點(diǎn),進(jìn)入compiler.run函數(shù)镀娶,

 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 函數(shù)中也是執(zhí)行了一系列的hook立膛,我們編寫的testplugin就會(huì)在this.hooks.run.callAsync處執(zhí)行,關(guān)于plugin的注冊(cè)和運(yùn)行具體細(xì)節(jié)梯码,本篇先不講宝泵,只需知道注冊(cè)通過tap,運(yùn)行通過call即可轩娶,儿奶,
到了這里,基本的plugin的運(yùn)行過程我們已經(jīng)了解鳄抒,接下來我們通過幾個(gè)目標(biāo)來對(duì)loader的執(zhí)行過程進(jìn)行分析闯捎,

  1. 模塊如何匹配到相對(duì)應(yīng)loader
  2. 模塊是如何遞歸的解析當(dāng)前模塊引用模塊的
  3. loader是在哪里執(zhí)行的

回到源代碼,執(zhí)行完一些hooks后嘁酿,進(jìn)入到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的執(zhí)行,重點(diǎn)是make 的hook闹司,我們進(jìn)入娱仔,make hook通過htmlWebpackPlugin注冊(cè)了一個(gè)回調(diào),回調(diào)中又注冊(cè)了一個(gè)SingleEntryPlugin游桩,然后又重新執(zhí)行了make.callAsync牲迫,進(jìn)入了SingleEntryPlugin的回調(diào)

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

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

可以看到,主要執(zhí)行了addEntry方法借卧,addEntry中執(zhí)行addEntry hook盹憎,然后調(diào)用_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 創(chuàng)建modeuleFactory對(duì)象铐刘,然后執(zhí)行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();
                        });

對(duì)于loader的匹配陪每,發(fā)生于moduleFactory.create()中,其中執(zhí)行beforeResolve hook,執(zhí)行完的回調(diào)函數(shù)中執(zhí)行factory镰吵,factory中執(zhí)行resolver檩禾,resolver是 resolver hook的回調(diào)函數(shù),其中通過this.ruleSet.exec和request的分割分別完成loader的匹配疤祭,對(duì)module匹配到的loader的生成即在這里完成盼产,之后注入到module對(duì)象中,接下來我們回到moduleFactory.create的回調(diào)函數(shù)
此時(shí)生成的module對(duì)象中有幾個(gè)顯著的屬性勺馆,

userRequest:
loaders

即當(dāng)前模塊的路徑和匹配到的loader戏售,本例中index.js模塊即匹配到了testloader侨核,我們編寫的測(cè)試loader,

testloader

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

關(guān)于loader的編寫本篇也不細(xì)講灌灾,借用一句文檔的描述

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.

如何寫一個(gè)loader

我們回到源碼搓译,moduleFactory.create回調(diào)函數(shù)中,執(zhí)行了buildModule锋喜,
buildModule中執(zhí)行了module.build(),build中執(zhí)行doBuild侥衬,doBuild中執(zhí)行runloaders,自此開始即為對(duì)loader的執(zhí)行跑芳,runloaders中執(zhí)行iteratePitchingLoaders轴总,然后執(zhí)行l(wèi)oadLoader,通過import或者require等模塊化方法加載loader資源博个,這里分為幾種loaders怀樟,根據(jù)不同情況,最終執(zhí)行runSyncOrAsync盆佣,runSyncOrAsync中

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

通過LOADER_EXECUTION()方法對(duì)loader進(jìn)行往堡,執(zhí)行,返回執(zhí)行結(jié)果共耍,繼續(xù)執(zhí)行其他loader虑灰,loader的執(zhí)行即為此處,
loader執(zhí)行完成之后痹兜,buildModule執(zhí)行完成穆咐,進(jìn)行callback的執(zhí)行,其中執(zhí)行了moduleFactory.create中定義的afterBuild函數(shù)字旭,afterBuild函數(shù)執(zhí)行了processModuleDependencies函數(shù)对湃,processModuleDependencies函數(shù)中通過內(nèi)部定義的addDependency和addDependenciesBlock方法,生成當(dāng)前module所依賴的module遗淳,執(zhí)行addModuleDependencies

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

傳入此模塊的依賴拍柒,addModuleDependencies中循環(huán)對(duì)sortedDependencies進(jìn)行了factory.create,factory.create中又執(zhí)行了beforeResolve hook屈暗,從而又執(zhí)行上面流程拆讯,匹配loader,執(zhí)行l(wèi)oader养叛,對(duì)依賴進(jìn)行遍歷等步驟种呐,所以,通過這個(gè)深度優(yōu)先遍歷一铅,即可對(duì)所有模塊及其依賴模塊進(jìn)行l(wèi)oade的匹配和處理陕贮,自此堕油,loader學(xué)習(xí)的三個(gè)目標(biāo)已經(jīng)達(dá)成

make hook主要內(nèi)容即是這些潘飘,之后又執(zhí)行了seal肮之,afterCopile等等等hook,這些即為一些關(guān)于代碼分割卜录,抽離等等插件的執(zhí)行時(shí)機(jī)戈擒,為我們插件的編寫提供了一些入口,compiler和compilation執(zhí)行過程中的所有hook可以查看文檔艰毒,一共有九十多個(gè)(汗顏??)compiler hook
筐高,compilation hook

至此,loader的執(zhí)行過程和plugin的執(zhí)行過程已經(jīng)非常清晰丑瞧,本篇文章目的也已達(dá)到柑土,如果大家對(duì)某些hook的執(zhí)行位置感興趣或者對(duì)某些插件某些loader感興趣,即可使用debugger根據(jù)此流程進(jìn)行跟蹤绊汹,從而對(duì)插件稽屏,loader的使用更加得心應(yīng)手,

本篇文章示例代碼github地址

如果本篇文章對(duì)你了解webpack有一定的幫助西乖,順便留個(gè)star ><

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末狐榔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子获雕,更是在濱河造成了極大的恐慌薄腻,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件届案,死亡現(xiàn)場(chǎng)離奇詭異庵楷,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)楣颠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門嫁乘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人球碉,你說我怎么就攤上這事蜓斧。” “怎么了睁冬?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵挎春,是天一觀的道長。 經(jīng)常有香客問我豆拨,道長直奋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任施禾,我火速辦了婚禮脚线,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘弥搞。我一直安慰自己邮绿,他們只是感情好渠旁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著船逮,像睡著了一般顾腊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挖胃,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天杂靶,我揣著相機(jī)與錄音,去河邊找鬼酱鸭。 笑死吗垮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凹髓。 我是一名探鬼主播抱既,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼扁誓!你這毒婦竟也來了防泵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤蝗敢,失蹤者是張志新(化名)和其女友劉穎捷泞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寿谴,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锁右,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讶泰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咏瑟。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖痪署,靈堂內(nèi)的尸體忽然破棺而出码泞,到底是詐尸還是另有隱情,我是刑警寧澤狼犯,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布余寥,位于F島的核電站,受9級(jí)特大地震影響悯森,放射性物質(zhì)發(fā)生泄漏宋舷。R本人自食惡果不足惜省店,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一驳遵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绣张,春花似錦、人聲如沸绎狭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坟岔。三九已至,卻和暖如春摔桦,著一層夾襖步出監(jiān)牢的瞬間社付,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工邻耕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸥咖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓兄世,卻偏偏與公主長得像啼辣,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子御滩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 說在前面:這些文章均是本人花費(fèi)大量精力研究整理鸥拧,如有轉(zhuǎn)載請(qǐng)聯(lián)系作者并注明引用,謝謝本文的受眾人群不是webpack...
    RockSAMA閱讀 6,899評(píng)論 2 7
  • 前幾篇文章中削解,我們介紹了webpack v4.20.2相關(guān)的內(nèi)容富弦,但是很多老項(xiàng)目,還在使用webpack 3氛驮,也要...
    何幻閱讀 2,657評(píng)論 0 1
  • 寫在開頭 先說說為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,273評(píng)論 4 31
  • GitChat技術(shù)雜談 前言 本文較長腕柜,為了節(jié)省你的閱讀時(shí)間,在文前列寫作思路如下: 什么是 webpack矫废,它要...
    蕭玄辭閱讀 12,674評(píng)論 7 110
  • 全民健身政策實(shí)施多年盏缤,關(guān)注健身的人越來越多,人們已經(jīng)意識(shí)到良好的健身習(xí)慣對(duì)于保持健康的重要性蓖扑,更多的人開始走進(jìn)健身...
    火鳥健身閱讀 105評(píng)論 0 0