转载自知乎,原文链接:https://zhuanlan.zhihu.com/p/141447713
通过插件我们可以扩展webpack
,在合适的时机通过Webpack
提供的 API 改变输出结果,使webpack
可以执行更广泛的任务,拥有更强的构建能力。 本文将尝试探索 webpack
插件的工作流程,进而去揭秘它的工作原理。同时需要你对webpack
底层和构建流程的一些东西有一定的了解。
想要了解 webpack 的插件的机制,需要弄明白以下几个知识点: 1. 一个简单的插件的构成 2. webpack
构建流程 3. Tapable
是如何把各个插件串联到一起的 4. compiler
以及compilation
对象的使用以及它们对应的事件钩子。
插件基本结构
plugins
是可以用自身原型方法apply
来实例化的对象。apply
只在安装插件被Webpack compiler
执行一次。apply
方法传入一个webpck compiler
的引用,来访问编译器回调。
一个简单的插件结构:
1 | class HelloPlugin{ |
安装插件时, 只需要将它的一个实例放到Webpack config plugins
数组里面:
1 | const HelloPlugin = require('./hello-plugin.js'); |
先来分析一下webpack Plugin的工作原理
- 读取配置的过程中会先执行
new HelloPlugin(options)
初始化一个HelloPlugin
获得其实例。 - 初始化
compiler
对象后调用HelloPlugin.apply(compiler)
给插件实例传入compiler
对象。 - 插件实例在获取到
compiler
对象后,就可以通过compiler.plugin(事件名称, 回调函数)
监听到 Webpack 广播出来的事件。 并且可以通过compiler
对象去操作Webpack
。
webapck 构建流程
在编写插件之前,还需要了解一下Webpack
的构建流程,以便在合适的时机插入合适的插件逻辑。
Webpack的基本构建流程如下: 1. 校验配置文件 :读取命令行传入或者webpack.config.js
文件,初始化本次构建的配置参数 2. 生成Compiler
对象:执行配置文件中的插件实例化语句new MyWebpackPlugin()
,为webpack
事件流挂上自定义hooks
3. 进入entryOption
阶段:webpack
开始读取配置的Entries
,递归遍历所有的入口文件 4. run/watch
:如果运行在watch
模式则执行watch
方法,否则执行run
方法 5. compilation
:创建Compilation
对象回调compilation
相关钩子,依次进入每一个入口文件(entry
),使用loader对文件进行编译。通过compilation
我可以可以读取到module
的resource
(资源路径)、loaders
(使用的loader)等信息。再将编译好的文件内容使用acorn
解析生成AST静态语法树。然后递归、重复的执行这个过程, 所有模块和和依赖分析完成后,执行 compilation
的 seal
方法对每个 chunk 进行整理、优化、封装__webpack_require__
来模拟模块化操作. 6. emit
:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets
上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
1 | // 修改或添加资源 |
afterEmit
:文件已经写入磁盘完成done
:完成编译 奉上一张滴滴云博客的 WebPack 编译流程图,不喜欢看文字讲解的可以看流程图理解记忆 WebPack 编译流程图 原图出自:https://blog.didiyun.com/index.php/2019/03/01/webpack/
看完之后,如果还是看不懂或者对缕不清webpack构建流程的话,建议通读一下全文,再回来看这段话,相信一定会对webpack构建流程有很更加深刻的理解。
理解事件流机制 Tabable
webpack
本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。
Webpack
的 Tapable
事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运作,使得整个系统扩展性良好。
Tapable
也是一个小型的 library,是Webpack
的一个核心工具。类似于node
中的events
库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。
webpack中最核心的负责编译的Compiler
和负责创建bundles的Compilation
都是Tapable的实例,可以直接在 Compiler
和 Compilation
对象上广播和监听事件,方法如下:
1 | /** |
Tapable
类暴露了tap
、tapAsync
和tapPromise
方法,可以根据钩子的同步/异步方式来选择一个函数注入逻辑。
tap
同步钩子
1 | compiler.hooks.compile.tap('MyPlugin', params => { |
tapAsync
异步钩子,通过callback
回调告诉Webpack
异步执行完毕 tapPromise
异步钩子,返回一个Promise
告诉Webpack
异步执行完毕
1 | compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => { |
Tabable用法
1 | const { |
简单实现一个 SyncHook
1 | class Hook{ |
tapable是如何将webapck/webpack插件关联的?
Compiler.js
1 | const { AsyncSeriesHook ,SyncHook } = require("tapable"); |
MyPlugin.js
1 | const Compiler = require('./Compiler') |
想要深入了解tapable
的文章可以看看这篇文章:
webpack4
核心模块tapable
源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html
理解Compiler(负责编译)
开发插件首先要知道compiler
和 compilation
对象是做什么的
Compiler
对象包含了当前运行Webpack
的配置,包括entry、output、loaders
等配置,这个对象在启动Webpack
时被实例化,而且是全局唯一的。Plugin
可以通过该对象获取到Webpack的配置信息进行处理。
如果看完这段话,你还是没理解compiler
是做啥的,不要怕,接着看。 运行npm run build
,把compiler
的全部信息输出到控制台上console.log(Compiler)
。
1 | // 为了能更直观的让大家看清楚compiler的结构,里面的大量代码使用省略号(...)代替。 |
Compiler源码精简版代码解析
源码地址(948行):https://github.com/webpack/webpack/blob/master/lib/Compiler.js
1 | const { SyncHook, SyncBailHook, AsyncSeriesHook } = require("tapable"); |
apply
方法中插入钩子的一般形式如下:
1 | // compiler提供了compiler.hooks,可以根据这些不同的时刻去让插件做不同的事情。 |
理解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 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:
1 | class Plugin { |
2、监听文件变化
Webpack
会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation
。
在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation
,为此可以使用如下代码:
1 | // 当依赖的文件发生变化时会触发 watch-run 事件 |
默认情况下 Webpack
只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML
文件。 由于 JavaScript
文件不会去导入 HTML
文件,Webpack
就不会监听 HTML
文件的变化,编辑 HTML
文件时就不会重新触发新的 Compilation
。 为了监听 HTML
文件的变化,我们需要把 HTML
文件加入到依赖列表中,为此可以使用如下代码:
1 | compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => { |
3、修改输出资源
有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit
事件,因为发生 emit
事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此emit事件是修改 Webpack 输出资源的最后时机。
所有需要输出的资源会存放在 compilation.assets
中,compilation.assets
是一个键值对,键为需要输出的文件名称,值为文件对应的内容。
设置 compilation.assets
的代码如下:
1 | // 设置名称为 fileName 的输出资源 |
判断webpack使用了哪些插件
1 | // 判断当前配置使用使用了 ExtractTextPlugin, |
以上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。
1 | compilation.warnings.push("warning"); |
文章中的案例demo代码展示
https://github.com/6fedcom/fe-blog/tree/master/webpack/plugin
webpack打包过程或者插件代码里该如何调试?
- 在当前webpack项目工程文件夹下面,执行命令行:
1 | node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress |
其中参数–inspect-brk就是以调试模式启动node:
终端会输出:
1 | Debugger listening on ws://127.0.0.1:9229/1018c03f-7473-4d60-b62c-949a6404c81d |
- 谷歌浏览器输入 chrome://inspect/#devices
- 然后点一下Chrome调试器里的“继续执行”,断点就提留在我们设置在插件里的debugger断点了。