webpack构建流程分析

前言

webpack是一个强大的打包工具,拥有灵活、丰富的插件机制,网上关于如何使用webpack及webpack原理分析的技术文档层出不穷。最近本身也在学习webpack的过程当中,记录并分享一下,但愿对你有点帮助。 本文主要探讨,webpack的一次构建流程中,主要干了哪些事儿。 (我们只研究研究构建的总体流程哈,细节不看🙈)javascript

已知,Webpack 源码是一个插件的架构,不少功能都是经过诸多的内置插件实现的。Webpack为此专门本身写一个插件系统,叫 Tapable 主要提供了注册和调用插件的功能。 一块儿研究以前,但愿你对 tapable 有所了解~java

调试

阅读源码最直接的方式是在 chrome 中经过断点在关键代码上进行调试,咱们能够用 node-inspector进行这次debugger。node

"scripts": {
    "build": "webpack --config webpack.prod.js",
    "debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress",
},
复制代码

执行npm run build && npm run debugwebpack

// 入口文件
import { helloWorld } from './helloworld.js';
document.write(helloWorld());

// helloworld.js
export function helloWorld() {
    return 'bts';
}

// webpack.prod.js
module.exports = {
    entry: {
        index: './src/index.js',
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name]_[chunkhash:8].js'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: 'babel-loader',
            },
        ]
    },
};

复制代码

基本架构

先经过一张大图总体梳理一下webpack的主体流程,再细节一点的稍后再介绍 web

流程图中展现了些核心任务点,简要说明下这些任务点作了事儿:

  • 经过 yargs 解析 configshell 中的配置项
  • webpack 初始化过程,首先会根据第一步的 options 生成 compiler 对象,而后初始化 webpack 的内置插件及 options 配置
  • run 表明编译的开始,会构建 compilation 对象,用于存储这一次编译过程的全部数据
  • make 执行真正的编译构建过程,从入口文件开始,构建模块,直到全部模块建立结束
  • seal 生成 chunks,对 chunks 进行一系列的优化操做,并生成要输出的代码
  • seal 结束后,Compilation 实例的全部工做到此也所有结束,意味着一次构建过程已经结束
  • emit 被触发以后,webpack 会遍历 compilation.assets, 生成全部文件,而后触发任务点 done,结束构建流程

构建流程

在学习其余技术博客时都有相似上面的主体流程的分析,道理都懂,但不打断点看的细节点,说服不了本身。如下是一些任务点的详细动做,建议有兴趣的小伙伴多打几个debuggerchrome

强烈建议在每一个重要钩子的回调函数中打debugger,否则可能跳着跳着就走远了shell

webpack准备阶段

webpack启动入口,webpack-cli/bin/cli.jsnpm

const webpack = require("webpack");
    // 使用yargs来解析命令行参数并合并配置文件中的参数(options),
    // 而后调用lib/webpack.js实例化compile 并返回
let compiler;
try {
	compiler = webpack(options);
} catch (err) {}
复制代码
// lib/webpack.js
const webpack = (options, callback) => {
    // 首先会检查配置参数是否合法
    
    // 建立Compiler
    let compiler;
    compiler = new Compiler(options.context);
    
    compiler.options = new WebpackOptionsApply().process(options, compiler);
    
    ...
    if (options.watch === true || ..) {
        ...
        return compiler.watch(watchOptions, callback);
    }
	compiler.run(callback);
}
复制代码

建立Compiler

建立了 compiler 对象,compiler 能够理解为 webpack 编译的调度中心,是一个编译器实例,在 compiler 对象记录了完整的 webpack 环境信息,在 webpack 的每一个进程中,compiler 只会生成一次。数组

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            beforeCompile: new AsyncSeriesHook(["params"]),
            compile: new SyncHook(["params"]),
            afterCompile: new AsyncSeriesHook(["compilation"]),
            make: new AsyncParallelHook(["compilation"]),
            entryOption: new SyncBailHook(["context", "entry"])
            // 定义了不少不一样类型的钩子
        };
        // ...
    }
}
复制代码

能够看到 Compiler 对象继承自 Tapable,初始化时定义了不少钩子。babel

初始化默认插件和Options配置

WebpackOptionsApply 类中会根据配置注册对应的插件,其中有个比较重要的插件

new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
复制代码

EntryOptionPlugin插件中订阅了compiler的entryOption钩子,并依赖SingleEntryPlugin插件

module.exports = class EntryOptionPlugin {
	apply(compiler) {
		compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
			return new SingleEntryPlugin(context, item, name);
		});
	}
};
复制代码

