前言

Plugin(插件) 是 webpack 生态的的一个关键部分。它为社区提供了一种强大的方法来扩展 webpack 和开发 webpack 的编译过程。本文将尝试探索 webpack plugin,揭秘它的工作原理,以及如何开发一个 plugin。

一、Plugin 的作用

关于 Plugin 的作用,引用一下 webpack 官方的介绍:

Plugins expose the full potential of the webpack engine to third-party developers. Using staged build callbacks, developers can introduce their own behaviors into the webpack build process.

我把它通俗翻译了下:

通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。

二、Plugin 工作原理

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。

webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

——「深入浅出 Webpack」

站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

三、webpack 的一些底层逻辑

开发一个 plugin 比开发一个 loader 更高级一些(关于 loader 的开发,可以看我的另一篇文章「揭秘webpack loader」),因为我们会用到一些 webpack 比较底层的内部组件。因此我们需要了解一些 webpack 的底层逻辑。

webpack 内部执行流程

一次完整的 webpack 打包大致是这样的过程:

  • 将命令行参数与 webpack 配置文件 合并、解析得到参数对象。
  • 参数对象传给 webpack 执行得到 Compiler 对象。
  • 执行 Compilerrun方法开始编译。每次执行 run 编译都会生成一个 Compilation 对象。
  • 触发 Compilermake方法分析入口文件,调用 compilationbuildModule 方法创建主模块对象。
  • 生成入口文件 AST(抽象语法树),通过 AST 分析和递归加载依赖模块。
  • 所有模块分析完成后,执行 compilationseal 方法对每个 chunk 进行整理、优化、封装。
  • 最后执行 CompileremitAssets 方法把生成的文件输出到 output 的目录中。

webpack 底层基本流程图

webpack 内部的一些钩子

什么是钩子

钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来,这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。

Tapable

Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 hooks,在 Tapable 源码中可以看到,他们是:

  1. // https://github.com/webpack/tapable/blob/master/lib/index.js
  2. exports.SyncHook = require("./SyncHook");
  3. exports.SyncBailHook = require("./SyncBailHook");
  4. exports.SyncWaterfallHook = require("./SyncWaterfallHook");
  5. exports.SyncLoopHook = require("./SyncLoopHook");
  6. exports.AsyncParallelHook = require("./AsyncParallelHook");
  7. exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
  8. exports.AsyncSeriesHook = require("./AsyncSeriesHook");
  9. exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
  10. exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
  11. exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");

Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:可以注册同步钩子和异步钩子。
  • tapAsync:回调方式注册异步钩子。
  • tapPromise:Promise方式注册异步钩子。

webpack 里的几个非常重要的对象,Compiler, CompilationJavascriptParser 都继承了 Tapable 类,它们身上挂着丰富的钩子。

Compiler Hooks

Compiler 编译器模块是创建编译实例的主引擎。大多数面向用户的插件都首先在 Compiler 上注册。

compiler上暴露的一些常用的钩子:

钩子 类型 什么时候调用
run AsyncSeriesHook 在编译器开始读取记录前执行
compile SyncHook 在一个新的compilation创建之前执行
compilation SyncHook 在一次compilation创建后执行插件
make AsyncParallelHook 完成一次编译之前执行
emit AsyncSeriesHook 在生成文件到output目录之前执行,回调参数: compilation
afterEmit AsyncSeriesHook 在生成文件到output目录之后执行
assetEmitted AsyncSeriesHook 生成文件的时候执行,提供访问产出文件信息的入口,回调参数:fileinfo
done AsyncSeriesHook 一次编译完成后执行,回调参数:stats

Compilation Hooks

Compilation 是 Compiler 用来创建一次新的编译过程的模块。一个 Compilation 实例可以访问所有模块和它们的依赖。在一次编译阶段,模块被加载、封装、优化、分块、散列和还原。

Compilation 也继承了 Tapabl 并提供了很多生命周期钩子。

Compilation 上暴露的一些常用的钩子:

钩子 类型 什么时候调用
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

JavascriptParser Hooks

Parser 解析器实例在 Compiler 编译器中产生,用于解析 webpack 正在处理的每个模块。我们可以用它提供的 Tapable 钩子自定义解析过程。

JavascriptParser 上暴露的一些常用的钩子:

钩子 类型 什么时候调用
evaluate SyncBailHook 在计算表达式的时候调用。
statement SyncBailHook 为代码片段中每个已解析的语句调用的通用钩子
import SyncBailHook 为代码片段中每个import语句调用,回调参数:statement,source
export SyncBailHook 为代码片段中每个export语句调用,回调参数:statement
call SyncBailHook 解析一个call方法的时候调用,回调参数:expression
program SyncBailHook 解析一个表达式的时候调用,回调参数:expression

