Webpack 源碼分析(3)— Webpack 構建流程分析

文章首發於個人博客 https://github.com/mcuking/bl...

在開始分析源碼以前,筆者先把以前收集到的 webpack 構建流程圖貼在下面。後面的分析過程讀者能夠對照着這張圖來進行理解。css

webpack 構建流程.png

構建準備階段

回顧前面的文章,在 webpack-cli 從新調用 webpack 包時,首先執行的就是 node_module/webpack/lib/webpack.js 中的函數。以下:node

const webpack = (options, callback) => {
    ...
    let compiler;
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(
            Array.from(options).map(options => webpack(options))
        );
    } else if (typeof options === "object") {
        // 檢查傳入的 options 並設置默認項
        options = new WebpackOptionsDefaulter().process(options);

        // 初始化一個 compiler 對象實例
        compiler = new Compiler(options.context);
        // 將 options 掛在到這個實例對象上
        compiler.options = options;

        // 清理構建的緩存
        new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
        }).apply(compiler);

        // 遍歷 options.plugins 數組,將用戶配置的 plugins 所有初始化
        // 並將插件內部業務邏輯綁定到 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();

        // 根據 options 中配置的參數,實例化內部插件並綁定到 compiler 實例對象上
        // 例如 externals 有配置,則須要配置 ExternalsPlugins
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    ...
        if (callback) {
        ...
        compiler.run(callback);
    }
    return compiler;
};

其中 WebpackOptionsDefaulter 這個類的做用就是檢測 options 並設置默認配置項,其關鍵代碼以下:webpack

class WebpackOptionsDefaulter extends OptionsDefaulter {
    constructor() {
        super();

        this.set("entry", "./src");

        this.set("devtool", "make", options =>
            options.mode === "development" ? "eval" : false
        );
        this.set("cache", "make", options => options.mode === "development");

        this.set("context", process.cwd());
        this.set("target", "web");
        ...
    }
}

set 方法和 process 方法都是繼承自父類 OptionsDefaulter,這裏就不贅述了。git

接着是初始化了一個 compiler 對象,並將處理好的 options 掛載到實例上。而後開始初始化 options.plugins 上的插件,將插件綁定到 compiler 實例對象上,若是 plugin 是函數,則直接調用;若是是對象,則調用對象上的 apply 方法。這也就爲何 webpack 的插件配置通常都是對象實例數組的緣由,以下:es6

{
  plugins: [new HtmlWebpackPlugin()];
}

關於 webpack 插件機制的內容請參考上篇文章 Webpack 源碼分析(2)— Tapable 與 Webpack 的關聯github

最後調用了一個名爲 WebpackOptionsApply 的類,咱們看下其實現的部分代碼:web

class WebpackOptionsApply extends OptionsApply {
    constructor() {
        super();
    }

    process(options, compiler) {
        ...
        new EntryOptionPlugin().apply(compiler);
        compiler.hooks.entryOption.call(options.context, options.entry);
        ...
        if (options.externals) {
            ExternalsPlugin = require("webpack/lib/ExternalsPlugin");
            new ExternalsPlugin(
                options.output.libraryTarget,
                options.externals
            ).apply(compiler);
        }
        ...
    }
}

從中不難發現,WebpackOptionsApply 主要做用就是根據 options 中的設置,來掛載對應的插件到 compiler 實例對象上,例如若是設置了 externals,則掛載 ExternalsPlugin 插件。須要注意的是,有些插件是默認必需要掛載的,而不禁 options 中的設置決定,例如 EntryOptionPlugin 插件。算法

這裏咱們正好能夠到 EntryOptionPlugin 看下咱們在 options 裏常常設置的參數 entry 到底支持幾種類型,主要代碼以下:數組

const itemToPlugin = (context, item, name) => {
  if (Array.isArray(item)) {
    return new MultiEntryPlugin(context, item, name);
  }
  return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugin {
  apply(compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      if (typeof entry === 'string' || Array.isArray(entry)) {
        itemToPlugin(context, entry, 'main').apply(compiler);
      } else if (typeof entry === 'object') {
        for (const name of Object.keys(entry)) {
          itemToPlugin(context, entry[name], name).apply(compiler);
        }
      } else if (typeof entry === 'function') {
        new DynamicEntryPlugin(context, entry).apply(compiler);
      }
      return true;
    });
  }
};

有上面代碼咱們能夠知道 entry 能夠是字符串、數組、對象和函數,其中當是數組時,則掛載 MultiEntryPlugin 插件,也就是說 webpack 會將多個文件打包成一個文件。而當是對象時,則遍歷每一個鍵值對,而後執行 itemToPlugin 方法,也就是說 webpack 會將對象中的每一項入口對應的文件分別打包成不一樣的文件,這個就對應到了咱們常說的多頁面打包場景。緩存

