深入webpack4源碼(二)—— 基本運行流程

好了。。終于可以開始看源碼了,先了解下大體流程。

這里直接開始說webpack的源碼,就不再細說webpack-cli了。
這倆的區別就是,webpack核心庫,webpack-cli處理webpack的一系列命令行操作。感興趣的可以看看這篇文章,其實直接看源碼也很簡單就是常規的那一套使用yargs這個庫,然后從命令行接受命令行參數轉化為webpack實際的參數。

進入正題,如果你使用過create-react-app或者vue-cli這樣的工具,你都發現他們都并沒有直接用命令行打包,而是自己寫了一個腳本然后require('webpack')然后傳入config進行打包,這樣的好處是更靈活,更好控制,以下來自create-react-app中react-script的build:

const webpack = require('webpack');
function build(previousFileSizes) {
  let compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
        // ....
    })  
})
}

先看webpack的package.json:

"main": "lib/webpack.js",

說明我們require的就是lib/webpack.js

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

這段代碼主要的工作就是初始化complier。


options為數組的情況,目前項目中還真沒用過,以后有用到了再補充。

validateSchema

這部分主要就是校驗options并且返回報錯:

const validateObject = (schema, options) => {
    const validate = ajv.compile(schema);
    const valid = validate(options);
    return valid ? [] : filterErrors(validate.errors);
};

return validateObject(schema, options);

schema是一個json形式的描述文件,描述著各個字段是什么類型,以及對應的錯誤信息,感覺就像async-validator的rules一樣,然后調用了一個叫ajv的庫,將秒速文件轉為了校驗函數校驗options。

WebpackOptionsDefaulter

其實webpack以及內置了很多默認的config,這個對象的作用就是合并默認的config和傳入的config。但是在源碼里這里不叫config,而是options。

class WebpackOptionsDefaulter extends OptionsDefaulter {
  constructor() {
      // this.set(xx, 'xx')
  }
}

set方法就是給默認字段設置對應的值。
更核心的方法都在OptionsDefaulter里,比如webpack.js中的options = new WebpackOptionsDefaulter().process(options);的process
OptionsDefaulter中,一開始就是定了兩個方法,很有意思:

const getProperty = (obj, name) => {
    name = name.split(".");
    for (let i = 0; i < name.length - 1; i++) {
        obj = obj[name[i]];
        if (typeof obj !== "object" || !obj || Array.isArray(obj)) return;
    }
    return obj[name.pop()];
};

const setProperty = (obj, name, value) => {
    name = name.split(".");
    for (let i = 0; i < name.length - 1; i++) {
        if (typeof obj[name[i]] !== "object" && obj[name[i]] !== undefined) return;
        if (Array.isArray(obj[name[i]])) return;
        if (!obj[name[i]]) obj[name[i]] = {};
        obj = obj[name[i]];
    }
    obj[name.pop()] = value;
};
  • getProperty:其作用就是拿對象的某個字段的值,但是這里的name可以是'xxx.xxx.xxx'這樣的格式,我們如果直接a.b.c.d如果b或者c就是undefined,那么直接就報錯了,這里直接巧妙的避免了這樣的情況,類似于Experimental的語法 optional-chaining,如let value = a?.b?.c?.d,如果有undefined最終就直接返回undefined了。
  • setProperty:也類似,比如a是{},我們不能直接設置a.b.c = xxx,這樣如果b是undefined也會報錯,這里就可以直接設置了。
class OptionsDefaulter {
    constructor() {
        this.defaults = {};
        this.config = {};
    }

    process(options) {
        options = Object.assign({}, options);
        for (let name in this.defaults) {
            switch (this.config[name]) {
                case undefined:
                    // ...
                    break;
                case "call":
                    // ...
                    break;
                case "make":
                    // ...
                    break;
                case "append": {
                    // ...
                }
                default:
            }
        }
        return options;
    }

    set(name, config, def) {
        if (def !== undefined) {
            this.defaults[name] = def;
            this.config[name] = config;
        } else {
            this.defaults[name] = config;
            delete this.config[name];
        }
    }
}

我們看到外層的WebpackOptionsDefaulter使用了很多set,其實就是這里的set,參數name代表option的具體名字,config代表具體的值。這里要說下的就是def:如果有def的時候,config就代表具體的合并方法的類型,而def就代表了option的具體值,具體的處理都在process中。

初始化complier

初始化complier就沒有太多好說的,主要就是初始化了一系列的勾子,和一系列參數:

this.hooks = { ... };
/** @type {string=} */
this.name = undefined;
/** @type {Compilation=} */
this.parentCompilation = undefined;
/** @type {string} */
this.outputPath = "";

this.outputFileSystem = null;
this.inputFileSystem = null;

/** @type {string|null} */
this.recordsInputPath = null;
/** @type {string|null} */
this.recordsOutputPath = null;
this.records = {};
this.removedFiles = new Set();
/** @type {Map<string, number>} */
this.fileTimestamps = new Map();
/** @type {Map<string, number>} */
this.contextTimestamps = new Map();
/** @type {ResolverFactory} */
this.resolverFactory = new ResolverFactory();

這些參數的具體含義簡單的看名字就知道,不懂的其實也要等到用到才明白。
可以講下的是ResolverFactory,看見resolve就能想到路徑解析。這個就是一個路徑解析器的工廠,在需要的時候根據option返回一個路徑解析器(這個類也有自己的勾子):

const { Tapable, HookMap, SyncHook, SyncWaterfallHook } = require("tapable");
const Factory = require("enhanced-resolve").ResolverFactory;