对webpack底层逻辑和tapable钩子有了这些了解后,我们就可以进一步尝试开发一个插件了。

四、如何开发一个webpack plugin

plugin 的基本结构

一个 webpack plugin 由如下部分组成:

  1. 一个命名的 Javascript 方法或者 JavaScript 类。
  2. 它的原型上需要定义一个叫做 apply 的方法。
  3. 注册一个事件钩子。
  4. 操作webpack内部实例特定数据。
  5. 功能完成后,调用webpack提供的回调。

一个基本的 plugin 代码结构大致长这个样子:

  1. // plugins/MyPlugin.js
  2. class MyPlugin {
  3. apply(compiler) {
  4. compiler.hooks.done.tap('My Plugin', (stats) => {
  5. console.log('Bravo!');
  6. });
  7. }
  8. }
  9. module.exports = MyPlugin;

这就是一个最简单的 webpack 插件了,它注册了 Compiler 上的异步串行钩子 done,在钩子中注入了一条控制台打印的语句。根据上文钩子的介绍我们可以知道,done 会在一次编译完成后执行。所以这个插件会在每次打包结束,向控制台首先输出这句 Bravo!

开发一个文件清单插件

我希望每次webpack打包后,自动产生一个打包文件清单,上面要记录文件名、文件数量等信息。

思路:

  • 显然这个操作需要在文件生成到dist目录之前进行,所以我们要注册的是Compiler上的emit钩子。
  • emit 是一个异步串行钩子,我们用 tapAsync 来注册。
  • emit 的回调函数里我们可以拿到 compilation 对象,所有待生成的文件都在它的 assets 属性上。
  • 通过 compilation.assets 获取我们需要的文件信息,并将其整理为新的文件内容准备输出。
  • 然后往 compilation.assets 添加这个新的文件。

插件完成后,最后将写好的插件放到 webpack 配置中,这个包含文件清单的文件就会在每次打包的时候自动生成了。

实现:

  1. // plugins/FileListPlugin.js
  2. class FileListPlugin {
  3. constructor (options) {
  4. // 获取插件配置项
  5. this.filename = options && options.filename ? options.filename : 'FILELIST.md';
  6. }
  7. apply(compiler) {
  8. // 注册 compiler 上的 emit 钩子
  9. compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
  10. // 通过 compilation.assets 获取文件数量
  11. let len = Object.keys(compilation.assets).length;
  12. // 添加统计信息
  13. let content = `# ${len} file${len>1?'s':''} emitted by webpack\n\n`;
  14. // 通过 compilation.assets 获取文件名列表
  15. for(let filename in compilation.assets) {
  16. content += `- ${filename}\n`;
  17. }
  18. // 往 compilation.assets 中添加清单文件
  19. compilation.assets[this.filename] = {
  20. // 写入新文件的内容
  21. source: function() {
  22. return content;
  23. },
  24. // 新文件大小(给 webapck 输出展示用)
  25. size: function() {
  26. return content.length;
  27. }
  28. }
  29. // 执行回调,让 webpack 继续执行
  30. cb();
  31. })
  32. }
  33. }
  34. module.exports = FileListPlugin;

测试:

在 webpack.config.js 中配置我们自己写的plugin:

  1. plugins: [
  2. new MyPlugin(),
  3. new FileListPlugin({
  4. filename: '_filelist.md'
  5. })
  6. ]

npm run build 执行,可以看到生成了 _filelist.md 文件:

打开 dist 目录,可以看到_filelist.md 文件中列出了 webpack 打包后的文件:

成功!

总结

本文总结了 webpack plugin 的工作原理、wepack底层执行的基本流程以及介绍了 tapable 和常用的 hooks,最后通过两个小例子演示了如何自己开发一个webpack插件。

开发插件并非难如登天的事情,当遇到通过配置无法解决的问题,又一时找不到好的插件时,不如试试自己编写一个插件来解决,相信我,你会越来越强的!

本文的源码均可在这里获取:https://github.com/yc111/webpack-plugin

欢迎交流~

Happy New Year!

--

参考

https://webpack.js.org/api/compiler-hooks/

https://webpack.js.org/api/compilation-hooks/

https://webpack.js.org/api/parser/

https://github.com/yc111/webpack/tree/master/lib

https://webpack.js.org/contribute/writing-a-plugin/

https://github.com/webpack/tapable#tapable

https://webpack.js.org/concepts/#plugins

https://webpack.js.org/api/plugins/

欢迎转载,转载请注明出处:http://champyin.com/2020/01/12/揭秘webpack-plugin/

