webpack編譯流程淺析

前言

webpack 只支持JS模塊,全部其餘類型的模塊,好比圖片,css等,都須要經過對應的loader轉成JS模塊。因此在webpack中不管任何類型的資源,本質上都被當成JS模塊處理。css

Webpack 源碼是一個插件的架構,他的不少功能都是經過諸多的內置插件實現的。node

從配置文件讀取 entry 開始,到最後輸出 bundle.js 的過程,就是主線react

應該關心以下幾點:webpack

webpack 的編譯過程主要有哪些階段?(生命週期)git

webpack 是如何 從 entry 開始解析出整個依賴樹的?github

loaders 是在什麼時候被調用的?web

最終是如何知道要生成幾個文件,以及每一個文件的內容的?json

而其餘一些不重要的問題咱們儘可能忽略,好比如何解析配置,如何處理錯誤,HASH 規則等。數組

編譯源碼分爲如下幾步:promise

  1. 根據咱們的webpack配置註冊號對應的插件;

  2. 調用 compile.run 進入編譯階段,

  3. 在編譯的第一階段是 compilation,他會註冊好不一樣類型的module對應的 factory,否則後面碰到了就不知道如何處理了

  4. 進入 make 階段,會從 entry 開始進行兩步操做:

  5. 第一步是調用 loaders 對模塊的原始代碼進行編譯,轉換成標準的JS代碼

  6. 第二步是調用 acorn 對JS代碼進行語法分析,而後收集其中的依賴關係。每一個模塊都會記錄本身的依賴關係,從而造成一顆關係樹

  7. 最後調用 compilation.seal 進入 render 階段,根據以前收集的依賴,決定生成多少文件,每一個文件的內容是什麼

當咱們const webpack = require('webpack')時:

會從webpack包的package.json文件的main字段找到入口文件, 這裏是:

"main": "lib/index.js",
複製代碼

入口

咱們找到lib/index.js文件: 發現它經過module.exports對外暴露了webpack及其餘一些方法。

這裏有個有意思的函數:

const exportPlugins = (obj, mappings) => {
	for (const name of Object.keys(mappings)) {
		Object.defineProperty(obj, name, {
			configurable: false,
			enumerable: true,
			get: mappings[name]
		});
	}
};

exportPlugins((module.exports.cache = {}), {
	MemoryCachePlugin: () => require("./cache/MemoryCachePlugin")
});
複製代碼

經過這個函數, 實現了往module.exports這個對象上添加屬性, 並設置屬性的configurable, enumerable, get等特性。

webpack

咱們順着index.js找到webpack.js

const webpack = (options, callback) => {
    if (Array.isArray(options)) {
        compiler = createMultiCompiler(options);
    } else {
        compiler = createCompiler(options);
    }
    if (callback) {}

    return compiler;
};

module.exports = webpack;
複製代碼

簡化一下發現webpack是一個方法, 支持兩個參數, callback是一個可選的回調。 options支持數組形式,這裏咱們暫時按只傳一個非數組的options往下走(options就是咱們在項目裏配置的webpack.config.js文件)

這裏進入 createCompiler方法, 發現了compiler原來是一個構造函數的實例,

const createCompiler = options => {
    options = new WebpackOptionsDefaulter().process(options);   //針對webpack的默認設置,主要功能內容都在原型上面
    const compiler = new Compiler(options.context);             //根據配置的option生成compiler實例, 此時的options.context是process.cwd() 方法返回 Node.js 進程的當前工做目錄。
    compiler.options = options;
    new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    if (Array.isArray(options.plugins)) {                   ///這裏會解析webpack.config.js的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);            ////這裏會解析webpack.config.js的entry
    return compiler;
};

複製代碼

Compiler

咱們進到compiler.js中來看這個Compiler構造函數。 當咱們執行編譯時, 我從create-react-app的cli上發現會執行compiler實例的run方法。

run (callback) {                                 //compiler實例編譯要執行的run方法
        this.cache.endIdle(err => {
            if (err) return finalCallback(err);

            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);
                    });
                });
            });
        });
    }
複製代碼

發現這裏出現了hooks, 在Compiler不難找到hooks在constructor中已經註冊, 來自tapable這個庫。

beforerun, run都是來自這個庫的AsyncSeriesHook的實例 compile是來自這個庫的SyncHook的實例

beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
compile: new SyncHook(["params"]),
複製代碼

爲了下一步的解讀, 咱們首先就要去弄清tapable這個庫都有些什麼東西。

