webpack4 整体打包流程分析

165 阅读7分钟

webpack 代码之难读,可以说在我读过的源码里面也是前几的
究其原因,就是里面大量应用了 发布订阅模式 ,强依赖了 Tapable
导致 所有代码的执行流程支离破碎,经常读着读着,就会发现, “诶,这个函数是哪里来的?”

1. 编译准备工作

1.1 初始化 Compiler

webpack/lib/webpack.js#41
创建 Compiler 对象 -> 这个是横跨整个 打包周期的,拥有着文件读写的能力

compiler = new Compiler(options.context); 

webpack/lib/webpack.js#43
将 node 读文件的能力 赋予 Compiler

new NodeEnvironmentPlugin({
          infrastructureLogging: options.infrastructureLogging
  }).apply(compiler);

1.2 开启编译

webpack/lib/Compiler.js#247
开启编译

run(callback) {
  ....
  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);
                        /** 注意这里的 onCompiled,在后文会提及 */
                        this.compile(onCompiled);
                });
        });
    });
}

1.2 准备编译的工具

webpack/lib/Compiler.js#660
webpack/lib/Compiler.js#632
开启编译->

  1. 准备 编译的 paramsnormalModuleFactory模块工厂)
  2. 创建 Compilation。 在一个 编译的过程中,会出现多个 Compilation,如,监听到文件的改动,就会出现一个新的 Compilation,它获取了 前文 Compiler 的读写能力
  3. 触发 compilation & make 钩子 ->
  4. SingleEntryPlugin 监听 第3步的钩子执行
  5. JavascriptModulesPlugin 监听 第3步的钩子执行
compile(callback) {
/** 准备打包的工具类 */
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
        if (err) return callback(err);

        this.hooks.compile.call(params);
        /** 创建一个 Compilation */
        const compilation = this.newCompilation(params);
        /** 这里触发了 make 钩子 */
        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);
                                });
                        });
                });
        });
    });
}

newCompilationParams() {
        const params = {
                /** createNormalModuleFactory 创建 模块打包 工厂 */
                /** 会在后文中的 SingleEntryPlugin 注入 Compilation */
		normalModuleFactory: this.createNormalModuleFactory(),
                contextModuleFactory: this.createContextModuleFactory(),
                compilationDependencies: new Set()
        };
        return params;
}

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);
    /** 这里触发了 SingleEntryPlugin 的钩子 */ 
    this.hooks.compilation.call(compilation, params); 
    return compilation;
}

webpack/lib/SingleEntryPlugin.js#29
为上文提到的 Compilation 挂载 模块工厂的能力 和 依赖处理的容器
这个在后文中的 处理 依赖 极为有用
调用 CompilationaddEntry,真正地开始编译流程

apply(compiler) {
    compiler.hooks.compilation.tap(
        "SingleEntryPlugin",
        (compilation, { normalModuleFactory }) => {
                compilation.dependencyFactories.set(
                        SingleEntryDependency,
                        /** 让 compilation 具有了 模块工厂的能力 */
                        normalModuleFactory 
                );
        }
    );

    compiler.hooks.make.tapAsync(
            "SingleEntryPlugin",
            (compilation, callback) => {
                /** 入口文件 */ 
                // chunkname
                /** 根目录 */ 
                const { entry, name, context } = this;
                /** 依赖关系处理 容器 */ 
                const dep = SingleEntryPlugin.createDependency(entry, name);
                /** 调用了 addEntry 函数,开始了 编译的过程 */
                compilation.addEntry(context, dep, name, callback);
            }
    );
}

webpack/lib/JavascriptModulesPlugin.js#14
挂载了一堆的 hook,作用就是 处理 javascript 为 AST 语法树,这里的依赖是 acorn

