做者:champyin
原文: http://champyin.com/2020/01/1...
转载请注明出处
Plugin(插件) 是 webpack 生态的的一个关键部分。它为社区提供了一种强大的方法来扩展 webpack 和开发 webpack 的编译过程。本文将尝试探索 webpack plugin,揭秘它的工做原理,以及如何开发一个 plugin。javascript
关于 Plugin 的做用,引用一下 webpack 官方的介绍:java
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 能够执行更普遍的任务,拥有更强的构建能力。webpack
webpack 就像一条生产线,要通过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每一个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。
插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源作处理。webpack 经过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程当中会广播事件,插件只须要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运做。
webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
——「深刻浅出 Webpack」
站在代码逻辑的角度就是:webpack 在编译代码过程当中,会触发一系列 Tapable 钩子事件,插件所作的,就是找到相应的钩子,往上面挂上本身的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。git
开发一个 plugin 比开发一个 loader 更高级一些(关于 loader 的开发,能够看个人另外一篇文章「揭秘webpack loader」),由于咱们会用到一些 webpack 比较底层的内部组件。所以咱们须要了解一些 webpack 的底层逻辑。github
一次完整的 webpack 打包大体是这样的过程:web
webpack 配置文件
合并、解析获得参数对象。Compiler
对象。Compiler
的 run
方法开始编译。每次执行 run
编译都会生成一个 Compilation
对象。Compiler
的 make
方法分析入口文件,调用 compilation
的 buildModule
方法建立主模块对象。AST(抽象语法树)
,经过 AST
分析和递归加载依赖模块。compilation
的 seal
方法对每一个 chunk
进行整理、优化、封装。Compiler
的 emitAssets
方法把生成的文件输出到 output
的目录中。webpack 底层基本流程图express
钩子的本质就是:事件。为了方便咱们直接介入和控制编译过程,webpack 把编译过程当中触发的各种关键事件封装成事件接口暴露了出来,这些接口被很形象地称作:hooks
(钩子)。开发插件,离不开这些钩子。npm
Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 hooks
,在 Tapable 源码中能够看到,他们是:api
// 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 还统一暴露了三个方法给插件,用于注入不一样类型的自定义构建行为:app
tap
:能够注册同步钩子和异步钩子。tapAsync
:回调方式注册异步钩子。tapPromise
:Promise方式注册异步钩子。webpack 里的几个很是重要的对象,Compiler
, Compilation
和 JavascriptParser
都继承了 Tapable 类,它们身上挂着丰富的钩子。
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 是 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 |
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 由以下部分组成:
apply
的方法。一个基本的 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打包后,自动产生一个打包文件清单,上面要记录文件名、文件数量等信息。
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插件。
开发插件并不是难如登天的事情,当遇到经过配置没法解决的问题,又一时找不到好的插件时,不如试试本身编写一个插件来解决,相信我,你会愈来愈强的!
本文的源码都可在这里获取:https://github.com/yc111/webp...
欢迎交流~
Happy New Year!
--
参考
https://webpack.js.org/api/co...
https://webpack.js.org/api/co...
https://webpack.js.org/api/pa...
https://github.com/yc111/webp...
https://webpack.js.org/contri...
https://github.com/webpack/ta...
https://webpack.js.org/concep...
https://webpack.js.org/api/pl...
欢迎转载,转载请注明出处:http://champyin.com/2020/01/1...
本文同步发表于:
揭秘webpack plugin | 掘金