tapable

Webpack爲此專門本身寫一個插件系統,叫 Tapable , 主要提供了註冊和調用插件的功能。

咱們先弄清楚這裏須要用到的 SyncHookAsyncSeriesHook

SyncHook

SyncHook 爲串行同步執行,不關心事件處理函數的返回值,在觸發事件以後,會按照事件註冊的前後順序執行全部的事件處理函數。

在 tapable 解構的 SyncHook 是一個類,註冊事件需先建立實例,建立實例時支持傳入一個數組,數組內存儲事件觸發時傳入的參數,實例的 tap 方法用於註冊事件,支持傳入兩個參數,第一個參數爲事件名稱,在 Webpack 中通常用於存儲事件對應的插件名稱(名字隨意,只是起到註釋做用), 第二個參數爲事件處理函數,函數參數爲執行 call 方法觸發事件時所傳入的參數的形參。

AsyncSeriesHook

AsyncSeriesHook 爲異步串行執行,經過 tapAsync 註冊的事件,經過 callAsync 觸發,經過 tapPromise 註冊的事件,經過 promise 觸發,能夠調用 then 方法。

注: 異步串行是指,事件處理函數內三個定時器的異步執行時間分別爲 1s、2s 和 3s,而三個事件處理函數執行完總共用時接近 6s,因此三個事件處理函數執行是須要排隊的,必須一個一個執行,當前事件處理函數執行完才能執行下一個。

AsyncSeriesHook 的 next 執行機制更像 Express 和 Koa 中的中間件,在註冊事件的回調中若是不調用 next,則在觸發事件時會在沒有調用 next 的事件處理函數的位置 「卡死」,即不會繼續執行後面的事件處理函數,只有都調用 next 才能繼續,而最後一個事件處理函數中調用 next 決定是否調用 callAsync 的回調。

總結

在 tapable 源碼中,註冊事件的方法 tab、tapSync、tapPromise 和觸發事件的方法 call、callAsync、promise 都是經過 compile 方法快速編譯出來的,咱們本文中這些方法的實現只是遵守了 tapable 庫這些 「鉤子」 的事件處理機制進行了模擬,以方便咱們瞭解 tapable,爲學習 Webpack 原理作了一個鋪墊,在 Webpack 中,這些 「鉤子」 的真正做用就是將經過配置文件讀取的插件與插件、加載器與加載器之間進行鏈接,「並行」 或 「串行」 執行。

咱們對tapable的hooks有了必定理解, 繼續往下走:

若是沒有出錯, 咱們會執行下面這個方法

this.compile(onCompiled);
複製代碼

compile方法

這個compile不是上面的對象實例compile, 而是在Compiler中定義的compile方法

代碼中的new Compilation(params)Compilation是一個2000多行代碼的構造函數, 獲取到的compilation實例,它就是咱們須要的編譯對象.

compilation 會存儲編譯一個 entry 的全部信息,包括他的依賴,對應的配置等

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);       //最主要的在這裏, 獲取compliation實例

        const logger = compilation.getLogger("webpack.Compiler");

        logger.time("make hook");
        this.hooks.make.callAsync(compilation, err => {
            logger.timeEnd("make hook");
            if (err) return callback(err);

            process.nextTick(() => {    //將 callback 添加到下一個時間點的隊列。 在 JavaScript 堆棧上的當前操做運行完成以後以及容許事件循環繼續以前,此隊列會被徹底耗盡。
                logger.time("finish compilation");
                compilation.finish(err => {             //下面的finish方法
                    logger.timeEnd("finish compilation");
                    if (err) return callback(err);

                    logger.time("seal compilation");
                    compilation.seal(err => {
                        logger.timeEnd("seal compilation");
                        if (err) return callback(err);

                        logger.time("afterCompile hook");
                        this.hooks.afterCompile.callAsync(compilation, err => {
                            logger.timeEnd("afterCompile hook");
                            if (err) return callback(err);

                            return callback(null, compilation);
                        });
                    });
                });
            });
        });
    });
}
複製代碼

compilation.finish()

收集依賴

