揭秘webpack插件工作流程和原理(二)

简介: 揭秘webpack插件工作流程和原理

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 用法相同,钩子类型不同,也可以在某些钩子上访问 tapAsynctapPromise。

控制台输出console.log(compilation)8eef2ebd3ee0f55d88427444448c99e9_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpg

通过 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打包过程或者插件代码里该如何调试?

  1. 在当前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
  1. 谷歌浏览器输入 chrome://inspect/#devices

a78a30b29c939d843b90846c3e8c995e_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpg点击inspect

  1. 然后点一下Chrome调试器里的“继续执行”,断点就提留在我们设置在插件里的debugger断点了。

d55d55c393e7ccdef55447395daf67a9_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpgdebugger


目录
相关文章
|
1月前
|
测试技术 开发者
如何确保 Webpack plugin 与其他插件的兼容性?
【10月更文挑战第23天】确保 Webpack plugin 与其他插件的兼容性需要从多个方面进行考虑和努力。通过遵循规范、进行充分测试、保持沟通协作等方式,
|
1月前
|
监控 前端开发 JavaScript
Webpack 中 HMR 插件的工作原理
【10月更文挑战第23天】可以进一步深入探讨 HMR 工作原理的具体细节、不同场景下的应用案例,以及与其他相关技术的结合应用等方面的内容。通过全面、系统地了解 HMR 插件的工作原理,能够更好地利用这一功能,为项目的成功开发提供有力保障。同时,要不断关注技术的发展动态,以便及时掌握最新的 HMR 技术和最佳实践。
|
1月前
|
缓存 前端开发 JavaScript
Webpack 动态加载的原理
【10月更文挑战第23天】Webpack 动态加载通过巧妙的机制和策略,实现了模块的按需加载和高效运行,提升了应用程序的性能和用户体验。同时,它也为前端开发提供了更大的灵活性和可扩展性,适应了不断变化的业务需求和技术发展。
|
1月前
|
缓存 前端开发 JavaScript
webpack 原理
【10月更文挑战第23天】Webpack 原理是一个复杂但又非常重要的体系。它通过模块解析、依赖管理、加载器和插件的协作,实现了对各种模块的高效打包和处理,为现代前端项目的开发和部署提供了强大的支持。同时,通过代码分割、按需加载、热模块替换等功能,提升了应用程序的性能和用户体验。随着前端技术的不断发展,Webpack 也在不断演进和完善,以适应不断变化的需求和挑战。
|
2月前
|
缓存 前端开发 JavaScript
Webpack 打包的基本原理
【10月更文挑战第5天】
|
2月前
|
移动开发 JavaScript 前端开发
webpack学习四:使用webpack配置plugin,来使用HtmlWebpackPlugin、uglifyjs-webpack-plugin、webpack-dev-server等插件简化开发
这篇文章主要介绍了如何通过配置Webpack的插件,如HtmlWebpackPlugin、uglifyjs-webpack-plugin和webpack-dev-server,来简化前端开发流程。
67 0
webpack学习四:使用webpack配置plugin,来使用HtmlWebpackPlugin、uglifyjs-webpack-plugin、webpack-dev-server等插件简化开发
|
2月前
|
前端开发 JavaScript 数据可视化
Webpack加载器和插件之间有什么区别
【10月更文挑战第13天】Webpack加载器和插件之间有什么区别
|
3月前
|
JavaScript 前端开发
手写一个简易bundler打包工具带你了解Webpack原理
该文章通过手写一个简易的打包工具bundler,帮助读者理解Webpack的工作原理,包括模块解析、依赖关系构建、转换源代码以及生成最终输出文件的整个流程。
|
4月前
|
前端开发 JavaScript 开发者
Angular与Webpack协同优化:打造生产级别的打包配置——详解从基础设置到高级代码拆分和插件使用
【8月更文挑战第31天】在现代前端开发中,优化应用性能和加载时间至关重要,尤其是对于使用Angular框架的项目。本文通过代码示例详细展示了如何配置Webpack,以实现生产级别的打包优化。从基础配置到生产环境设置、代码拆分,再到使用加载器与插件,每个步骤都旨在提升应用效率,确保快速加载和稳定运行。通过这些配置,开发者能更好地控制资源打包,充分发挥Webpack的强大功能。
145 0
|
JSON JavaScript 前端开发