webpack运行流程、源码解析,Tabable原理

4,867 阅读8分钟

前言

在了解原理之前,我们需要手动配置并运行过简单的webpack,如:

重要的几个点:

  • entry: 入口文件
  • module: 模块,一个文件是一个模块
  • loader:文件转换器,能让你在js中引入各种其他文件如 .txt .css
  • plugin:插件,最重要,实现各种功能
  • chunk:代码块,按需加载中的分块文件

基于node环境,js代码有了操作计算机文件的权限,因此,webpack就是一堆js代码,然后去折腾一堆文件。

工厂流水线

webpck可以看成是一个工厂的流水线。

一个饮料流水线的例子

从图中可以看到,整个过程是从左至右进行的,紫色的字所代表的,就是一个大的操作集, 操作集中可以包含多个子操作,如 发酵 操作集中,有3个子操作,也就是说只有完成这3个子操作,才能结束发酵这个操作集,才能进入到下一个操作集:调配

webpack 流程

现在,我们把webpack带入到流程图中,蓝色大字。

解析1: hook(钩子)

图中的每一个紫色操作集,在webpack中,都是一个hook的实例,hook是一个,定义了一些方法如,添加plugin,运行所有plugin等等。hook有多种,同步or异步执行plugin,是否带返回值等....

例如

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");
 
 var hook = new SyncHook()  // 实例一个同步的hook
 hook.addPlugin(plugin1)
 hook.addPlugin(plugin2)
 ...
 
 Complier.emit = function() {
    hook.runAllPlugins()
 }
 
 Complier.emit() // 运行

解析2:Complier,Compliation

图中可以发现,加粗的蓝色为Compliation。webpack中Complier负责整个的构建流程(准备、编译、输出等),而Compliation只负责其中的 编译 过程。还有,Compliation只代表一次编译,也就是说,每当文件有变动,就重新生成一个Compliation实例,即一个文件结构,对应一个Compliation。

一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。

解析3:运行流程

对于紫色操作集,webpack中是定义 的,所以按顺序执行代码就行

Complier.beforeRun() // 执行对应的hook中的一堆plugin
Complier.run() // 执行对应的hook中的一堆plugin
Complier.make() // 执行对应的hook中的一堆plugin执行一堆plugin
Compliation.buildModule() // 执行对应的hook中的一堆plugin执行一堆plugin
...

而每个里面的plugin是活的,且允许用户自定义添加plugin,之前说了是hook类提供的plugin相关的方法,接下来讲一下hook是怎么玩的 ==> Tabable

Tabable

tapable库暴露了很多Hook(钩子)类,为插件提供挂载的钩子。

我们主要看这hook类,其他的Hook可以后自行研究

exports.SyncHook = require("./SyncHook");

SyncHook.js

继承自Hook.js,这里我就都写一起了,挑干的

class SyncHook {
    constructor() {
        // 存放plugins
        this.taps = []  
    }
    // 添加plugin
    tap(name, fn) {
        item = Object.assign({}, {type: 'sync', name, fn})
        // 添加plugin
        this.taps.push(item)
        // 实际上,在push之前,还有会注册拦截器、对taps中的plugins进行排序等,这里只简单模拟下
    }
    // 执行plugins
    // 这个call,走的是SyncHook.js中的compile()
    call() {
        // factory.setup(this, options);
	// return factory.create(options);
	// 实际上后面走的callTapsSeries、callTap
	
    	// 模拟
        this.taps.forEach(tap => {  // 逐个plugin执行
            tap.fn()
        })
	
    }
}

所以,hook就是订阅发布设计模式,将plugin的回调函数存到数组中,再集中执行。

Compiler.js