finish(callback) {
    const { moduleGraph, modules } = this;
    for (const module of modules) {
        moduleGraph.finishModule(module);
    }
    this.hooks.finishModules.callAsync(modules, err => {
        if (err) return callback(err);

        // extract warnings and errors from modules
        for (const module of modules) {
            this.reportDependencyErrorsAndWarnings(module, [module]);
            const errors = module.getErrors();
            if (errors !== undefined) {
                if (module.isOptional(this.moduleGraph)) {
                    for (const error of errors) {
                        if (!error.module) {
                            error.module = module;
                        }
                        this.warnings.push(error);
                    }
                } else {
                    for (const error of errors) {
                        if (!error.module) {
                            error.module = module;
                        }
                        this.errors.push(error);
                    }
                }
            }
            const warnings = module.getWarnings();
            if (warnings !== undefined) {
                for (const warning of warnings) {
                    if (!warning.module) {
                        warning.module = module;
                    }
                    this.warnings.push(warning);
                }
            }
        }

        callback();
    });
}
複製代碼

compilation.seal()

把全部依賴的模塊都經過對應的模板 render 出一個拼接好的字符串

seal(callback) {
    const chunkGraph = new ChunkGraph(this.moduleGraph);
    this.chunkGraph = chunkGraph;

    for (const module of this.modules) {
        ChunkGraph.setChunkGraphForModule(module, chunkGraph);
    }

    this.hooks.seal.call();

    while (this.hooks.optimizeDependencies.call(this.modules)) {
        /* empty */
    }
    this.hooks.afterOptimizeDependencies.call(this.modules);

    this.hooks.beforeChunks.call();
    for (const [name, dependencies] of this.entryDependencies) {
        const chunk = this.addChunk(name);
        chunk.name = name;
        const entrypoint = new Entrypoint(name);
        entrypoint.setRuntimeChunk(chunk);
        this.namedChunkGroups.set(name, entrypoint);
        this.entrypoints.set(name, entrypoint);
        this.chunkGroups.push(entrypoint);
        connectChunkGroupAndChunk(entrypoint, chunk);

        for (const dep of dependencies) {
            entrypoint.addOrigin(null, { name }, dep.request);

            const module = this.moduleGraph.getModule(dep);
            if (module) {
                chunkGraph.connectChunkAndModule(chunk, module);
                chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
                this.assignDepth(module);
            }
        }
    }
    buildChunkGraph(
        this,
        /** @type {Entrypoint[]} */ (this.chunkGroups.slice())
    );
    this.hooks.afterChunks.call(this.chunks);

    this.hooks.optimize.call();

    while (this.hooks.optimizeModules.call(this.modules)) {
        /* empty */
    }
    this.hooks.afterOptimizeModules.call(this.modules);

    while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {
        /* empty */
    }
    this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);

    this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
        if (err) {
            return callback(
                makeWebpackError(err, "Compilation.hooks.optimizeTree")
            );
        }

        this.hooks.afterOptimizeTree.call(this.chunks, this.modules);

        while (this.hooks.optimizeChunkModules.call(this.chunks, this.modules)) {
            /* empty */
        }
        this.hooks.afterOptimizeChunkModules.call(this.chunks, this.modules);

        const shouldRecord = this.hooks.shouldRecord.call() !== false;

        this.hooks.reviveModules.call(this.modules, this.records);
        this.hooks.beforeModuleIds.call(this.modules);
        this.hooks.moduleIds.call(this.modules);
        this.hooks.optimizeModuleIds.call(this.modules);
        this.hooks.afterOptimizeModuleIds.call(this.modules);

        this.hooks.reviveChunks.call(this.chunks, this.records);
        this.hooks.beforeChunkIds.call(this.chunks);
        this.hooks.chunkIds.call(this.chunks);
        this.hooks.optimizeChunkIds.call(this.chunks);
        this.hooks.afterOptimizeChunkIds.call(this.chunks);

        this.sortItemsWithChunkIds();

        if (shouldRecord) {
            this.hooks.recordModules.call(this.modules, this.records);
            this.hooks.recordChunks.call(this.chunks, this.records);
        }

        this.hooks.optimizeCodeGeneration.call(this.modules);

        this.hooks.beforeModuleHash.call();
        this.createModuleHashes();
        this.hooks.afterModuleHash.call();

        this.hooks.beforeCodeGeneration.call();
        this.codeGenerationResults = this.codeGeneration();
        this.hooks.afterCodeGeneration.call();

        this.hooks.beforeRuntimeRequirements.call();
        this.processRuntimeRequirements(this.entrypoints.values());
        this.hooks.afterRuntimeRequirements.call();

        this.hooks.beforeHash.call();
        this.createHash();
        this.hooks.afterHash.call();

        if (shouldRecord) {
            this.hooks.recordHash.call(this.records);
        }

        this.clearAssets();

        this.hooks.beforeModuleAssets.call();
        this.createModuleAssets();

        const cont = () => {
            this.hooks.additionalChunkAssets.call(this.chunks);
            this.summarizeDependencies();
            if (shouldRecord) {
                this.hooks.record.call(this, this.records);
            }

            this.hooks.additionalAssets.callAsync(err => {
                if (err) {
                    return callback(
                        makeWebpackError(err, "Compilation.hooks.additionalAssets")
                    );
                }
                this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
                    if (err) {
                        return callback(
                            makeWebpackError(err, "Compilation.hooks.optimizeChunkAssets")
                        );
                    }
                    this.hooks.afterOptimizeChunkAssets.call(this.chunks);
                    this.hooks.optimizeAssets.callAsync(this.assets, err => {
                        if (err) {
                            return callback(
                                makeWebpackError(err, "Compilation.hooks.optimizeAssets")
                            );
                        }
                        this.hooks.afterOptimizeAssets.call(this.assets);
                        if (this.hooks.needAdditionalSeal.call()) {
                            this.unseal();
                            return this.seal(callback);
                        }
                        this.hooks.finishAssets.callAsync(this.assets, err => {
                            if (err) {
                                return callback(
                                    makeWebpackError(err, "Compilation.hooks.finishAssets")
                                );
                            }
                            this.hooks.afterFinishAssets.call(this.assets);
                            this.cache.storeBuildDependencies(
                                this.buildDependencies,
                                err => {
                                    if (err) {
                                        return callback(err);
                                    }
                                    return this.hooks.afterSeal.callAsync(callback);
                                }
                            );
                        });
                    });
                });
            });
        };

        if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
            this.hooks.beforeChunkAssets.call();
            this.createChunkAssets(err => {
                if (err) {
                    return callback(err);
                }
                cont();
            });
        } else {
            cont();
        }
    });
}
複製代碼

