浅析webpack源码——webpack的流程

猪小花1号2018-09-13 11:26

作者:牟金涛


webpack可以说是目前最流行的前端脚手架,至少在Parceljs这类脚手架工具成熟之前,webpack依旧是前端架构首选,虽然其存在配置庞大复杂等问题,但其层次不穷功能强大甚至奇葩的loader与plugin满足了各种打包需求。 不过webpack不仅仅是配置复杂,实际上写一个webpack的插件也远比gulp复杂,相比gulp webpack的打包流程要复杂很多,因为一些项目需要,我简单的研究了一下webpack的源码,在此记录分享一下。

重要的前言

个人觉webpack源码中与流程相关最重要的几个部分其实包括了complier,compilation与plugin,前两者控制着流程后者则控制流程中具体操作,实际上webpack中的plugin并不单纯的是指我们配置的plugin,其内置的plugin更多,控制这入口文件的读取,parse过程中require的处理,模板路径处理等等......。可以先了解Tapable,个人理解Tapable就是一个事件对象,存储各种事件队列回调函数(这些回调就是plugin apply注册的函数)tapable可以通过applyPlugins,applyPluginsAsync来同步或者异步的回调这些函数。

流程相关的主要文件

  • /lib/Compilation.js
  • /lib/Compiler.js
  • /lib/webpack.js
  • /lib/WebpackOptionApply.js

webpack的初始运行

我们一般调用webpack是通过命令行读取config文件来实现的,但是实际上我们也可以手动在js文件中起用webpack

const webpack = require('webpack');
const config = require('./webpack.config.js');
const path = require('path');
const fs = require('fs');


const compiler = webpack(config);
// compiler.watch({}, err => {
compiler.run(err => {
    if (err) {
        console.error(err)
        throw err;
    }
});

如果没有回调函数webpack只会生成comiler而不会运行compiler。而compiler是webpack中最重要的对象,它是整个webpack的核心,compiler控制着整个webpack的流程,从options处理一直到所有的资源处理完毕emit。 具体的事件请阅读webpack文档中的 comiler event hook 具体的实现在/lib/webpack.js

    let compiler;
    if(Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if(typeof options === "object") {
        new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler();
        compiler.context = options.context;
        compiler.options = options;
        new NodeEnvironmentPlugin().apply(compiler);
        if(options.plugins && Array.isArray(options.plugins)) {
            compiler.apply.apply(compiler, options.plugins);
        }
        compiler.applyPlugins("environment");
        compiler.applyPlugins("after-environment");
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if(callback) {
        if(typeof callback !== "function") throw new Error("Invalid argument: callback");
        if(options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
            const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : (options.watchOptions || {});
            return compiler.watch(watchOptions, callback);
        }
        compiler.run(callback);
    }
    return compiler;

这一段中涉及了webpack中很重要的两个流程步骤,一个是compiler的建立,另一个则是传入参数。

    compiler.options = new WebpackOptionsApply().process(options, compiler);

WebpackOptionsApply是处理参数的对象定义在/lib/WebpackOptionsApply是处理参数的对象定义在.js中,为什么说其重要,因为这个过程中的webpack并不是单纯的处理用户shell传参与config.js的传参,更多的其在处理过程中会apply大部分webpack需要的plugin,不说了直接上部分源码吧,有兴趣的可以打开webpack源码看一下

compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);

compiler.apply(
    new CompatibilityPlugin(),
    new HarmonyModulesPlugin(options.module),
    new AMDPlugin(options.module, options.amd || {}),
    new CommonJsPlugin(options.module),
    new LoaderPlugin(),
    new NodeStuffPlugin(options.node),
    new RequireJsStuffPlugin(),
    new APIPlugin(),
    new ConstPlugin(),
    new UseStrictPlugin(),
    new RequireIncludePlugin(),
    new RequireEnsurePlugin(),
    new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles),
    new ImportPlugin(options.module),
    new SystemPlugin(options.module)
);

这里只是部分compiler添加plugin的内容,实际上WebpackOptionsApply还包括了其他的Plugin插入,包括了用户的plugin在此不再罗列。

compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);

