揭秘webpack plugin
前言
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 工作原理
plugin 的工作原理,简单来说,就是webpack在编译过程中提供了一些生命周期钩子,我们的插件会在这些钩子事件中注入我们要执行的任务(注册处理器),当 webpack 执行构建的时候,它的 tapable 事件流会自动调用这些钩子,从而触发我们注入的方法,执行我们的自定义任务。
三、webpack 的一些底层逻辑
开发一个 plugin 比开发一个 loader 更高级一些,因为我们会用到一些 webpack 比较底层的内部组件。因此我们需要了解一些 webpack 的底层逻辑。
webpack 内部执行流程
一次完整的 webpack 打包大致是这样的过程:
- 将命令行参数与
webpack 配置文件
合并、解析得到参数对象。 - 参数对象传给 webpack 执行得到
compiler
对象。 - 执行
compiler
的run
方法开始编译。每次执行run
编译都会生成一个compilation
对象。 - 触发
compiler
的make
方法分析入口文件,调用compilation
的buildmodule
方法创建主模块对象。 - 生成入口文件
ast(抽象语法树)
,通过ast
分析和递归加载依赖模块。 - 所有模块分析完成后,执行
compilation
的seal
方法对每个chunk
进行整理、优化、封装。 - 最后执行
compiler
的emitassets
方法把生成的文件输出到output
的目录中。
webpack 底层基本流程图
webpack 内部的一些钩子
在 webpack 编译打包的过程中,会触发一些关键事件,为了方便我们直接介入和控制编译过程,webpack 把这些事件封装成接口暴露了出来,这些接口被叫做 hooks
(钩子)。开发插件,离不开这些钩子。
tapable
tapable 是 webpack 的核心功能库,它为 webpack 提供了统一的插件接口(钩子)类型定义。webpack 中目前有十种 hooks
,在 tapable 源码中可以看到,他们是:
// https://github.com/webpack/tapable/blob/master/lib/index.js exports.synchook = require("./synchook"); exports.syncbailhook = require("./syncbailhook"); exports.syncwaterfallhook = require("./syncwaterfallhook"); exports.syncloophook = require("./syncloophook"); exports.asyncparallelhook = require("./asyncparallelhook"); exports.asyncparallelbailhook = require("./asyncparallelbailhook"); exports.asyncserieshook = require("./asyncserieshook"); exports.asyncseriesbailhook = require("./asyncseriesbailhook"); exports.asyncseriesloophook = require("./asyncseriesloophook"); exports.asyncserieswaterfallhook = require("./asyncserieswaterfallhook");
tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
-
tap
:注册钩子,同步钩子和异步钩子都可以使用。 -
tapasync
:回调方式注册异步钩子。 -
tappromise
:promise方式注册异步钩子。
webpack 里有几个非常重要的对象,他们是 compiler
, compilation
和 javascriptparser
。这些对象都继承了 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 | 生成文件的时候执行,提供访问产出文件信息的入口,回调参数:file ,info
|
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 由如下部分组成:
- 一个命名的 javascript 方法或者 javascript 类。
- 它的原型上需要定义一个叫做
apply
的方法。 - 注册一个事件钩子。
- 操作webpack内部实例特定数据。
- 功能完成后,调用webpack提供的回调。
一个基本的 plugin 代码结构大致长这个样子:
// plugins/myplugin.js class myplugin { apply(compiler) { compiler.hooks.done.tap('my plugin', (stats) => { console.log('bravo!'); }); } } module.exports = myplugin;
这就是一个最简单的 webpack 插件了,它注册了 compiler
上的异步串行钩子 done
,在钩子中注入了一条控制台打印的语句。根据上文钩子的介绍我们可以知道,done
会在一次编译完成后执行。所以这个插件会在每次打包结束,向控制台首先输出这句 bravo!
。
开发一个文件清单插件
我希望每次webpack打包后,自动产生一个打包文件清单,上面要记录文件名、文件数量等信息。
思路:
- 显然这个操作需要在文件生成到dist目录之前进行,所以我们要注册的是
compiler
上的emit
钩子。 -
emit
是一个异步串行钩子,我们用tapasync
来注册。 - 在
emit
的回调函数里我们可以拿到compilation
对象,所有待生成的文件都在它的assets
属性上。 - 通过
compilation.assets
获取我们需要的文件信息,并将其整理为新的文件内容准备输出。 - 然后往
compilation.assets
添加这个新的文件。
插件完成后,最后将写好的插件放到 webpack 配置中,这个包含文件清单的文件就会在每次打包的时候自动生成了。
实现:
// plugins/filelistplugin.js class filelistplugin { constructor (options) { // 获取插件配置项 this.filename = options && options.filename ? options.filename : 'filelist.md'; } apply(compiler) { // 注册 compiler 上的 emit 钩子 compiler.hooks.emit.tapasync('filelistplugin', (compilation, cb) => { // 通过 compilation.assets 获取文件数量 let len = object.keys(compilation.assets).length; // 添加统计信息 let content = `# ${len} file${len>1?'s':''} emitted by webpack\n\n`; // 通过 compilation.assets 获取文件名列表 for(let filename in compilation.assets) { content += `- ${filename}\n`; } // 往 compilation.assets 中添加清单文件 compilation.assets[this.filename] = { // 写入新文件的内容 source: function() { return content; }, // 新文件大小(给 webapck 输出展示用) size: function() { return content.length; } } // 执行回调,让 webpack 继续执行 cb(); }) } } module.exports = filelistplugin;
测试:
在 webpack.config.js 中配置我们自己写的plugin:
plugins: [ new myplugin(), new filelistplugin({ filename: '_filelist.md' }) ]
npm run build
执行,可以看到生成了 _filelist.md
文件:
打开 dist
目录,可以看到_filelist.md
文件中列出了 webpack 打包后的文件:
成功!
总结
本文总结了 webpack plugin 的工作原理、wepack底层执行的基本流程以及介绍了 tapable 和常用的 hooks,最后通过两个小例子演示了如何自己开发一个webpack插件。
开发插件并非难如登天的事情,当遇到通过配置无法解决的问题,又一时找不到好的插件时,不如试试自己编写一个插件来解决,相信我,你会越来越强的!
本文的源码均可在这里获取:
欢迎交流~
happy new year!
--
参考
欢迎转载,转载请注明出处: