Webpack系列-第三篇流程雜記

系列文章

Webpack系列-第一篇基礎雜記
Webpack系列-第二篇插件機制雜記
Webpack系列-第三篇流程雜記

前言

本文章個人理解, 只是爲了理清webpack流程, 沒有關注內部過多細節, 如有錯誤, 請輕噴~

調試

1.使用以下命令運行項目,./scripts/build.js是你想要開始調試的地方

node --inspect-brk ./scripts/build.js --inline --progress

2.打開chrome://inspect/#devices即可調試

流程圖

image

入口

入口處在bulid.js,可以看到其中的代碼是先實例化webpack,然後調用compiler的run方法

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

entry-option(compiler)

webpack.js

webpack在node_moduls下面的\webpack\lib\webpack.js(在此前面有入口參數合併),找到該文件可以看到相關的代碼如下

const webpack = (options, callback) => {
    ......
    let compiler;
    // 處理多個入口
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        // webpack的默認參數
        options = new WebpackOptionsDefaulter().process(options);
        console.log(options) // 見下圖
        // 實例化compiler
        compiler = new Compiler(options.context);
        compiler.options = options;
        // 對webpack的運行環境處理
        new NodeEnvironmentPlugin().apply(compiler);
        // 根據上篇的tabpable可知 這裏是爲了註冊插件
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                plugin.apply(compiler);
            }
        }
        // 觸發兩個事件點 environment/afterEnviroment
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        // 設置compiler的屬性並調用默認配置的插件,同時觸發事件點entry-option
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        ......
        compiler.run(callback);
    }
    return compiler;
};

image
可以看出options保存的就是本次webpack的一些配置參數,而其中的plugins屬性則是webpack中最重要的插件

new WebpackOptionsApply().process

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;
    compiler.dependencies = options.dependencies;
    if (typeof options.target === "string") {
        let JsonpTemplatePlugin;
        let FetchCompileWasmTemplatePlugin;
        let ReadFileCompileWasmTemplatePlugin;
        let NodeSourcePlugin;
        let NodeTargetPlugin;
        let NodeTemplatePlugin;
    
        switch (options.target) {
            case "web":
                JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
                FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin");
                NodeSourcePlugin = require("./node/NodeSourcePlugin");
                new JsonpTemplatePlugin().apply(compiler);
                new FetchCompileWasmTemplatePlugin({
                    mangleImports: options.optimization.mangleWasmImports
                }).apply(compiler);
                new FunctionModulePlugin().apply(compiler);
                new NodeSourcePlugin(options.node).apply(compiler);
                new LoaderTargetPlugin(options.target).apply(compiler);
                break;
            case "webworker":......
            ......
        }
    }
    new JavascriptModulesPlugin().apply(compiler);
    new JsonModulesPlugin().apply(compiler);
    new WebAssemblyModulesPlugin({
        mangleImports: options.optimization.mangleWasmImports
    }).apply(compiler);
    
    new EntryOptionPlugin().apply(compiler);
    // 觸發事件點entry-options並傳入參數 context和entry 
    compiler.hooks.entryOption.call(options.context, options.entry);
    new CompatibilityPlugin().apply(compiler);
    ......
    new ImportPlugin(options.module).apply(compiler);
    new SystemPlugin(options.module).apply(compiler);
}

run(compiler)

調用run時,會先在內部觸發beforeRun事件點,然後再在讀取recodes關於records可以參考該文檔)之前觸發run事件點,這兩個事件都是異步的形式,注意run方法是實際上整個webpack打包流程的入口。可以看到,最後調用的是compile方法,同時傳入的是onCompiled函數

run(callback) {
    if (this.running) return callback(new ConcurrentCompilationError());
    const finalCallback = (err, stats) => {
        ......
    };
    this.running = true;
    
    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);
            });
        });
    });
}

compile(compiler)

compile方法主要上觸發beforeCompile、compile、make等事件點,並實例化compilation,這裏我們可以看到傳給compile的newCompilationParams參數, 這個參數在後面相對流程中也是比較重要,可以在這裏先看一下

