Compiler源码精简版代码解析
源码地址(948行):https://github.com/webpack/webpack/blob/master/lib/Compiler.js
const { SyncHook, SyncBailHook, AsyncSeriesHook } = require("tapable"); class Compiler { constructor() { // 1. 定义生命周期钩子 this.hooks = Object.freeze({ // ...只列举几个常用的常见钩子,更多hook就不列举了,有兴趣看源码 done: new AsyncSeriesHook(["stats"]),//一次编译完成后执行,回调参数:stats beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]),//在编译器开始读取记录前执行 emit: new AsyncSeriesHook(["compilation"]),//在生成文件到output目录之前执行,回调参数: compilation afterEmit: new AsyncSeriesHook(["compilation"]),//在生成文件到output目录之后执行 compilation: new SyncHook(["compilation", "params"]),//在一次compilation创建后执行插件 beforeCompile: new AsyncSeriesHook(["params"]), compile: new SyncHook(["params"]),//在一个新的compilation创建之前执行 make:new AsyncParallelHook(["compilation"]),//完成一次编译之前执行 afterCompile: new AsyncSeriesHook(["compilation"]), watchRun: new AsyncSeriesHook(["compiler"]), failed: new SyncHook(["error"]), watchClose: new SyncHook([]), afterPlugins: new SyncHook(["compiler"]), entryOption: new SyncBailHook(["context", "entry"]) }); // ...省略代码 } newCompilation() { // 创建Compilation对象回调compilation相关钩子 const compilation = new Compilation(this); //...一系列操作 this.hooks.compilation.call(compilation, params); //compilation对象创建完成 return compilation } watch() { //如果运行在watch模式则执行watch方法,否则执行run方法 if (this.running) { return handler(new ConcurrentCompilationError()); } this.running = true; this.watchMode = true; return new Watching(this, watchOptions, handler); } run(callback) { if (this.running) { return callback(new ConcurrentCompilationError()); } this.running = true; process.nextTick(() => { this.emitAssets(compilation, err => { if (err) { // 在编译和输出的流程中遇到异常时,会触发 failed 事件 this.hooks.failed.call(err) }; if (compilation.hooks.needAdditionalPass.call()) { // ... // done:完成编译 this.hooks.done.callAsync(stats, err => { // 创建compilation对象之前 this.compile(onCompiled); }); } this.emitRecords(err => { this.hooks.done.callAsync(stats, err => { }); }); }); }); this.hooks.beforeRun.callAsync(this, err => { this.hooks.run.callAsync(this, err => { this.readRecords(err => { this.compile(onCompiled); }); }); }); } compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { this.hooks.compile.call(params); const compilation = this.newCompilation(params); //触发make事件并调用addEntry,找到入口js,进行下一步 this.hooks.make.callAsync(compilation, err => { process.nextTick(() => { compilation.finish(err => { // 封装构建结果(seal),逐次对每个module和chunk进行整理,每个chunk对应一个入口文件 compilation.seal(err => { this.hooks.afterCompile.callAsync(compilation, err => { // 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程, // 不然运行流程将会一直卡在这不往下执行 return callback(null, compilation); }); }); }); }); }); }); } emitAssets(compilation, callback) { const emitFiles = (err) => { //...省略一系列代码 // afterEmit:文件已经写入磁盘完成 this.hooks.afterEmit.callAsync(compilation, err => { if (err) return callback(err); return callback(); }); } // emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(这是最后一次修改最终文件的机会) this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath, {}); mkdirp(this.outputFileSystem, outputPath, emitFiles); }); } // ...省略代码 }
apply
方法中插入钩子的一般形式如下:
// compiler提供了compiler.hooks,可以根据这些不同的时刻去让插件做不同的事情。 compiler.hooks.阶段.tap函数('插件名称', (阶段回调参数) => { }); compiler.run(callback)
理解Compilation
Compilation
对象代表了一次资源版本构建。当运行 webpack
开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation
,从而生成一组新的编译资源。一个 Compilation
对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation
对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。
简单来说,Compilation
的职责就是构建模块和Chunk,并利用插件优化构建过程。
和 Compiler
用法相同,钩子类型不同,也可以在某些钩子上访问 tapAsync
和 tapPromise。
控制台输出console.log(compilation)
通过 Compilation
也能读取到 Compiler
对象。
源码2000多行,看不动了- -,有兴趣的可以自己看看。 https://github.com/webpack/webpack/blob/master/lib/Compilation.js
介绍几个常用的Compilation Hooks
buildModule(SyncHook)
:在模块开始编译之前触发,可以用于修改模
succeedModule(SyncHook)
:在模块开始编译之前触发,可以用于修改模块
finishModules(AsyncSeriesHook)
:当所有模块都编译成功后被调用
seal(SyncHook)
:当一次compilation
停止接收新模块时触发
optimizeDependencies(SyncBailHook)
:在依赖优化的开始执行
optimize(SyncHook)
:在优化阶段的开始执行
optimizeModules(SyncBailHook)
:在模块优化阶段开始时执行,插件可以在这个钩子里执行对模块的优化,回调参数:modules
optimizeChunks(SyncBailHook)
:在代码块优化阶段开始时执行,插件可以在这个钩子里执行对代码块的优化,回调参数:chunks
optimizeChunkAssets(AsyncSeriesHook)
:优化任何代码块资源,这些资源存放在compilation.assets
上。一个 chunk
有一个 files
属性,它指向由一个chunk
创建的所有文件。任何额外的 chunk
资源都存放在 compilation.additionalChunkAssets
上。回调参数:chunks
optimizeAssets(AsyncSeriesHook)
:优化所有存放在 compilation.assets
的所有资源。回调参数:assets
|
Compiler 和 Compilation 的区别
Compiler
代表了整个 Webpack
从启动到关闭的生命周期,而 Compilation
只是代表了一次新的编译,只要文件有改动,compilation
就会被重新创建。
常用 API
插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack
性能、等等,总之插件通过调用Webpack
提供的 API
能完成很多事情。 由于 Webpack
提供的 API
非常多,有很多 API
很少用的上,又加上篇幅有限,下面来介绍一些常用的 API。
读取输出资源、代码块、模块及其依赖
有些插件可能需要读取 Webpack
的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。 在 emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:
class Plugin { apply(compiler) { compiler.plugin('emit', function (compilation, callback) { // compilation.chunks 存放所有代码块,是一个数组 compilation.chunks.forEach(function (chunk) { // chunk 代表一个代码块 // 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块 chunk.forEachModule(function (module) { // module 代表一个模块 // module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组 module.fileDependencies.forEach(function (filepath) { }); }); // Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件 // 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时, // 该 Chunk 就会生成 .js 和 .css 两个文件 chunk.files.forEach(function (filename) { // compilation.assets 存放当前所有即将输出的资源 // 调用一个输出资源的 source() 方法能获取到输出资源的内容 let source = compilation.assets[filename].source(); }); }); // 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。 // 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。 callback(); }) } }
监听文件变化
Webpack
会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation
。
在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation
,为此可以使用如下代码:
// 当依赖的文件发生变化时会触发 watch-run 事件 compiler.hooks.watchRun.tap('MyPlugin', (watching, callback) => { // 获取发生变化的文件列表 const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes; // changedFiles 格式为键值对,键为发生变化的文件路径。 if (changedFiles[filePath] !== undefined) { // filePath 对应的文件发生了变化 } callback(); });
默认情况下 Webpack
只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML
文件。 由于 JavaScript
文件不会去导入 HTML
文件,Webpack
就不会监听 HTML
文件的变化,编辑 HTML
文件时就不会重新触发新的 Compilation
。 为了监听 HTML
文件的变化,我们需要把 HTML
文件加入到依赖列表中,为此可以使用如下代码:
compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => { // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译 compilation.fileDependencies.push(filePath); callback(); });
3、修改输出资源
有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit
事件,因为发生 emit
事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此emit事件是修改 Webpack 输出资源的最后时机。
所有需要输出的资源会存放在 compilation.assets
中,compilation.assets
是一个键值对,键为需要输出的文件名称,值为文件对应的内容。
设置 compilation.assets
的代码如下:
// 设置名称为 fileName 的输出资源 compilation.assets[fileName] = { // 返回文件内容 source: () => { // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer return fileContent; }, // 返回文件大小 size: () => { return Buffer.byteLength(fileContent, 'utf8'); } }; callback();
判断webpack使用了哪些插件
// 判断当前配置使用使用了 ExtractTextPlugin, // compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数 function hasExtractTextPlugin(compiler) { // 当前配置所有使用的插件列表 const plugins = compiler.options.plugins; // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例 return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null; }
以上4种方法来源于文章: [Webpack学习-Plugin] :http://wushaobin.top/2019/03/15/webpackPlugin/
管理 Warnings 和 Errors
做一个实验,如果你在 apply
函数内插入 throw new Error("Message")
,会发生什么,终端会打印出 Unhandled rejection Error: Message
。然后 webpack 中断执行。 为了不影响 webpack
的执行,要在编译期间向用户发出警告或错误消息,则应使用 compilation.warnings 和 compilation.errors。
compilation.warnings.push("warning"); compilation.errors.push("error");
文章中的案例demo代码展示
https://github.com/6fedcom/fe-blog/tree/master/webpack/plugin
webpack打包过程或者插件代码里该如何调试?
- 在当前webpack项目工程文件夹下面,执行命令行:
node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress
其中参数--inspect-brk就是以调试模式启动node:
终端会输出:
Debugger listening on ws://127.0.0.1:9229/1018c03f-7473-4d60-b62c-949a6404c81d For help, see: https://nodejs.org/en/docs/inspector
- 谷歌浏览器输入 chrome://inspect/#devices
点击inspect
- 然后点一下Chrome调试器里的“继续执行”,断点就提留在我们设置在插件里的debugger断点了。
debugger