class Compiler {
    constructor() {
        super();
        this.hooks = {  // 一大堆hooks
            shouldEmit: new SyncBailHook(["compilation"]),
            done: new AsyncSeriesHook(["stats"]),
            additionalPass: new AsyncSeriesHook([]),
            beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),
            emit: new AsyncSeriesHook(["compilation"]),
            afterEmit: new AsyncSeriesHook(["compilation"]),
	    compilation: new SyncHook(["compilation", "params"]),
	    beforeCompile: new AsyncSeriesHook(["params"]),
	    compile: new SyncHook(["params"]),
    	    make: new AsyncParallelHook(["compilation"]),
	    afterCompile: new AsyncSeriesHook(["compilation"]),
	    watchRun: new AsyncSeriesHook(["compiler"]),
	    environment: new SyncHook([]),
	    afterEnvironment: new SyncHook([]),
	    afterPlugins: new SyncHook(["compiler"]),
	    entryOption: new SyncBailHook(["context", "entry"])
        }
    }
    
    run() {
        // 这个callAsync就是上面hook类中的call,只不过是异步的
        this.hooks.beforeRun.callAsync(this, err => {
            this.hooks.run.callAsync(this, err => {
                this.readRecords(err => {
		    this.compile(); //
		});
	    });
        });
    }
    
    compile() {
        this.hooks.beforeCompile.callAsync(err => {
            this.hooks.compile.call(params);
            const compilation = this.newCompilation(params);
            this.hooks.make.callAsync()
            ...
        })
    }
}

从上面我们可以看到了,this.hooks里定义了全部的 操作子集 , 而run、compile、等都是操作集,每个操作集里面会运行多个子集,这就和之前的流程图对应上了。

不过,上面都是执行hook的call方法,那么,hook里面的plugins是什么时候tap加进去的呢?接着看

所以

plugin

class MyPlugin {
    apply(compiler) {   // 接收传过来的compiler实例
        compiler.hooks.beforeRun.tap('MyPlugin', function() {
            // 回调函数
            console.log(3333)
            file等操作...
        })
        // 可以注册多个不同的hook
        compiler.hooks.afterCompile.tap('MyPlugin', function() {
            // 回调函数
            console.log(444)
            file等操作...
        })
    }
}

class YouPlugin {
    apply(compiler) {   // 接收传过来的compiler实例
        compiler.hooks.compile.tap('YouPlugin', function() {
            // 回调函数
            console.log(444)
            file等操作...
        })
        
    }
}

webpack.js

const webpack = (options) => {
    let compiler = new Compiler()
    // 循环plugins,把每个子plugin添加到对应的compiler的操作集上
    for (const plugin of options.plugins) {				
        plugin.apply(compiler); // 将plugin注册添加到compiler对应中
    }
    // 注册plugin结束
   
    // 开始运行。。。
    compiler.run()
    
}

执行!

const options = {
    plugins: [
        new MyPlugin(),
        new YouPlugin()
    ]
}

webpack(options)

!!!想查看一些插件是什么时候加到hook中的,打开WebpackOptionsApply.js文件

到这里,webpack的整体运行流程就差不多了,接下来,我们逐个研究流程中的各个操作集细节~

源码部分

注意:按文件搜索目标文件,比如想查看hooks.beforeRun的插件是什么时候注册的,搜索lib文件夹hooks.beforeRun.tap,若按照代码去找,需要花很长时间。。。

主要研究以下这些

  • Compiler.run()
  • Compiler.compile() 开始编译
  • Compiler.make() 从入口分析依赖以及间接依赖模块,创建模块对象
  • Compilation.buildModule() 模块构建
  • Compiler.normalModuleFactory() 构建
  • Compilation.seal() 构建结果封装, 不可再更改
  • Compiler.afterCompile() 完成构建,缓存数据
  • Compiler.emit() 输出到dist目录

Compiler.run()

// Compiler.js

run() {
    // NodeEnvironmentPlugin.js 中 beforeRun添加plugins
    this.hooks.beforeRun.callAsync(this, err => {
            // CachePlugin.js 中 beforeRun添加plugins 
            this.hooks.run.callAsync(this, err => {
                this.readRecords(err => {
                // 开始编译,重要
                    this.compile(onCompiled);
                });
            });
    });
    
    // 按文件搜索this.hooks.beforeRun.tap  就能找到在哪个js中添加的,其他插件同样这样搜
}

Compiler.compile() 重要