compile(callback) {
    const params = this.newCompilationParams();
    // 觸發事件點beforeCompile,並傳入參數CompilationParams
    this.hooks.beforeCompile.callAsync(params, err => {
        if (err) return callback(err);
        // 觸發事件點compile,並傳入參數CompilationParams
        this.hooks.compile.call(params);
        // 實例化compilation
        const compilation = this.newCompilation(params);
        // 觸發事件點make
        this.hooks.make.callAsync(compilation, err => {
            ....
        });
    });
}

newCompilationParams返回的參數分別是兩個工廠函數和一個Set集合

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

compilation(compiler)

從上面的compile方法看, compilation是通過newCompilation方法調用生成的,然後觸發事件點thisCompilation和compilation,可以看出compilation在這兩個事件點中最早當成參數傳入,如果你在編寫插件的時候需要儘快使用該對象,則應該在該兩個事件中進行。

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;
    // 觸發事件點thisCompilation和compilation, 同時傳入參數compilation和params
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
}

下面是打印出來的compilation屬性
image

關於這裏爲什麼要有thisCompilation這個事件點和子編譯器(childCompiler),可以參考該文章
總結起來就是:

子編譯器擁有完整的模塊解析和chunk生成階段,但是少了某些事件點,如"make", "compile", "emit", "after-emit", "invalid", "done", "this-compilation"。 也就是說我們可以利用子編譯器來獨立(於父編譯器)跑完一個核心構建流程,額外生成一些需要的模塊或者chunk。

make(compiler)

從上面的compile方法知道, 實例化Compilation後就會觸發make事件點了。
觸發了make時, 因爲webpack在前面實例化SingleEntryPlugin或者MultleEntryPlugin,SingleEntryPlugin則在其apply方法中註冊了一個make事件,

apply(compiler) {
    compiler.hooks.compilation.tap(
        "SingleEntryPlugin",
        (compilation, { normalModuleFactory }) => {
            compilation.dependencyFactories.set(
                SingleEntryDependency,
                normalModuleFactory  // 工廠函數,存在compilation的dependencyFactories集合
            );
        }
    );
    
    compiler.hooks.make.tapAsync(
        "SingleEntryPlugin",
        (compilation, callback) => {
            const { entry, name, context } = this;
    
            const dep = SingleEntryPlugin.createDependency(entry, name);
            // 進入到addEntry
            compilation.addEntry(context, dep, name, callback);
        }
    );
}

事實上addEntry調用的是Comilation._addModuleChain,acquire函數比較簡單,主要是處理module時如果任務太多,就將moduleFactory.create存入隊列等待

_addModuleChain(context, dependency, onModule, callback) {
    ......
    // 取出對應的Factory
    const Dep = /** @type {DepConstructor} */ (dependency.constructor);
    const moduleFactory = this.dependencyFactories.get(Dep);
    ......
    this.semaphore.acquire(() => {
        moduleFactory.create(
            {
                contextInfo: {
                    issuer: "",
                    compiler: this.compiler.name
                },
                context: context,
                dependencies: [dependency]
            },
            (err, module) => {
                ......
            }
        );
    });
    }

moduleFactory.create則是收集一系列信息然後創建一個module傳入回調

buildModule(compilation)

回調函數主要上執行buildModule方法

this.buildModule(module, false, null, null, err => {
    ......
    afterBuild();
});
buildModule(module, optional, origin, dependencies, thisCallback) {
    // 處理回調函數
    let callbackList = this._buildingModules.get(module);
    if (callbackList) {
        callbackList.push(thisCallback);
        return;
    }
    this._buildingModules.set(module, (callbackList = [thisCallback]));
    
    const callback = err => {
        this._buildingModules.delete(module);
        for (const cb of callbackList) {
            cb(err);
        }
    };
    // 觸發buildModule事件點
    this.hooks.buildModule.call(module);
    module.build(
        this.options,
        this,
        this.resolverFactory.get("normal", module.resolveOptions),
        this.inputFileSystem,
        error => {
            ......
        }
    );
    }