其中最重要的是这一段插入的plugin,这是webpack的options处理plugin,其还有很重要的功能插件SingleEntryPlugin。在EntryOptionPlugin中会为每一个chunk入口增加一个SingleEntryPlugin用于调用compilation的添加入口的函数

compiler的职责

之前有说过每一个webpack运行会创建一个compiler对象,compiler实际上一个Tapable的子类,它贯穿了整个webpack打包,控制着webpack的编译开始与资源生产结束 lib/compiler.js


class Compiler extends Tapable {
    run(){
        //启动打包
    },
    compile() {
        //开始编译
    }
    ......
}

其中比较重要的是compile函数的调用,其实例化了一个Compilation对象。

compile(callback) {
    const params = this.newCompilationParams();
    this.applyPluginsAsync("before-compile", params, err => {
        if(err) return callback(err);

        this.applyPlugins("compile", params);
        //这里newCompilation实例化了一个Compilation,并抛出了Compilation事件
        const compilation = this.newCompilation(params);

        this.applyPluginsParallel("make", compilation, err => {
            if(err) return callback(err);

            compilation.finish();

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

                this.applyPluginsAsync("after-compile", compilation, err => {
                    if(err) return callback(err);

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

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.applyPlugins("this-compilation", compilation, params);
    this.applyPlugins("compilation", compilation, params);
    return compilation;
}

Compilation创建是一个很重要的时间节点,大部分与模块处理相关的逻辑都会在这个时候添加到Compilation对象中,很多plugin会这样写


        compiler.plugin('this-compilation', (compilation, params) => {
            compilation.plugin('additional-assets', (callback) => {

            });
            compilation.plugin('optimize-chunk-assets', (chunks, callback) => {

            });
        });

Compilation

Compilation事件 Compilation也是一个Tapable的子类,这应该是webpack中最核心的一个对象,它控制着webpack处理依赖生成模块,承载了生成的模块与静态资源列表。 在创建 module 之前,Compiler 会触发 make,并调用 Compilation.addEntry 方法(SingleEntryPlugin调用),通过 options 对象的 entry 字段找到我们的入口js文件。之后,在 addEntry 中调用私有方法 _addModuleChain ,这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。

addEntry(context, entry, name, callback) {
        const slot = {
            name: name,
            module: null
        };
        this.preparedChunks.push(slot);
        this._addModuleChain(context, entry, (module) => {

            entry.module = module;
            this.entries.push(module);
            module.issuer = null;

        }, (err, module) => {
            if(err) {
                return callback(err);
            }

            if(module) {
                slot.module = module;
            } else {
                const idx = this.preparedChunks.indexOf(slot);
                this.preparedChunks.splice(idx, 1);
            }
            return callback(null, module);
        });
    }
_addModuleChain(context, dependency, onModule, callback) {
        const start = this.profile && Date.now();

        const moduleFactory = this.dependencyFactories.get(dependency.constructor);
        ......
        this.semaphore.acquire(() => {
            moduleFactory.create({
                contextInfo: {
                    issuer: "",
                    compiler: this.compiler.name
                },
                context: context,
                dependencies: [dependency]
            }, (err, module) => {
                .......
                const result = this.addModule(module);
                if(!result) {
                    module = this.getModule(module);

                    onModule(module);

                    if(this.profile) {
                        const afterBuilding = Date.now();
                        module.profile.building = afterBuilding - afterFactory;
                    }

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

                    if(this.profile) {
                        const afterBuilding = Date.now();
                        module.profile.building = afterBuilding - afterFactory;
                    }

                    moduleReady.call(this);
                });
                function moduleReady() {
                    this.semaphore.release();
                    this.processModuleDependencies(module, err => {
                        if(err) {
                            return callback(err);
                        }

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

在这里做了两件事

  • 创建模块
  • 构建模块

创建模块根据模块的内容调用模块工程创建出该模块moduleFactory.create()。然后进行第二步buildModule,buildModule实际上就是调用了module的build,那这个build做了什么?

  • 引用模块的原始文件内容
  • 将原始文件内容压到module中loaders列表(这个应该是Factory生成模块对象的时候加上的)中通过loaders处理原始文件
  • 用 acorn 将处理过后的模块内容进行parse生成ast。

遍历ast如果ast中有新的模块引入怎么办,上面moduleReady回调中调用processModuleDependencies开始递归处理依赖的 module,重复之前的操作。更多内容可以查阅webpack中NormalModule.js这个文件,其中可以着重关注与build与doBuild两个函数的内容。这个是所有 module 的 父类,它有几种不同子类:NormalModule , MultiModule , ContextModule等。

而当所有的模块编译处理完毕之后,运行重新交回给compiler,compiler会调用Compilation中的seal方法

seal(callback) {
    const self = this;
    self.applyPlugins0("seal");
    self.nextFreeModuleIndex = 0;
    self.nextFreeModuleIndex2 = 0;
    self.preparedChunks.forEach(preparedChunk => {
        const module = preparedChunk.module;
        const chunk = self.addChunk(preparedChunk.name, module);
        const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);
        entrypoint.unshiftChunk(chunk);

        chunk.addModule(module);
        module.addChunk(chunk);
        chunk.entryModule = module;
        self.assignIndex(module);
        self.assignDepth(module);
        self.processDependenciesBlockForChunk(module, chunk);
    });
    self.sortModules(self.modules);
    self.applyPlugins0("optimize");

    while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) ||
        self.applyPluginsBailResult1("optimize-modules", self.modules) ||
        self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)) { /* empty */ }
    self.applyPlugins1("after-optimize-modules", self.modules);
    .....
}

seal阶段会抛出各种事件(可能是抛出最多的一个阶段了),这一阶段主要的功能其实是plugin做的,compilation主要负责流程的控制,而plugin会在流程中对传入的模块做一个优化处理,比如生成额外的资源,比如在生成内容前面加一段标记的js诸如此类的功能,起到整理chunk与module的作用,生成编译后的源码,合并,拆分,生成 hash 。我们自己自定义的plugin很多会在这一阶段处理代码,对plugin开发来说这是一个很关键的节点。

最后

终于该处理完的module和chunk的代码都已经优化完了,该干啥?生成assets,这个过程也是在seal的过程中。

seal(callback) {
    ......
    self.applyPlugins0("before-module-assets");
    self.createModuleAssets();
    if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) {
        self.applyPlugins0("before-chunk-assets");
        self.createChunkAssets();
    }
    .....
}

compilation.createChunkAssets是生产模块静态资源的方法,实际上就是延着chunks树调用module与chunk对应的template方法生成最终的静态js代码(Template 中的 render 方法)。 生成完毕之后运行会重新交还给complier,complier做的事就简单了,触发emmit将assets对象生成真正的文件,当然也可能是webpack-dev-server中的虚拟文件,反正到此的话整个打包就真正的结束。

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

    if(this.applyPluginsBailResult("should-emit", compilation) === false) {
        const stats = new Stats(compilation);
        stats.startTime = startTime;
        stats.endTime = Date.now();
        this.applyPlugins("done", stats);
        return callback(null, stats);
    }

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

        if(compilation.applyPluginsBailResult("need-additional-pass")) {
            compilation.needAdditionalPass = true;

            const stats = new Stats(compilation);
            stats.startTime = startTime;
            stats.endTime = Date.now();
            this.applyPlugins("done", stats);

            this.applyPluginsAsync("additional-pass", err => {
                if(err) return callback(err);
                this.compile(onCompiled);
            });
            return;
        }

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

            const stats = new Stats(compilation);
            stats.startTime = startTime;
            stats.endTime = Date.now();
            this.applyPlugins("done", stats);
            return callback(null, stats);
        });
    });
};

complier的打包具体可以看onCompiled回调函数与emitAssets实际资源生产函数。

总结

来个总结吧,webpack本身最主要的核心是Compilation与Module两个,Compilation完成编译过程中的入口添加,依赖处理,打包chunk的静态资源等,Module则是完成具体单个module的处理。主要思想就是tapable的流程控制。

参考:细说 webpack 之流程篇 webpack API


本文来自网易实践者社区,经作者牟金涛授权发布