到這裏是否是發現當看懂了源碼,就會對以前死記硬背的 webpack 配置有了更深刻的理解了呢?其實這就是閱讀源碼的一個很是棒的好處。

接下來則是調用了 compiler 對象的 run 方法,那麼咱們就回到 Compiler 文件中,進一步分析 Compiler 中到底作了哪些事情。

模塊構建(make)階段

下面就是 Compiler 類的關鍵代碼:

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            // 總共有 26 個鉤子,下面列舉的是比較常見的
            run: new AsyncSeriesHook(["compiler"]),
            emit: new AsyncSeriesHook(["compilation"]),
            compilation: new SyncHook(["compilation", "params"]),
            compile: new SyncHook(["params"]),
            make: new AsyncParallelHook(["compilation"]),
            ...
        },
        ...
    }

    watch(watchOptions, handler) {

    }

    run(callback) {
        const onCompiled = (err, compilation) => {
            ...
        };

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

    ...

    emitAssets(compilation, callback) {

    }

    ...

    createCompilation() {
        return new Compilation(this);
    }

    newCompilation(params) {
        const compilation = this.createCompilation();
        compilation.fileTimestamps = this.fileTimestamps;
        compilation.contextTimestamps = this.contextTimestamps;
        compilation.name = this.name;
        compilation.records = this.records;
        compilation.compilationDependencies = params.compilationDependencies;
        this.hooks.thisCompilation.call(compilation, params);
        this.hooks.compilation.call(compilation, params);
        return compilation;
    }

    createNormalModuleFactory() {
        const normalModuleFactory = new NormalModuleFactory(
            this.options.context,
            this.resolverFactory,
            this.options.module || {}
        );
        this.hooks.normalModuleFactory.call(normalModuleFactory);
        return normalModuleFactory;
    }

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

    ...

    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(err => {
                    if (err) return callback(err);

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

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

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

在分析這塊源碼前,咱們先明確下 webpack 兩個核心概念: Compiler 和 Compilation。

  • Compiler 類(./lib/Compiler.js): webpack 的主要引擎,在 compiler 對象記錄了完整的 webpack 環境信息,在 webpack 從啓動到結束,compiler 只會生成一次。你能夠在 compiler 對象上讀取到 webpack config 信息,outputPath 等;
  • Compilation 類(./lib/Compilation.js):表明了一次單一的版本構建和生成資源。compilation 編譯做業能夠屢次執行,好比 webpack 工做在 watch 模式下,每次監測到源文件發生變化時,都會從新實例化一個 compilation 對象。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。

Compiler 和 Compilation 區別?

Compiler 表明的是不變的 webpack 環境; Compilation 表明的是一次編譯做業,每一次的編譯均可能不一樣。

compiler.run()

單獨截取 run 方法以下:

run(callback) {
    const onCompiled = (err, compilation) => {
        ...
       if (this.hooks.shouldEmit.call(compilation) === false) {
        const stats = new Stats(compilation);
        stats.startTime = startTime;
        stats.endTime = Date.now();
        this.hooks.done.callAsync(stats, err => {
            if (err) return finalCallback(err);
            return finalCallback(null, stats);
        });
        return;
    }
        ...
    };

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

在 run 函數裏,首先觸發了一些鉤子:beforeRun -> run -> done,並在觸發 run 鉤子的時候,執行了 this.compile 方法。那麼咱們就去看下這個 compile 方法具體作了些什麼。

compiler.compile()

首先截取 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(err => {
                if (err) return callback(err);

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

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

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

代碼中初始化了一個 compilation 實例對象,另外和 run 方法一些,compile 也觸發一系列鉤子:beforeCompile -> compile -> make -> afterCompile

其中根據最上面的流程圖,在 make 鉤子階段,webpack 開始了真正的對模塊的編譯。那麼咱們看下到底什麼邏輯訂閱了 make 鉤子。經過全局搜索 hooks.make.tapAsync,咱們能夠看到 SingleEntryPlugin、MultiEntryPlugin、DllEntryPlugin、DynamicEntryPlugin 等插件中都訂閱了 make 鉤子。

那麼咱們先進入 SingleEntryPlugin 文件(./lib/SingleEntryPlugin.js)中查看,關鍵代碼以下:

class SingleEntryPlugin {
    constructor(context, entry, name) {
        this.context = context;
        this.entry = entry;
        this.name = name;
    }

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

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

    ...
}

其中主要調用了 compilation.addEntry 方法,繼續查看 compilation.addEntry(./lib/Compilation.js)。

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

    ...
    this._addModuleChain(
        context,
        entry,
        module => {
            this.entries.push(module);
        },
        (err, module) => {
            ...

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

經過上面代碼咱們能夠看到 addEntry 又調用了 _addModuleChain,後面調用我就不在這裏展現代碼了,直接把調用棧列出來,感興趣的同窗能夠自行查看源碼。調用棧以下:

this.addEntry -> this._addModuleChain -> this.addModule -> this.buidModule -> module.build

addEntry 的做用是將模塊的入口信息傳遞給模塊鏈中,即 addModuleChain,隨後繼續調用 compiliation.factorizeModule,這些調用最後會將 entry 的入口信息」翻譯「成一個模塊(嚴格上說,模塊通常是 NormalModule 實例化後的對象)。

當模塊開始構建時,會觸發 buidModule 鉤子。下面是 buidModule 方法的關鍵代碼,其中 module.build 執行成功後,會觸發 succeedModule 鉤子,若是失敗則觸發 failedModule 鉤子。

buildModule(module, optional, origin, dependencies, thisCallback) {
    ...

    this.hooks.buildModule.call(module);
    module.build(
        this.options,
        this,
        this.resolverFactory.get("normal", module.resolveOptions),
        this.inputFileSystem,
        error => {
            ...
            const originalMap = module.dependencies.reduce((map, v, i) => {
                map.set(v, i);
                return map;
            }, new Map());
            module.dependencies.sort((a, b) => {
                const cmp = compareLocations(a.loc, b.loc);
                if (cmp) return cmp;
                return originalMap.get(a) - originalMap.get(b);
            });
            if (error) {
                this.hooks.failedModule.call(module, error);
                return callback(error);
            }
            this.hooks.succeedModule.call(module);
            return callback();
        }
    );
}

那麼 module 又是從哪裏來的?從 Compilation.js 代碼中咱們能夠知道這個是 Module 類的實例,其中又具體分爲 NormalModule、ExternalModule、MutiModule、DelegatedModule 等。

咱們先進入到常見的 NormalModule 中查看源碼(文件地址 ./lib/NormalModule.js)。nomalModule.build 又調用了自身的 nomalModule.doBuild 方法

doBuild(options, compilation, resolver, fs, callback) {
    ...

    runLoaders(
        {
            resource: this.resource,
            loaders: this.loaders,
            context: loaderContext,
            readResource: fs.readFile.bind(fs)
        },
        (err, result) => {...}
    )
}

nomalModule.doBuild 方法又調用了 runLoaders 方法來調用對應的 loader 對模塊進行編譯,最終會經過 loader 的組合將全部模塊(css,less,jpg 等)編譯成標準的 js 模塊。

寫過 webpack loader 童鞋應該對 runLoader 比較熟悉,這個能夠獨立運行 webpack loader,而無需安裝整個 webpack,對於調試 webpack loader 很方便。

模塊構建完成以後,在 normalModule.doBuild 方法的最後一個參數即回調函數中,會使用 acorn 的 parse 方法將構建後的標準 js 模塊內容轉換成 AST 語法樹,經過其中的 require 語句來找到這個模塊所依賴的其餘模塊,而後將該模塊也添加到依賴列表中,最後遍歷依賴列表依次去構建。總結來講就是不斷的分析模塊的依賴和不斷的構建模塊,直到全部涉及到的模塊都構建完成。

// 將 js 模塊轉成 AST 語法書,並分析該模塊因此來的模塊
const result = this.parser.parse(source);
...

當全部模塊都構建完成,會存放在 Compilation 對象的 modules 數組屬性中。構建成功後會觸發 succeedModule 鉤子,不然會觸發 failedModule 鉤子。到此模塊構建(make)階段就結束了。

優化階段

模塊構建完成後,就會調用 Compilation 對象上的 seal 方法,該方法主要是觸發 seal 鉤子,開始對模塊構建結果進行不少的優化操做,其中就包含了基於 module 生成 chunk 的邏輯。下面是 chunk 生成的算法:

  1. webpack 先將 entry 中對應的 module 都生成一個新的 chunk;
  2. 遍歷 module 的依賴列表,將依賴的 module 也加入到 chunk 中;
  3. 若是一個依賴的 module 是動態引入的模塊(例如 require.ensure 或者 es6 中的 dynamic-import 的引入方式),那麼就會根據這個 module 建立新的 chunk,並繼續遍歷依賴;
  4. 重複上面的過程,直到獲得全部的 chunks。

生成 chunk 以後,接下來還會調用 Compilation.createHash 方法爲文件生成 hash,例如 js 通常設置 chunkHash,css 通常設置 contentHash 等。

文件 hash 建立完成以後,則會調用 createModuleAssets 方法將上個階段構建傳出來的標準 js 模塊,放在 Compilation 的 assets 對象屬性上去,key 是文件名,value 是構建後的模塊內容。到此優化階段就結束了。

文件生成階段

優化階段完成以後,就會進入到文件生成階段。主要是在 Compiler 中觸發 emit 鉤子,調用 compilation.getPath 獲取到文件輸出的目錄,而後將生成的文件寫到磁盤對應的目錄中。

到此爲止,Webpack 的構建過程就完成了。經歷了整個源碼解讀過程,相信讀者對 Webpack 的理解會更加深刻了。

相關文章

相關文章
相關標籤/搜索