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 处理流程分析

相关文章
相关标签/搜索