SingleEntryPlugin 插件中订阅了 compilermake 钩子,并在回调中等待执行 addEntry,但此时 make 钩子还并无被触发哦

apply(compiler) {
    compiler.plugin("compilation", (compilation, params) => {
        const normalModuleFactory = params.normalModuleFactory;
        // 这里记录了 SingleEntryDependency 对应的工厂对象是 NormalModuleFactory
        compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
    });
    compiler.hooks.make.tapAsync(
        "SingleEntryPlugin",
        (compilation, callback) => {
        	const { entry, name, context } = this;
        
        	// 建立单入口依赖 
        	const dep = SingleEntryPlugin.createDependency(entry, name);
        	// 正式进入构建阶段
        	compilation.addEntry(context, dep, name, callback);
        }
    );
}
复制代码

run

初始化 compiler 后,根据 optionswatch 判断是否启动了 watch,若是启动 watch 了就调用 compiler.watch 来监控构建文件,不然启动 compiler.run 来构建文件,compiler.run 就是咱们这次编译的入口方法,表明着要开始编译了。

构建编译阶段

调用 compiler.run 方法来启动构建

run(callback) {
    const onCompiled = (err, compilation) => {
    	this.hooks.done.callAsync(stats, err => {
    		return finalCallback(null, stats);
    	});
    };
    
    // 执行订阅了compiler.beforeRun钩子插件的回调
    this.hooks.beforeRun.callAsync(this, err => {
        // 执行订阅了compiler.run钩子插件的回调
    	this.hooks.run.callAsync(this, err => {
    		this.compile(onCompiled);
    	});
    });
}
复制代码

compiler.compile 开始真正执行咱们的构建流程,核心代码以下

compile(callback) {
    // 实例化核心工厂对象
    const params = this.newCompilationParams();
    // 执行订阅了compiler.beforeCompile钩子插件的回调
    this.hooks.beforeCompile.callAsync(params, err => {
        // 执行订阅了compiler.compile钩子插件的回调
        this.hooks.compile.call(params);
        // 建立这次编译的Compilation对象
        const compilation = this.newCompilation(params);
        
        // 执行订阅了compiler.make钩子插件的回调
        this.hooks.make.callAsync(compilation, err => {
            
            compilation.finish(err => {
                compilation.seal(err => {
                    this.hooks.afterCompile.callAsync(compilation, err => {
                		return callback(null, compilation);
                	});
                })
            })
        })
    })
}
复制代码

compile阶段,Compiler 对象会开始实例化两个核心的工厂对象,分别是 NormalModuleFactoryContextModuleFactory。工厂对象顾名思义就是用来建立实例的,它们后续用来建立 module 实例的,包括 NormalModule 以及 ContextModule 实例。

Compilation

建立这次编译的 Compilation 对象,核心代码以下:

newCompilation(params) {
    // 实例化Compilation对象
    const compilation = new Compilation(this);
    this.hooks.thisCompilation.call(compilation, params);
    // 调用this.hooks.compilation通知感兴趣的插件
    this.hooks.compilation.call(compilation, params);
    return compilation;
}
复制代码

Compilation 对象是后续构建流程中最核心最重要的对象,它包含了一次构建过程当中全部的数据。也就是说一次构建过程对应一个 Compilation 实例。在建立 Compilation 实例时会触发钩子 compilaiionthisCompilation

在Compilation对象中:

  • modules 记录了全部解析后的模块
  • chunks 记录了全部chunk
  • assets记录了全部要生成的文件

上面这三个属性已经包含了 Compilation 对象中大部分的信息,但目前也只是有个大体的概念,特别是 modules 中每一个模块实例究竟是什么东西,并不太清楚。先不纠结,毕竟此时 Compilation 对象刚刚生成。

make

Compilation 实例建立完成以后,webpack 的准备阶段已经完成,下一步将开始 modules 的生成阶段。

this.hooks.make.callAsync() 执行订阅了 make 钩子的插件的回调函数。回到上文,在初始化默认插件过程当中(WebpackOptionsApply类),SingleEntryPlugin 插件中订阅了 compilermake 钩子,并在回调中等待执行 compilation.addEntry 方法。

生成modules

compilation.addEntry 方法会触发第一批 module 的解析,即咱们在 entry 中配置的入口文件 index.js。在深刻 modules 的构建流程以前,咱们先对模块实例 module 的概念有个了解。

modules