module.exports = class ResolverFactory extends Tapable {
    constructor() {
        super();
        this.hooks = {
            resolveOptions: new HookMap(
                () => new SyncWaterfallHook(["resolveOptions"])
            ),
            resolver: new HookMap(() => new SyncHook(["resolver", "resolveOptions"]))
        };
        this.cache1 = new WeakMap();
        this.cache2 = new Map();
    }

    get(type, resolveOptions) {
        const cachedResolver = this.cache1.get(resolveOptions);
        if (cachedResolver) return cachedResolver();
        const ident = `${type}|${JSON.stringify(resolveOptions)}`;
        const resolver = this.cache2.get(ident);
        if (resolver) return resolver;
        const newResolver = this._create(type, resolveOptions);
        this.cache2.set(ident, newResolver);
        return newResolver;
    }

    _create(type, resolveOptions) {
        // ....
    }
};

他也是主要調用的enhanced-resolve這個庫來解析路徑,并對解析器做了緩存。使用案例

應用NodeEnvironmentPlugin插件

該插件的主要作用是給complier初始化輸入輸出文件系統和監視文件系統。

class NodeEnvironmentPlugin {
    apply(compiler) {
        compiler.inputFileSystem = new CachedInputFileSystem(
            new NodeJsInputFileSystem(),
            60000
        );
        const inputFileSystem = compiler.inputFileSystem;
        compiler.outputFileSystem = new NodeOutputFileSystem();
        compiler.watchFileSystem = new NodeWatchFileSystem(
            compiler.inputFileSystem
        );
        compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
            if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
        });
    }
}
module.exports = NodeEnvironmentPlugin;
  • 輸出文件系統:非常簡單,就是包裝了一層node原生的fsapi。
  • 輸入文件系統:復雜,目前還沒有看見使用的地方。
  • 監視文件系統:復雜,目前還沒有看見使用的地方,但是主要的作用就是監視文件改動及熱更新等。

應用插件

這里很重要,我們webpack.config.js中的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);
        }
    }
}

就像上一節講過的,在這里只是webpack抽象后的掛載插件,而真正的在勾子上掛載執行函數都是具體在每個plugin里實現的。
之前我們說過我們必須為plugin實現apply方法,實際上這里也展示了掛載動態插件的方式,就是為plugin實現call方法。

WebpackOptionsApply

這個對象做的事情理解起來非常簡單,那就是根據options為complier應用插件(但插件本身復雜度,哭了)。

process(options, compiler) {
  let ExternalsPlugin;
  compiler.outputPath = options.output.path;
  compiler.recordsInputPath = options.recordsInputPath ||     options.recordsPath;
  compiler.recordsOutputPath =
    options.recordsOutputPath || options.recordsPath;
  compiler.name = options.name;
  // TODO webpack 5 refactor this to     MultiCompiler.setDependencies() with a WeakMap
  // @ts-ignore TODO
  compiler.dependencies = options.dependencies;
  new xxxPlugin().apply(compiler);
  // ........
}

我們之前說過,webpack是插件機制,那到底是怎么樣的插件機制呢?其實就是webpack聲明了很多勾子,然后調用了很多勾子。 emmm,所以你看源碼的時候你會發現,他就是調用了很多勾子,然后代碼就完成打包了,但是你根本找不到處理代碼的地方到底在哪里,實際上做處理的是掛載在勾子上的插件,那么為了完成webpack的所有功能,到底掛載了哪些插件?這個問題的答案就在這個對象里。

在這里插一段其他文章里,我很認同的一段話:

  • 聯系松散。你可以發現:使用tapable鉤子類似事件監聽模式,雖然能有效解耦,但鉤子的注冊與調用幾乎完全無關,很難將一個鉤子的“創建 - 注冊 - 調用”過程有效聯系起來。
  • 模塊交互基于鉤子。webpack內部模塊與插件在很多時候,是通過鉤子機制來進行聯系與調用的。但是,基于鉤子的模式是松散的。例如你看到源碼里一個模塊提供了幾個鉤子,但你并不知道,在何時、何地該鉤子會被調用,又在何時、何地鉤子上被注冊了哪些方法。這些以往都是需要我們通過在代碼庫中搜索關鍵詞來解決。
  • 鉤子數量眾多。webpack內部的鉤子非常多,數量達到了180+,類型也五花八門。除了官網列出的compiler與compilation中那些常用的鉤子,還存在著眾多其他可以使用的鉤子。有些有用的鉤子你可能無從知曉,例如我最近用到的localVars、requireExtensions等鉤子。
  • 內置插件眾多。webpack v4+ 本身內置了許多插件。即使非插件,webpack的模塊自身也經常使用tapable鉤子來交互。甚至可以認為,webpack項目中的各個模塊都是“插件化”的。這也使得幾乎每個模塊都會和各種鉤子“打交道”。

我的建議是這個對象可以暫時不管,用到再查。

到現在只講了complier的初始化,實際上complier.run()后你會發現更多的勾子調用,而且compiler里還會引用其他模塊,其他模塊還有自己的勾子,所以我們在閱讀源碼的時候,建議可以看見了某個勾子調用就全局搜下這個勾子的名字加上調用方法,然后再看掛載的執行函數到底做了什么處理。例如:這個勾子賦值給了complier.hooks.run,并且async的勾子,那么全局就搜索hooks.run.taphooks.run.tapAsynchooks.run.tapPromise,然后再看到底做了什么。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容