webpack 流程解析(3) 建立compilation物件

csywweb 發表於 2021-10-15
Webpack

前言

webpack初始化完成之後,則會通過傳入的options.watch來判斷是否要開啟watch,如果開啟watch則會執行watch的流程,如果是run,則會執行run的流程,本系列只關注主線,所以我們直接從run開始,watch感興趣的同學可以自行研究研究

compiler.run()

直接看核心程式碼

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

簡單說就是做了這麼幾件事。

  • 觸發beforeRun的回撥
  • 觸發run的回撥
  • 然後調this.readRecords
  • readRecords的回撥裡呼叫this.compile(onCompiled)開啟編譯

我們一步一步看
beforeRun會觸發之前在NodeEnvironmentPlugin中註冊的 beforeRun 鉤子,這個plugin會判斷inputFileSystem是否被配置,如果沒有配置則執行purge清理方法。

readRecords會讀取一些統計資訊,由於沒有配置recordsInputPath,這裡會把this.records初始為{}

建立compilation例項

接下來就執行到compiler.compiler()方法。
compiler.compiler方法貫穿了整個編譯過程。首先compiler例項化了一個compilation

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);
        // do something
    }
}

獲取引數

newCompilationParams() {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory()
    };
    return params;
}

引數有兩個,一個是NormalModuleFactory的例項,一個是ContextModuleFactory的例項。ContextModuleFactory這個引數我在compilation裡面沒搜到,暫且略過,後續打個斷點看會不會走進來。這裡主要看下NormalModuleFactory

NormalModuleFactory

先看下例項化NormalModuleFactory的引數

const normalModuleFactory = new NormalModuleFactory({
    context: this.options.context,
    fs: this.inputFileSystem,
    resolverFactory: this.resolverFactory,
    options: this.options.module,
    associatedObjectForCache: this.root,
    layers: this.options.experiments.layers
});

注意這裡的resolverFactory, 以後會用到。
接下來看下new NormalModuleFactory的時候發生了啥

constructor({
    context,
    fs,
    resolverFactory,
    options,
    associatedObjectForCache,
    layers = false
}) {
    super();
    this.hooks = 定義了很多hooks
    this.resolverFactory = resolverFactory;
    this.ruleSet = ruleSetCompiler.compile([
            {
                rules: options.defaultRules
            },
            {
                rules: options.rules
            }
    ]);
    this.context = context || "";
    this.fs = fs;
    this._globalParserOptions = options.parser;
    this._globalGeneratorOptions = options.generator;
    /** @type {Map<string, WeakMap<Object, TODO>>} */
    this.parserCache = new Map();
    /** @type {Map<string, WeakMap<Object, Generator>>} */
    this.generatorCache = new Map();
    /** @type {Set<Module>} */
    this._restoredUnsafeCacheEntries = new Set();

    const cacheParseResource = parseResource.bindCache(
        associatedObjectForCache
    );
    this.hooks.factorize.tapAsync(
        // do something
    );
    this.hooks.resolve.tapAsync(
        // dosomething
    );
}

可能覺得太長了不看,我直接給大家翻譯一下幹了啥

  • 定義了很多內部的hook,比方說最後註冊的兩個 reslover,factorize
  • 定義了很多構建module需要的變數,這裡先不細說。
  • 同時註冊了兩個NormalModuleFactory的內部 hook。會在合適的時機在被compilation物件呼叫

new NormalModuleFactory()之後,觸發了compiler上的normalModuleFactory鉤子

this.hooks.normalModuleFactory.call(normalModuleFactory);

繼續觸發鉤子回撥

然後觸發beforeCompilecompile鉤子。

開始例項化

newCompilation(params) {
    const compilation = this.createCompilation(params); //這裡簡單理解為new 了一下,
    compilation.name = this.name;
    compilation.records = this.records;
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
}

分類一下,這個函式做了兩件事。

  • new Compilation,再賦一點值
  • 註冊兩個鉤子

    new Compilation 內部細節

    Compilation物件表示了當前模組資源、編譯生成資源、變化的檔案、以及被跟蹤依賴的狀態資訊,代表了一次資源的構建。constructor程式碼太多就不貼在這裡了,大家可以自行去看看。

簡單總結一下,就是

  • 在compilation內部註冊了很多內部的鉤子。
  • 初始化了一些自身屬性
  • 例項化MainTemplate,ChunkTemplate,HotUpdateChunkTemplate, RuntimeTemplate, ModuleTemplate。用於提供編譯模板

例項化後的鉤子呼叫

this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);

compilation鉤子的呼叫,會呼叫到之前在entryplugin註冊的方法。
會往dependencyFactories裡增加依賴模組。

compilation.dependencyFactories.set(
        EntryDependency,
        normalModuleFactory
);
也許你好奇,這裡為什麼有兩個鉤子?原因是跟子編譯器有關。在 Compiler 的 createChildCompiler 方法裡建立子編譯器,其中 thisCompilation 鉤子不會被複制,而 compilation 會被複制。
子編譯器擁有完整的 module 和 chunk 生成,通過子編譯器可以獨立於父編譯器執行一個核心構建流程,額外生成一些需要的 module 和 chunk。

結束

到目前為止,描述構建流程的物件compiler和描述編譯過程的物件compilation 物件已經建立完成。下一篇文章我們進入構建流程。