一个依赖对象(Dependency)通过对应的工厂对象(Factory)建立以后,就可以生成对应的模块实例(Module)。

Dependency,能够理解为还未被解析成模块实例的依赖对象。好比配置中的入口模块,或者一个模块依赖的其余模块,都会先生成一个 Dependency 对象。每一个 Dependency 都会有对应的工厂对象,好比咱们此次debuger的代码,入口文件 index.js 首先生成 SingleEntryDependency, 对应的工厂对象是 NormalModuleFactory。(前文说到SingleEntryPlugin插件时有放代码,有疑惑的同窗能够往前翻翻看)

// 建立单入口依赖 
const dep = SingleEntryPlugin.createDependency(entry, name);
// 正式进入构建阶段
compilation.addEntry(context, dep, name, callback);
复制代码

SingleEntryPlugin插件订阅的make事件,将建立的单入口依赖传入compilation.addEntry方法,addEntry主要执行_addModuleChain()

_addModuleChain

_addModuleChain(context, dependency, onModule, callback) {
   ...
   
   // 根据依赖查找对应的工厂函数
   const Dep = /** @type {DepConstructor} */ (dependency.constructor);
   const moduleFactory = this.dependencyFactories.get(Dep);
   
   // 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
   moduleFactory.create({
       dependencies: [dependency]
       ...
   }, (err, module) => {
       ...
       const afterBuild = () => {
   	    this.processModuleDependencies(module, err => {
       		if (err) return callback(err);
       		callback(null, module);
           });
   	};
       
       this.buildModule(module, false, null, null, err => {
           ...
           afterBuild();
       })
   })
}
复制代码

_addModuleChain中接收参数dependency传入的入口依赖,使用对应的工厂函数NormalModuleFactory.create方法生成一个空的module对象,回调中会把此module存入compilation.modules对象和dependencies.module对象中,因为是入口文件,也会存入compilation.entries中。随后执行buildModule进入真正的构建module内容的过程。

buildModule

buildModule方法主要执行module.build(),对应的是NormalModule.build()