// Compiler.js
compile() {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
	this.hooks.compile.call(params);
	// 新建一个 compilation
	// compilation 里面也定义了 this.hooks , 原理和 Compiler 一样
	const compilation = this.newCompilation(params);
	// 执行make函数,重要的
	this.hooks.make.callAsync(compilation, err => {
	compilation.finish(err => {
	    compilation.seal(err => {
	        this.hooks.afterCompile.callAsync(compilation, err => {
	            return callback(null, compilation);
	        });
	    });
        });
    });
}

this.hooks.make 方法

一个新的compilation对象创建完毕, 即将从entry开始读取文件,根据文件类型和编译的loader对文件进行==编译==,编译完后再找出该文件依赖的文件,递归地编译和解析

// 搜索lib目录,compiler.hooks.make.tap
// 主要定位到文件SingleEntryPlugin.js,DllEntryPlugin.js,
// SingleEntryPlugin.js
apply(compiler) {
compiler.hooks.make.tapAsync(
	"SingleEntryPlugin",
        (compilation, callback) => {
            const { entry, name, context } = this;
            const dep = SingleEntryPlugin.createDependency(entry, name);
            // 进入compilation.addEntry方法
            compilation.addEntry(context, dep, name, callback);
}
);
    
}

Compilation.addEntry

// 主要执行的_addModuleChain方法

_addModuleChain方法

// 主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。

通过 *ModuleFactory.create方法创建模块,(有NormalModule , MultiModule , ContextModule , DelegatedModule 等)
对模块使用的loader进行加载。
调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST。遍历 AST,构建该模块所依赖的模块

_addModuleChain() {
    moduleFactory.create(rsu => { // 打开 NormalModuleFactory.js 查看 create 方法
        // 收集一系列信息然后创建一个module传入回调
        // 执行this.buildModule方法方法,重要
        this.buildModule() // 见下方
    })
}


buildModule

buildModule(module, optional, origin, dependencies, thisCallback) {
    // 触发buildModule事件点
    this.hooks.buildModule.call(module);
    //!!! 开始build,主要执行的是NormalModuleFactory生成的NormalModule中的build方法,中的build方法,打开NormalModule
    module.build(   // doBuild
        this.options,
        this,
        this.resolverFactory.get("normal", module.resolveOptions),
        this.inputFileSystem,
        error => {
            ......
        }
    );

NormalModule.js

build() {
    // 先看执行的doBuild
    return this.doBuild(options, compilation, resolver, fs, err => {
        // 调用parse方法,创建依赖Dependency并放入依赖数组
        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);
    }
    })
}
doBuild(options, compilation, resolver, fs, callback) {
    runLoaders(rsu => { // 获取loader相关的信息并转换成webpack需要的js文件,原理见下方链接
        callback()// 回build中执行ast(抽象语法树,编译过程中常见结构,vue、babel原理都有)
    })
}

runLoaders:segmentfault.com/a/119000001…

对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 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里面, 如下:

总结一下:

module 是 webpack 构建的核心实体,也是所有 module 的 父类,它有几种不同子类:NormalModule , MultiModule ,
ContextModule , DelegatedModule 等,一个依赖对象(Dependency,还未被解析成模块实例的依赖对象。
比如我们运行 webpack 时传入的入口模块,或者一个模块依赖的其他模块,都会先生成一个 Dependency 对象。)
经过对应的工厂对象(Factory)创建之后,就能够生成对应的模块实例(Module)。

Compilation.seal

buildModule后,就到了Seal封装构建结果这一步骤

seal(callback) {
    // 触发事件点seal
    this.hooks.seal.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.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

然后,对生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。

template.getRenderMainfest.render()

通过模板(MainTemplate、ChunkTemplate)把chunk生产 _webpack_requie() 的格式。

Compiler.done

最后一步,webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。

本文链接

webpack很复杂,其中好多个点都需要深入剖析,我们需要定期的回看源码,每一次都会有更深的理解。我借鉴了很多大佬的文章思路,理解尚浅,在通往大佬的过程中,我会经常回来完善~~~