compile方法的params參數

createNormalModuleFactory () {
    const normalModuleFactory = new NormalModuleFactory({       
        context: this.options.context,                      //node工做進程的文件目錄
        fs: this.inputFileSystem,
        resolverFactory: this.resolverFactory,
        options: this.options.module || {}                  //這裏就是webpack.config.js的module對象
    });
    this.hooks.normalModuleFactory.call(normalModuleFactory);
    return normalModuleFactory;
}

createContextModuleFactory () {
    const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
    this.hooks.contextModuleFactory.call(contextModuleFactory);
    return contextModuleFactory;
}

const params = {
    normalModuleFactory: this.createNormalModuleFactory(),          //這裏會解析webpack.config.js的loaders, 並經過loaders將它們轉換成js代碼
    contextModuleFactory: this.createContextModuleFactory()
};
return params;
複製代碼

onCompiled方法

const onCompiled = (err, compilation) => {
    if (err) return finalCallback(err);

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

    process.nextTick(() => {
        logger = compilation.getLogger("webpack.Compiler");
        logger.time("emitAssets");
        this.emitAssets(compilation, err => {
            logger.timeEnd("emitAssets");
            if (err) return finalCallback(err);

            if (compilation.hooks.needAdditionalPass.call()) {
                compilation.needAdditionalPass = true;

                const stats = new Stats(compilation);
                stats.startTime = startTime;
                stats.endTime = Date.now();
                logger.time("done hook");
                this.hooks.done.callAsync(stats, err => {
                    logger.timeEnd("done hook");
                    if (err) return finalCallback(err);

                    this.hooks.additionalPass.callAsync(err => {
                        if (err) return finalCallback(err);
                        this.compile(onCompiled);               //遞歸調用對下一層進行編譯
                    });
                });
                return;
            }

            logger.time("emitRecords");
            this.emitRecords(err => {
                logger.timeEnd("emitRecords");
                if (err) return finalCallback(err);

                const stats = new Stats(compilation);
                stats.startTime = startTime;
                stats.endTime = Date.now();
                logger.time("done hook");
                this.hooks.done.callAsync(stats, err => {
                    logger.timeEnd("done hook");
                    if (err) return finalCallback(err);
                    return finalCallback(null, stats);
                });
            });
        });
    });
};
複製代碼

後話

大概的理了理webpack編譯的主要思路。 事實上webpack上還有不少其餘的配置, 感受還有不少東西沒搞明白是作什麼用的。

參考:

webpack 處理流程分析

相關文章
相關標籤/搜索