// NormalModule.js
build(options, compilation, resolver, fs, callback) {
    return this.doBuild(options, compilation, resolver, fs, err => {
        ...
        // 一下子讲
    }
}
复制代码

先来看看doBuild中作了什么

doBuild(options, compilation, resolver, fs, callback) {
    ...
    runLoaders(
    	{
            resource: this.resource, // /src/index.js
            loaders: this.loaders, // `babel-loader`
            context: loaderContext,
            readResource: fs.readFile.bind(fs)
    	},
    	(err, result) => {
    	    ...
    	    const source = result.result[0]; 
    	    
    	    this._source = this.createSource(
            	this.binary ? asBuffer(source) : asString(source),
            	resourceBuffer,
            	sourceMap
            );
    	}
    )
}
复制代码

一句话说,doBuild 调用了相应的 loaders ,把咱们的模块转成标准的JS模块。这里,使用babel-loader 来编译 index.jssource就是 babel-loader 编译后的代码。

// source
"debugger; import { helloWorld } from './helloworld.js';document.write(helloWorld());”
复制代码

同时,还会生成this._source对象,有namevalue两个字段,name就是咱们的文件路径,value就是编译后的JS代码。模块源码最终是保存在 _source 属性中,能够经过 _source.source() 来获得。回到刚刚的NormalModule中的build方法

build(options, compilation, resolver, fs, callback) {
    ...
    return this.doBuild(options, compilation, resolver, fs, err => {
        const result = this.parser.parse(
        	this._source.source(),
        	{
        		current: this,
        		module: this,
        		compilation: compilation,
        		options: options
        	},
        	(err, result) => {
        		
        	}
        );
    }
}
复制代码

通过 doBuild 以后,咱们的任何模块都被转成了标准的JS模块。接下来就是调用Parser.parse方法,将JS解析为AST。

// Parser.js
const acorn = require("acorn");
const acornParser = acorn.Parser;
static parse(code, options) {
    ...
    let ast = acornParser.parse(code, parserOptions);
    return ast;
}
复制代码

生成的AST结果以下:

解析成AST最大做用就是收集模块依赖关系,webpack会遍历AST对象,遇到不一样类型的节点执行对应的函数。好比调试代码中出现的 import { helloWorld } from './helloworld.js'const xxx = require('XXX')的模块引入语句,webpack会记录下这些依赖项,并记录在module.dependencies数组中。到这里,入口module的解析过程就完成了,解析后的module你们有兴趣能够打印出来看下,这里我只截图了module.dependencies数组。
每一个 module 解析完成以后,都会触发 Compilation例对象的succeedModule钩子,订阅这个钩子获取到刚解析完的 module 对象。 随后,webpack会遍历module.dependencies数组,递归解析它的依赖模块生成module,最终咱们会获得项目所依赖的全部 modules。遍历的逻辑在 afterBuild() -> processModuleDependencies() -> addModuleDependencies() -> factory.create()
make阶段到此结束,接下去会触发 compilation.seal方法,进入下一个阶段。

生成chunks

compilation.seal 方法主要生成chunks,对chunks进行一系列的优化操做,并生成要输出的代码。webpack 中的 chunk ,能够理解为配置在 entry 中的模块,或者是动态引入的模块。

chunk内部的主要属性是_modules,用来记录包含的全部模块对象。因此要生成一个chunk,就先要找到它包含的全部modules。下面简述一下chunk的生成过程:

  • 先把 entry 中对应的每一个 module 都生成一个新的 chunk
  • 遍历module.dependencies,将其依赖的模块也加入到上一步生成的chunk中
  • 若某个module是动态引入的,为其建立一个新的chunk,接着遍历依赖

下图是咱们这次demo生成的this.chunks,_modules中有两个模块,分别是入口index模块,与其依赖helloworld模块。

在生成chunk的过程当中与过程后,webpack会对chunk和module进行一系列的优化操做,优化操做大都是由不一样的插件去完成。可见 compilation.seal 方法中,有大量的钩子执行的代码。

this.hooks.optimizeModulesBasic.call(this.modules);
this.hooks.optimizeModules.call(this.modules);
this.hooks.optimizeModulesAdvanced.call(this.modules);

this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups);

...
复制代码

例如,插件SplitChunksPlugin订阅了compilation的optimizeChunksAdvanced钩子。至此,咱们的modules和chunks都生成了,该去生成文件了。

生成文件

首先须要生成最终的代码,主要在compilation.seal 中调用了 compilation.createChunkAssets方法。

for (let i = 0; i < this.chunks.length; i++) {
    const chunk = this.chunks[i];
    const template = chunk.hasRuntime()
        ? this.mainTemplate
        : this.chunkTemplate;
    const manifest = template.getRenderManifest({
        ...
    })
    ...
    for (const fileManifest of manifest) {
        source = fileManifest.render();
    }
    
    ...
    this.emitAsset(file, source, assetInfo);
    
}
复制代码

createChunkAssets方法会遍历chunks,来渲染每个chunk生成代码。其实,compilation对象在实例化时,同时还会实例化三个对象,分别是MainTemplate, ChunkTemplateModuleTemplate。这三个对象是用来渲染chunk,获得最终代码模板的。它们之间的不一样在于,MainTemplate用来渲染入口 chunk,ChunkTemplate用来渲染非入口 chunk,ModuleTemplate用来渲染 chunk 中的模块。

这里, MainTemplateChunkTemplaterender 方法是用来生成不一样的"包装代码"的,MainTemplate 对应的入口 chunk 须要带有 webpack 的启动代码,因此会有一些函数的声明和启动。而包装代码中,每一个模块的代码是经过 ModuleTemplate 来渲染的,不过一样只是生成”包装代码”来封装真正的模块代码,而真正的模块代码,是经过模块实例的 source 方法来提供。这么说可能不是很好理解,直接看看最终生成文件中的代码,以下:

每一个chunk的源码生成以后,会调用 emitAsset 将其存在 compilation.assets 中。当全部的 chunk 都渲染完成以后,assets 就是最终更要生成的文件列表。至此, compilationseal 方法结束,也表明着 compilation 实例的全部工做到此也所有结束,意味着一次构建过程已经结束,接下来只有文件生成的步骤了。

emit

Compiler 开始生成文件前,钩子 emit 会被执行,这是咱们修改最终文件的最后一个机会,生成的在此以后,咱们的文件就不能改动了。

this.hooks.emit.callAsync(compilation, err => {
    if (err) return callback(err);
    outputPath = compilation.getPath(this.outputPath);
    this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
复制代码

webpack 会直接遍历 compilation.assets 生成全部文件,而后触发钩子done,结束构建流程。

总结

咱们将webpack核心的构建流程都过了一遍,但愿在阅读彻底文以后,对你们了解 webpack原理有所帮助~

本片文章代码都是通过删减更改处理的,都是为了能更好的理解。能力有限,若是有不正确的地方欢迎你们指正,一块儿交流学习。

相关文章
相关标签/搜索