apply(compiler) {
    compiler.hooks.compilation.tap(
        "JavascriptModulesPlugin",
        (compilation, { normalModuleFactory }) => {
                normalModuleFactory.hooks.createParser
                        .for("javascript/auto")
                        .tap("JavascriptModulesPlugin", options => {
                                return new Parser(options, "auto");
                        });
                normalModuleFactory.hooks.createParser
                        .for("javascript/dynamic")
                        .tap("JavascriptModulesPlugin", options => {
                                return new Parser(options, "script");
                        });
                normalModuleFactory.hooks.createParser
                        .for("javascript/esm")
                        .tap("JavascriptModulesPlugin", options => {
                                return new Parser(options, "module");
                        });
                normalModuleFactory.hooks.createGenerator
                        .for("javascript/auto")
                        .tap("JavascriptModulesPlugin", () => {
                                return new JavascriptGenerator();
                        });
                normalModuleFactory.hooks.createGenerator
                        .for("javascript/dynamic")
                        .tap("JavascriptModulesPlugin", () => {
                                return new JavascriptGenerator();
                        });
                normalModuleFactory.hooks.createGenerator
                        .for("javascript/esm")
                        .tap("JavascriptModulesPlugin", () => {
                                return new JavascriptGenerator();
                        });
                ...
}

2. 开始打包

2.1 开启打包流程

webpack/lib/Compilation.js#1143
在 上文中提到的 SingleEntryPluginmake 钩子 调用了 CompilationaddEntry
调用了 内部函数 _addModuleChain开启了打包的流程

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

    const slot = {
            name: name,
            // TODO webpack 5 remove `request`
            request: null,
            module: null
    };
    ...
    this._addModuleChain(
            context,
            entry,
            module => {
                    this.entries.push(module);
            },
            (err, module) => {
                    if (err) {
                            this.hooks.failedEntry.call(entry, name, err);
                            return callback(err);
                    }

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

2.2 _addModuleChain 打包流程

webpack/lib/Compilation.js#1033
开启了一个 并发打包的过程
但是,还是但是,这里并不是一个 安安稳稳就执行下去的过程,

  1. 先调用了 normalModuleFactorycreate
  2. create 执行完之后,再调用 接下来的一堆 回调函数
  3. 回调函数中 才开始 buildModule编译模块
  4. buildModule 又有回调函数 afterBuild ,用来 加载依赖
_addModuleChain(context, dependency, onModule, callback) {
    const start = this.profile && Date.now();
    const currentProfile = this.profile && {};
    ...
    // 并发打包
    this.semaphore.acquire(() => {
        moduleFactory.create(
            {
                contextInfo: {
                    issuer: "",
                    compiler: this.compiler.name
                },
                context: context,
                dependencies: [dependency]
        },
	(err, module) => {
                if (err) {
                    this.semaphore.release();
                    return errorAndCallback(new EntryModuleNotFoundError(err));
                }

                let afterFactory;

                if (currentProfile) {
                        afterFactory = Date.now();
                        currentProfile.factory = afterFactory - start;
                }
                // 将模块内容加到缓存中
                const addModuleResult = this.addModule(module);
                module = addModuleResult.module;

                onModule(module);

                dependency.module = module;
                module.addReason(null, dependency);

                const afterBuild = () => {
                        if (addModuleResult.dependencies) {
                        /** 加载依赖 */ 	
                        this.processModuleDependencies(module, err => {
                                if (err) return callback(err);
                                callback(null, module);
                            });
                    } else {
                        return callback(null, module);
                    }
                };

                if (addModuleResult.issuer) {
                    if (currentProfile) {
                        module.profile = currentProfile;
                    }
                }

                if (addModuleResult.build) {
                        // 开始编译模块
                       this.buildModule(module, false, null, null, err => {
                            if (err) {
                                    this.semaphore.release();
                                    return errorAndCallback(err);
                            }

                            if (currentProfile) {
                                    const afterBuilding = Date.now();
                                    currentProfile.building = afterBuilding - afterFactory;
                            }

                            this.semaphore.release();
                            afterBuild();
                    });
                } else {
                    this.semaphore.release();
                    this.waitForBuildingFinished(module, afterBuild);
                }
            }
        );
    });
}

2.3 准备某个模块的打包原料

webpack/lib/NormalModuleFactory.js#373
承接 上一个 函数中的 create 调用,先是触发了 beforeResolve钩子,又触发了factory钩子
注意的是,factory 会去处理 loader,所以是 将一个一个模块交给了 loader 处理一遍之后,再放入缓存中
然后 执行 上文中 _addModuleChain插入 的callback,将createdModule返回

create(data, callback) {
    const dependencies = data.dependencies;
    const cacheEntry = dependencyCache.get(dependencies[0]);
    if (cacheEntry) return callback(null, cacheEntry);
    const context = data.context || this.context;
    const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
    const request = dependencies[0].request;
    const contextInfo = data.contextInfo || {};
    this.hooks.beforeResolve.callAsync(
            {
                    contextInfo,
                    resolveOptions,
                    context,
                    request,
                    dependencies
            },
            (err, result) => {
                    if (err) return callback(err);

                    // Ignored
                    if (!result) return callback();
                    /** !! 触发了 factory 钩子 */
                    const factory = this.hooks.factory.call(null);

                    // Ignored
                    if (!factory) return callback();
                    /** 将模块丢给 loader 处理 */ 
                    factory(result, (err, module) => {
                            if (err) return callback(err);

                            if (module && this.cachePredicate(module)) {
                                    for (const d of dependencies) {
                                    /** 将 模块放入 依赖缓存中 */ 
         				dependencyCache.set(d, module);
                                    }
                            }
                /** 这里的 callback 就是 上文中 _addModuleChain 调用  create 传入的回调 */
                /** 调用链路太长,防止忘记再提醒一次 */ 
                            callback(null, module);
                    });
            }
    );
}

webpack/lib/NormalModuleFactory.js#124
上文中的 factory钩子 就是在 NormalModuleFactoryconstructor注册的,省略的一堆的定义之后,如下文
将处理好的结果放到 NormalModule
这里是做好了 build的前置工作,将文件内容、loader、parser、options 都封装完毕,并进行缓存

this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
    /** 处理loader */
    let resolver = this.hooks.resolver.call(null); // 

    // Ignored  loader 被处理完毕
    if (!resolver) return callback();

    resolver(result, (err, data) => {
            if (err) return callback(err);

            // Ignored
            if (!data) return callback();

            // direct module
            if (typeof data.source === "function") return callback(null, data);

            this.hooks.afterResolve.callAsync(data, (err, result) => {
                    if (err) return callback(err);

                    // Ignored
                    if (!result) return callback();

                    let createdModule = this.hooks.createModule.call(result);
                    if (!createdModule) {
                        if (!result.request) {
                           return callback(new Error("Empty dependency (no request)"));
                        }
                        /** 将处理完成之后的结果放在 NormalModule 中 */ 
                        createdModule = new NormalModule(result);
                    }

                    createdModule = this.hooks.module.call(createdModule, result);

                    return callback(null, createdModule);
            });
    });
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
    const contextInfo = data.contextInfo;
    const context = data.context;
    const request = data.request;

    const loaderResolver = this.getResolver("loader");
    const normalResolver = this.getResolver("normal", data.resolveOptions);

    asyncLib.parallel(
            [
                    callback =>
                            this.resolveRequestArray(
                                    contextInfo,
                                    context,
                                    elements,
                                    loaderResolver,
                                    callback
                            ),
                    callback => {
                            ...
                    }
            ],
            (err, results) => {
                    ...
                    asyncLib.parallel(
                            [
                                    ...
                            ],
                            (err, results) => {
                                ...
                                process.nextTick(() => {
                                    const type = settings.type;
                                    const resolveOptions = settings.resolve;
                                    callback(null, {
                                        context: context,
                                        request: loaders
                                                .map(loaderToIdent)
                                                .concat([resource])
                                                .join("!"),
                                        dependencies: data.dependencies,
                                        userRequest,
                                        rawRequest: request,
                                        loaders,
                                        resource,
                                        matchResource,
                                        resourceResolveData,
                                        settings,
                                        type,
                                        /** 就是上文中的 parser,解析 AST 语法树的 */ 
                                        parser: this.getParser(type, settings.parser),
                                        generator: this.getGenerator(type, settings.generator),
                                        resolveOptions
                                    });
                            });
                        }
                    );
            }
    );
});
}

2.4 正式编译

接下来就是进入 _addModuleChainbuildModule正式开始编译了 webpack/lib/Compilation.js#1112
这个函数其实在上文中写过,但是这里再写一次

this.buildModule(module, false, null, null, err => {
        if (err) {
                this.semaphore.release();
                return errorAndCallback(err);
        }

        if (currentProfile) {
                const afterBuilding = Date.now();
                currentProfile.building = afterBuilding - afterFactory;
        }

        this.semaphore.release();
        afterBuild();
});

webpack/lib/Compilation.js#723

  1. 调用了 module.build
  2. 触发了 succeedModule 回调(用来标注当前的进度的)
buildModule(module, optional, origin, dependencies, thisCallback) {
        let callbackList = this._buildingModules.get(module);
        if (callbackList) {
                callbackList.push(thisCallback);
                return;
        }
        this._buildingModules.set(module, (callbackList = [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();
            }
        );
}

2.5 真的编译doBuild + 准备好 ast、source 等数据

webpack/lib/NormalModule.js#427

  1. 初始化了一堆数据,如 _ast, _source
  2. 调用了 doBuild。。。,顾名思义,真的 在编译了
  3. 然后 调用 上文中提及的 parse 解析 AST 语法树
build(options, compilation, resolver, fs, callback) { //  真家伙开始编译了
    this.buildTimestamp = Date.now();
    this.built = true;
    this._source = null; // 源码		
    this._sourceSize = null;
    this._ast = null;  // AST 
    this._buildHash = "";
    this.error = null;
    this.errors.length = 0;
    this.warnings.length = 0;
    this.buildMeta = {};
    this.buildInfo = {
            cacheable: false,
            fileDependencies: new Set(),
            contextDependencies: new Set(),
            assets: undefined,
            assetsInfo: undefined
    };
    /** !!!!调用了 doBuild*/
    return this.doBuild(options, compilation, resolver, fs, err => {
            this._cachedSources.clear();

            // if we have an error mark module as failed and exit
            if (err) {
                    this.markModuleAsErrored(err);
                    this._initBuildHash(compilation);
                    return callback();
            }

            // check if this module should !not! be parsed.
            // if so, exit here;
            const noParseRule = options.module && options.module.noParse;
            if (this.shouldPreventParsing(noParseRule, this.request)) {
                    this._initBuildHash(compilation);
                    return callback();
            }

            ...

        try {
            const result = this.parser.parse(
                /** 当前的 AST 语法树 */
               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);
    }
    });
}

2.6 运行 loader 并调用 callback

webpack/lib/NormalModule.js#287

  1. 使用了 loader-runner 依赖 来运行当前的 准备的 module
  2. 然后给 上文中定义的各个 属性赋值
  3. 调用 callback
doBuild(options, compilation, resolver, fs, callback) {
    const loaderContext = this.createLoaderContext(
            resolver,
            options,
            compilation,
            fs
    );
     /** 先处理loader -> loader-runner */ 
   runLoaders(
    {
            resource: this.resource,
            loaders: this.loaders,
            context: loaderContext,
            readResource: fs.readFile.bind(fs)
    },
    (err, result) => {
            if (result) {
                    this.buildInfo.cacheable = result.cacheable;
                    /** 记录当前的依赖信息 */
                    this.buildInfo.fileDependencies = new Set(result.fileDependencies);
                    this.buildInfo.contextDependencies = new Set(
                            result.contextDependencies
                    );
            }

            ...

            const resourceBuffer = result.resourceBuffer;
            const source = result.result[0];
            const sourceMap = result.result.length >= 1 ? result.result[1] : null;
            const extraInfo = result.result.length >= 2 ? result.result[2] : null;

            if (!Buffer.isBuffer(source) && typeof source !== "string") {
                    const currentLoader = this.getCurrentLoader(loaderContext, 0);
                    const err = new Error(
                            `Final loader (${
                                    currentLoader
                                            ? compilation.runtimeTemplate.requestShortener.shorten(
                                                            currentLoader.loader
                                              )
                                            : "unknown"
                            }) didn't return a Buffer or String`
                    );
                    const error = new ModuleBuildError(this, err);
                    return callback(error);
            }

            this._source = this.createSource(
                    this.binary ? asBuffer(source) : asString(source),
                    resourceBuffer,
                    sourceMap
            );
            this._sourceSize = null;
            this._ast =
                    typeof extraInfo === "object" &&
                    extraInfo !== null &&
                    extraInfo.webpackAST !== undefined
                            ? extraInfo.webpackAST
                            : null;
            return callback();
    }
    );
}

2.7 梳理 callback 以及 onCompiled

我们可以简单的看一下,上一段doBuild中的 callback来源
webpack/lib/NormalModule.js#287
webpack/lib/NormalModule.js#445
webpack/lib/NormalModule.js#427
webpack/lib/Compilation.js#739 (这个 build 的回调函数底下还有一个 callback )
webpack/lib/Compilation.js#731
webpack/lib/Compilation.js#723 -> 里的 thisCallback
webpack/lib/Compilation.js#1112 -> 里的 afterBuild\ webpack/lib/Compilation.js#1097
webpack/lib/Compilation.js#1033
webpack/lib/Compilation.js#1165
webpack/lib/Compilation.js#1143
webpack/lib/SingleEntryPlugin.js#49
webpack/lib/Compiler.js#669
webpack/lib/Compiler.js#321
OKK,可以看到最后会调用 onCompiled

  1. 触发了 shouldEmit 钩子,如果返回false,就停止执行
  2. 调用了emitAssets函数,将 文件写入
  3. 触发 done钩子
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;
    }

    this.emitAssets(compilation, err => {
            if (err) return finalCallback(err);

            if (compilation.hooks.needAdditionalPass.call()) {
                    compilation.needAdditionalPass = true;
                    /** 当前文件的 Stats */
                    const stats = new Stats(compilation);
                    stats.startTime = startTime;
                    stats.endTime = Date.now();
                    this.hooks.done.callAsync(stats, err => {
                            if (err) return finalCallback(err);

                            this.hooks.additionalPass.callAsync(err => {
                                    if (err) return finalCallback(err);
                                    this.compile(onCompiled);
                            });
                    });
                    return;
            }

            this.emitRecords(err => {
                    if (err) return finalCallback(err);

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

2.8 加载依赖

webpack/lib/Compilation.js#1112 -> 里的 afterBuild\ 回过头看上文这么多的文件查找,这里还有一个 加载依赖的过程
如果当前模块存在 依赖时,进行加载,不执行 onCompiled processModuleDependencies中,就会获取 上文 SingleEntryPlugin提到的normalModuleFactory addModuleDependencies又会循环调用processModuleDependencies,就不再展开

const afterBuild = () => {
    if (recursive && addModuleResult.dependencies) {
    /** 加载依赖*/
    	this.processModuleDependencies(dependentModule, callback);
    } else {
        return callback();
    }
};

processModuleDependencies(module, callback) {
        const dependencies = new Map();

        const addDependency = dep => {
                const resourceIdent = dep.getResourceIdentifier();
                if (resourceIdent) {
                        /**  这里的 factory 来自于 SignalFactory */
                        const factory = this.dependencyFactories.get(dep.constructor);
                        if (factory === undefined) {
                                throw new Error(
                                        `No module factory available for dependency type: ${dep.constructor.name}`
                                );
                        }
                        let innerMap = dependencies.get(factory);
                        if (innerMap === undefined) {
                                dependencies.set(factory, (innerMap = new Map()));
                        }
                        let list = innerMap.get(resourceIdent);
                        if (list === undefined) innerMap.set(resourceIdent, (list = []));
                        list.push(dep);
                }
        };
        ...

        this.addModuleDependencies(
                module,
                sortedDependencies,
                this.bail,
                null,
                true,
                callback
        );
}

2.9 开始处理 chunk

webpack/lib/Compiler.js#669
又是和上文差不多,在 onCompiled 前的操作,也就是 make 钩子执行完成之后,的回调函数
调用了 compilation.seal 开始处理 chunk

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

        compilation.finish(err => {
                if (err) return callback(err);
                /** 开始处理 chunk */
                compilation.seal(err => {
                        if (err) return callback(err);

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

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

2.10 生成代码内容

webpack/lib/Compilation.js#1398 开始了生成代码内容 + 一堆的 缓存判断,内容比较多,但是可以注意有注释的部分

if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
        this.hooks.beforeChunkAssets.call();
        /** 生成代码内容*/
        this.createChunkAssets();
}
createChunkAssets() {
    const outputOptions = this.outputOptions;
    const cachedSourceMap = new Map();
    /** @type {Map<string, {hash: string, source: Source, chunk: Chunk}>} */
    const alreadyWrittenFiles = new Map();
    for (let i = 0; i < this.chunks.length; i++) {
            const chunk = this.chunks[i];
            chunk.files = [];
            let source;
            let file;
            let filenameTemplate;
            try {
                    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) {
                            const cacheName = fileManifest.identifier;
                            const usedHash = fileManifest.hash;
                            filenameTemplate = fileManifest.filenameTemplate;
                            const pathAndInfo = this.getPathWithInfo(
                                    filenameTemplate,
                                    fileManifest.pathOptions
                            );
                            file = pathAndInfo.path;
                            const assetInfo = pathAndInfo.info;

                            // check if the same filename was already written by another chunk
                            const alreadyWritten = alreadyWrittenFiles.get(file);
                            ...
                            if (
                                    this.cache &&
                                    this.cache[cacheName] &&
                                    this.cache[cacheName].hash === usedHash
                            ) {
                                    source = this.cache[cacheName].source;
                            } else {
                               /** 实际的文件内容 */
                            	source = fileManifest.render();
                                    // Ensure that source is a cached source to avoid additional cost because of repeated access
                                    if (!(source instanceof CachedSource)) {
                                            const cacheEntry = cachedSourceMap.get(source);
                                            if (cacheEntry) {
                                                    source = cacheEntry;
                                            } else {
                                                    const cachedSource = new CachedSource(source);
                                                    cachedSourceMap.set(source, cachedSource);
                                                    source = cachedSource;
                                            }
                                    }
                                    if (this.cache) {
                                            this.cache[cacheName] = {
                                                    hash: usedHash,
                                                    source
                                            };
                                    }
                            }
                            /** 缓存要输出的文件 */
                            this.emitAsset(file, source, assetInfo);
                            chunk.files.push(file);
                            this.hooks.chunkAsset.call(chunk, file);
                            alreadyWrittenFiles.set(file, {
                                    hash: usedHash,
                                    source,
                                    chunk
                            });
                    }
            }
            ...
    }
}

2.11 写入代码内容

webpack/lib/Compiler.js#353
看了前面那么多准备,终于可以写文件了

emitAssets(compilation, callback) {
    let outputPath;
    const emitFiles = err => {
            if (err) return callback(err);

            asyncLib.forEachLimit(
            compilation.getAssets(),
            15,
            ({ name: file, source }, callback) => {
                    let targetFile = file;
                    const queryStringIdx = targetFile.indexOf("?");
                    if (queryStringIdx >= 0) {
                            targetFile = targetFile.substr(0, queryStringIdx);
                    }

                    const writeOut = err => {
                        const targetPath = this.outputFileSystem.join(
                                outputPath,
                                targetFile
                        );
                        // TODO webpack 5 remove futureEmitAssets option and make it on by default
                    if (this.options.output.futureEmitAssets) {
                        ...
                        /** 写入文件 */
                        this.outputFileSystem.writeFile(targetPath, content, ...);
                    }
                        ...
                };

                if (targetFile.match(/\/|\\/)) {
                    const dir = path.dirname(targetFile);
                    this.outputFileSystem.mkdirp(
                            this.outputFileSystem.join(outputPath, dir),
                            writeOut
                    );
                } else {
                        writeOut();
                }
            },
            。。。
            );
    };
    /** 执行 文件写入,这里也是钩子中 最后的 获取文件内容 的机会 */
    this.hooks.emit.callAsync(compilation, err => {
            if (err) return callback(err);
            outputPath = compilation.getPath(this.outputPath);
            this.outputFileSystem.mkdirp(outputPath, emitFiles);
    });
}