-
Notifications
You must be signed in to change notification settings - Fork 14
Description
前言:此文经过研究《深入浅出Webpack》和 目前webpack源码 来编写
webpack里的几个基本概念:
- Entry:执行构建的入口,可抽象成输入,并且可以有多个entry。
- Module:在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始,递归找出所有依赖的模块。
- Loader:模块加载器,用于对模块的原内容按照需求进行加载转换。
- Plugin:插件,在Webpack构建流程中的特定时,广播对应的事件,此时插件可以监听这些事件的发生,在特定的时机做对应的事情。
- Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
- Bundle:打包后产出的一个/多个文件,常见配以[chunk-id] + Hash命名。
整个构建流程大致分为三个部分:
- 初始化阶段
- 编译阶段
- 输出阶段
接下来将从这三个阶段细分,讲讲webpack做了哪些工作:
初始化阶段
-
初始化参数
从配置文件和Shell语句中读取参数并合并,得出最终的配置。
Shell语句的处理一般由
webpack-cli命令行库工具执行,包括--config读取的配置文件,最后才将参数option传递给webpack。这就是为什么使用Webpack时需要安装这两个lib。期间如果配置文件(如:
webpack.config.js)中Plugins使用了new plugin()之类的语句,则会一并调用,实例化插件对象。 -
实例化Compiler
用得到的参数
option初始化Compiler实例,实例中包含了完整的Webpack默认配置。简化代码如下:const webpack = (options, callback) => { let compiler if (Array.isArray(options)) { // ... compiler = createMultiCompiler(options); } else { compiler = createCompiler(options); } // ... return compiler; }
一般全局只有一个compiler(多份配置
option则有多个compiler),并向外暴露run方法进行启动编译。Compiler是负责管理webpack整个打包流程的“ 主人公 ”。Compiler主要负责进行:文件监听与编译,初始化编译过程中的事件Hook,到了v4末版本的时候,Hook已多达二十多个,具体点击可查看
Compiler类中还声明了用于创建子编译对象
childCompiler的方法/** * @param {Compilation} compilation the compilation * @param {string} compilerName the compiler's name * @param {number} compilerIndex the compiler's index * @param {OutputOptions} outputOptions the output options * @param {WebpackPluginInstance[]} plugins the plugins to apply * @returns {Compiler} a child compiler */ createChildCompiler( compilation, compilerName, compilerIndex, outputOptions, plugins ) {}
用于Loader/插件有需要时创建,执行模块的分开编译。
-
Environment
应用Node的文件系统到compiler对象,方便后续的文件查找和读取
new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging }).apply(compiler);
-
加载插件
依次调用插件的
apply方法(默认每个插件对象实例都需要提供一个apply)若为函数则直接调用,将compiler实例作为参数传入,方便插件调用此次构建提供的Webpack API并监听后续的所有事件Hook。if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } }
-
应用默认的Webpack配置
applyWebpackOptionsDefaults(options); // 随即之后,触发一些Hook compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); new WebpackOptionsApply().process(options, compiler); compiler.hooks.initialize.call();
除了一些默认的文件系统上下文
context、resolver,以及处理的文件输出方式,这里的要应用的默认配置在v4包含新的performance性能优化、Optimization打包优化。
至此,完成了整个第一阶段的初始化。
编译阶段
-
启动编译
这里有个小逻辑区分是否是
watch,如果是非watch,则会正常执行一次compiler.run()。如果是监听文件(如:
--watch)的模式,则会传递监听的watchOptions,生成Watching实例,每次变化都重新触发回调。function watch(watchOptions, handler) { if (this.running) { return handler(new ConcurrentCompilationError()); } this.running = true; this.watchMode = true; return new Watching(this, watchOptions, handler); }
-
触发compile事件
该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。
-
Compilation
这是整个webpack构建打包的关键。每一次的编译(包括
watch检测到文件变化时),compiler都会创建一个Compilation对象,标识当前的模块资源、编译生成资源、变化的文件等。同时也提供很多事件回调给插件进行拓展。Compilation的生成,是在compiler执行compile方法时构造的,主要流程大概是:触发compile事件后,执行this.newCompilation获取新一轮的compilation,并作为参数触发make事件。然后异步执行此次compile (callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { // ... this.hooks.compile.call(params); const compilation = this.newCompilation(params); // ... this.hooks.make.callAsync(compilation, err => { //... process.nextTick(() => { compilation.finish(err => { // ...完成 this.hooks.afterCompile() } } } }
当中还设计到两个主要的钩子:
-
complication:这其实是一个同名的hook,是在上述代码
this.newComplication()中调用的,当其调用时已完成complication的实例化。 -
make:表示一个新的
Complication创建完毕。 -
after-compile:表示一次Compilation执行完成
在complication实例化的阶段,调用了Loader转换模块,并将原有的内容结合输出对应的抽象语法树(AST),并递归的分析其导入语句(如
import等),最终梳理所有模块的依赖关系形成依赖图谱。当所有模块都经过Loader转换完成,此时触发complication的
seal事件,根据依赖关系和配置开始着手生成chunk。 -
输出阶段
-
should-emit事件
所有需要输出的文件已经生成,询问插件有哪些文件需要输出,哪些不需要输出。
-
emit事件
确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出的内容。
-
after-emit事件
文件输出完毕
-
done事件
成功完成一次完整的编译和输出流程