build方法中調用的是doBuild,doBuild又通過runLoaders獲取loader相關的信息並轉換成webpack需要的js文件,最後通過doBuild的回調函數調用parse方法,創建依賴Dependency並放入依賴數組

return this.doBuild(options, compilation, resolver, fs, err => {
    // 在createLoaderContext函數中觸發事件normal-module-loader
    const loaderContext = this.createLoaderContext(
        resolver,
        options,
        compilation,
        fs
    );
    .....
    const handleParseResult = result => {
        this._lastSuccessfulBuildMeta = this.buildMeta;
        this._initBuildHash(compilation);
        return callback();
    };
    
    try {
        // 調用parser.parse
        const result = this.parser.parse(
            this._ast || this._source.source(),
            {
                current: this,
                module: this,
                compilation: compilation,
                options: options
            },
            (err, result) => {
                if (err) {
                    handleParseError(err);
                } else {
                    handleParseResult(result);
                }
            }
        );
        if (result !== undefined) {
            // parse is sync
            handleParseResult(result);
        }
    } catch (e) {
        handleParseError(e);
    }
    });

在ast轉換過程中也很容易得到了需要依賴的哪些其他模塊

succeedModule(compilation)

最後執行了module.build的回調函數,觸發了事件點succeedModule,並回到Compilation.buildModule函數的回調函數

module.build(
    this.options,
    this,
    this.resolverFactory.get("normal", module.resolveOptions),
    this.inputFileSystem,
    error => {
        ......
        觸發了事件點succeedModule
        this.hooks.succeedModule.call(module);
        return callback();
    }
);

this.buildModule(module, false, null, null, err => {
    ......
    // 執行afterBuild
    afterBuild();
});

對於當前模塊,或許存在着多個依賴模塊。當前模塊會開闢一個依賴模塊的數組,在遍歷 AST 時,將 require() 中的模塊通過 addDependency() 添加到數組中。當前模塊構建完成後,webpack 調用 processModuleDependencies 開始遞歸處理依賴的 module,接着就會重複之前的構建步驟。

 Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
  // 根據依賴數組(dependencies)創建依賴模塊對象
  var factories = [];
  for (var i = 0; i < dependencies.length; i++) {
    var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
    factories[i] = [factory, dependencies[i]];
  }
  ...
  // 與當前模塊構建步驟相同
}

最後, 所有的模塊都會被放入到Compilation的modules裏面, 如下:
image
image

總結一下:

module 是 webpack 構建的核心實體,也是所有 module 的 父類,它有幾種不同子類:NormalModule , MultiModule , ContextModule , DelegatedModule 等,一個依賴對象(Dependency,還未被解析成模塊實例的依賴對象。比如我們運行 webpack 時傳入的入口模塊,或者一個模塊依賴的其他模塊,都會先生成一個 Dependency 對象。)經過對應的工廠對象(Factory)創建之後,就能夠生成對應的模塊實例(Module)。

seal(compilation)

構建module後, 就會調用Compilation.seal, 該函數主要是觸發了事件點seal, 構建chunk, 在所有 chunks 生成之後,webpack 會對 chunks 和 modules 進行一些優化相關的操作,比如分配id、排序等,並且觸發一系列相關的事件點

seal(callback) {
    // 觸發事件點seal
    this.hooks.seal.call();
    // 優化
    ......
    this.hooks.afterOptimizeDependencies.call(this.modules);
    
    this.hooks.beforeChunks.call();
    // 生成chunk
    for (const preparedEntrypoint of this._preparedEntrypoints) {
        const module = preparedEntrypoint.module;
        const name = preparedEntrypoint.name;
        // 整理每個Module和chunk,每個chunk對應一個輸出文件。
        const chunk = this.addChunk(name);
        const entrypoint = new Entrypoint(name);
        entrypoint.setRuntimeChunk(chunk);
        entrypoint.addOrigin(null, name, preparedEntrypoint.request);
        this.namedChunkGroups.set(name, entrypoint);
        this.entrypoints.set(name, entrypoint);
        this.chunkGroups.push(entrypoint);
    
        GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
        GraphHelpers.connectChunkAndModule(chunk, module);
    
        chunk.entryModule = module;
        chunk.name = name;
    
        this.assignDepth(module);
    }
    this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());
    this.sortModules(this.modules);
    this.hooks.afterChunks.call(this.chunks);
    
    this.hooks.optimize.call();
    
    ......
    this.hooks.afterOptimizeModules.call(this.modules);
    
    ......
    this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
    
    this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
        ......
        this.hooks.beforeChunkAssets.call();
        this.createChunkAssets();  // 生成對應的Assets
        this.hooks.additionalAssets.callAsync(...)
    });
    }