揭秘webpack plugin的更多相关文章

  1. YYDS: Webpack Plugin开发

    目录 导读 一.cdn常规使用 二.开发一个webpack plugin 三.cdn优化插件实现 1.创建一个具名 JavaScript 函数(使用ES6的class实现) 2.在它的原型上定义 ap ...

  2. 如何开发webpack plugin

    继上回介绍了如何开发webpack loader 之后.趁热打铁,来继续看下webpack另一个核心组成:plugin. 下面也和loader一样,让我们一起从基本的官方文档着手看起. loader和 ...

  3. 简单webpack plugin 开发

    重要是学习下怎么开发webpack plugin,同时记录下 插件模型 webpack 是一个插件,可以是javascript class ,或者具名 class 定义apply 方法 指定一个绑定到 ...

  4. Webpack Plugin

    [Webpack Plugin] Since Loaders only execute transforms on a per-file basis, plugins are most commonl ...

  5. 案例实战之如何写一个webpack plugin

    案例实战之如何写一个webpack plugin 1.写一个生成打包文件目录的file.md文件 // 生成一个目录项目目录的文件夹 class FileListPlugin { constructo ...

  6. 揭秘webpack loader

    前言 Loader(加载器) 是 webpack 的核心之一.它用于将不同类型的文件转换为 webpack 可识别的模块.本文将尝试深入探索 webpack 中的 loader,揭秘它的工作原理,以及 ...

  7. [转] webpack之plugin内部运行机制

    简介 webpack作为当前最为流行的模块打包工具,几乎所有的主流前端开发框架(React.Vue等)都会将其作为默认的模块加载和打包工具.通过简单的配置项,使用各种相关的loader和plugin, ...

  8. 使用Webpack加速Vue.js应用的4种方式

    Webpack是开发Vue.js单页应用程序的重要工具. 通过管理复杂的构建步骤,你可以更轻松地开发工作流程,并优化应用程序的大小和性能. 其中介绍下面四种方式: 单个文件组件 优化Vue构建 浏览器 ...

  9. 翻译 | 关键CSS和Webpack: 减少阻塞渲染的CSS的自动化解决方案

    原文地址: Critical CSS and Webpack: Automatically Minimize Render-Blocking CSS 原文作者: Anthony Gore 译者: 蜗牛 ...

随机推荐

  1. WebService 基础知识点和用Postman调试

    阅读连接:Retrofit 用Soap协议访问WebService 详解 参考 1.java发HTTP POST请求(内容为xml格式) 2. android解析XML总结(SAX.Pull.Dom三 ...

  2. Python--day24--多继承

    如果本生没有func方法的话就调用距离自己最近的基类的方法 钻石继承: 查找方法的顺序:如下例的找func方法(广度优先) 例1: 例2: 漏斗继承: 小乌龟继承问题:(最顶端的节点F是最后查找的) ...

  3. POJ 2488 深搜dfs、

    题意:模拟国际象棋中马的走棋方式,其实和中国象棋的马走的方式其实是一样的,马可以从给定的方格棋盘中任意点开始,问是否能遍历全部格子,能的话输出字典序最小的走棋方式,否则输出impossible 思路: ...

  4. index() 方法返回指定元素相对于其他指定元素的 index 位置。

  5. Java 参数的值传递和引用传递

    在Java中,方法的参数的传递分为值传递(基本数据)和引用传递(引用数据:对象.字符串),这是最容易接受的.如果你能知道有这两种情况存在,那么,在遇到调用方法时,你可以避免很多问题的产生.但是,仔细查 ...

  6. java 使用反射调用方法

    每个Method的对象对应一个具体的底层方法.获得Method对象后,程序可以使用Method里面的invoke方法来执行该底层方法. Object invoke(Object obj,Object ...

  7. 解决 npm run dev b报错 “'webpack-dev-server' 不是内部或外部命令,也不是可运行的程序 或批处理文件。”

    摘自:https://www.cnblogs.com/laraLee/p/9174383.html 前提: 电脑已经安装了nodeJS和npm,  项目是直接下载的zip包. 在项目目录下运行“npm ...

  8. Hamcrest匹配器框架

    其实在之前的文章中已经使用过 Hamcrest 匹配器框架,本篇文章将系统的介绍它的使用. 为什么要用Hamcrest匹配器框架 Hamcrest是一款软件测试框架, 可以通过现有的匹配器类检查代码中 ...

  9. java反射小实例

    利用反射实现 对配置文件的更改达到更改方法的目的 文件夹目录 首先Student类中有个sleep方法 pro.properties定义了参数 最后是RelectTestMain. package c ...

  10. JavaScript实现版本号比较

    /* * JavaScript实现版本号比较 * 传入两个字符串,当前版本号:curV:比较版本号:reqV * 调用方法举例:Version('5.12.3','5.12.2'),将返回true * ...