每個 chunk 的生成就是找到需要包含的 modules。這裏大致描述一下 chunk 的生成算法:

1.webpack 先將 entry 中對應的 module 都生成一個新的 chunk
2.遍歷 module 的依賴列表,將依賴的 module 也加入到 chunk 中
3.如果一個依賴 module 是動態引入的模塊,那麼就會根據這個 module 創建一個新的 chunk,繼續遍歷依賴
4.重複上面的過程,直至得到所有的 chunks

chunk屬性圖
image

beforeChunkAssets && additionalChunkAssets(Compilation)

在觸發這兩個事件點的中間時, 會調用Compilation.createCHunkAssets來創建assets,

createChunkAssets() {
    ......
    // 遍歷chunk
    for (let i = 0; i < this.chunks.length; i++) {
        const chunk = this.chunks[i];
        chunk.files = [];
        let source;
        let file;
        let filenameTemplate;
        try {
            // 調用何種Template
            const template = chunk.hasRuntime()
                ? this.mainTemplate
                : this.chunkTemplate;
            const manifest = template.getRenderManifest({
                chunk,
                hash: this.hash,
                fullHash: this.fullHash,
                outputOptions,
                moduleTemplates: this.moduleTemplates,
                dependencyTemplates: this.dependencyTemplates
            }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
            for (const fileManifest of manifest) {
                .....
                }
                .....
                // 寫入assets對象
                this.assets[file] = source;
                chunk.files.push(file);
                this.hooks.chunkAsset.call(chunk, file);
                alreadyWrittenFiles.set(file, {
                    hash: usedHash,
                    source,
                    chunk
                });
            }
        } catch (err) {
            ......
        }
    }
    }

createChunkAssets會生成文件名和對應的文件內容,並放入Compilation.assets對象, 這裏有四個Template 的子類,分別是 MainTemplate.js , ChunkTemplate.js ,ModuleTemplate.js , HotUpdateChunkTemplate.js

  • MainTemplate.js: 對應了在 entry 配置的入口 chunk 的渲染模板
  • ChunkTemplate: 動態引入的非入口 chunk 的渲染模板
  • ModuleTemplate.js: chunk 中的 module 的渲染模板
  • HotUpdateChunkTemplate.js: 對熱替換模塊的一個處理。

模塊封裝(引用自http://taobaofed.org/blog/201...
模塊在封裝的時候和它在構建時一樣,都是調用各模塊類中的方法。封裝通過調用 module.source() 來進行各操作,比如說 require() 的替換。

MainTemplate.prototype.requireFn = "__webpack_require__";
MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) {
    var buf = [];
    // 每一個module都有一個moduleId,在最後會替換。
    buf.push("function " + this.requireFn + "(moduleId) {");
    buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash)));
    buf.push("}");
    buf.push("");
    ... // 其餘封裝操作
};

最後看看Compilation.assets對象
image

done(Compiler)

最後一步,webpack 調用 Compiler 中的 emitAssets() ,按照 output 中的配置項將文件輸出到了對應的 path 中,從而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則需要在 emit 觸發後對自定義插件進行擴展。

總結

webpack的內部核心還是在於compilationcompilermodulechunk等對象或者實例。寫下這篇文章也有助於自己理清思路,學海無涯~~~

引用

玩轉webpack(一):webpack的基本架構和構建流程
玩轉webpack(二):webpack的核心對象
細說 webpack 之流程篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章