经过插件咱们能够扩展webpack
,在合适的时机经过Webpack
提供的 API 改变输出结果,使webpack
能够执行更普遍的任务,拥有更强的构建能力。 本文将尝试探索 webpack
插件的工做流程,进而去揭秘它的工做原理。同时须要你对webpack
底层和构建流程的一些东西有必定的了解。php
想要了解 webpack 的插件的机制,须要弄明白如下几个知识点:css
webpack
构建流程
Tapable
是如何把各个插件串联到一块儿的
compiler
以及
compilation
对象的使用以及它们对应的事件钩子。
plugins
是能够用自身原型方法apply
来实例化的对象。apply
只在安装插件被Webpack compiler
执行一次。apply
方法传入一个webpck compiler
的引用,来访问编译器回调。html
class HelloPlugin{
// 在构造函数中获取用户给该插件传入的配置 constructor(options){ } // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象 apply(compiler) { // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑; compiler.hooks.emit.tap('HelloPlugin', (compilation) => { // 在功能流程完成后能够调用 webpack 提供的回调函数; }); // 若是事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时须要调用回调函数通知webpack,才会进入下一个处理流程。 compiler.plugin('emit',function(compilation, callback) { // 支持处理逻辑 // 处理完毕后执行 callback 以通知 Webpack // 若是不执行 callback,运行流程将会一直卡在这不往下执行 callback(); }); } } module.exports = HelloPlugin; 复制代码
安装插件时, 只须要将它的一个实例放到Webpack config plugins
数组里面:node
const HelloPlugin = require('./hello-plugin.js');
var webpackConfig = { plugins: [ new HelloPlugin({options: true}) ] }; 复制代码
先来分析一下webpack Plugin的工做原理webpack
new HelloPlugin(options)
初始化一个
HelloPlugin
得到其实例。
compiler
对象后调用
HelloPlugin.apply(compiler)
给插件实例传入
compiler
对象。
compiler
对象后,就能够经过
compiler.plugin(事件名称, 回调函数)
监听到 Webpack 广播出来的事件。 而且能够经过
compiler
对象去操做
Webpack
。
在编写插件以前,还须要了解一下Webpack
的构建流程,以便在合适的时机插入合适的插件逻辑。git
Webpack的基本构建流程以下:github
webpack.config.js
文件,初始化本次构建的配置参数
Compiler
对象:执行配置文件中的插件实例化语句
new MyWebpackPlugin()
,为
webpack
事件流挂上自定义
hooks
entryOption
阶段:
webpack
开始读取配置的
Entries
,递归遍历全部的入口文件
run/watch
:若是运行在
watch
模式则执行
watch
方法,不然执行
run
方法
compilation
:建立
Compilation
对象回调
compilation
相关钩子,依次进入每个入口文件(
entry
),使用loader对文件进行编译。经过
compilation
我能够能够读取到
module
的
resource
(资源路径)、
loaders
(使用的loader)等信息。再将编译好的文件内容使用
acorn
解析生成AST静态语法树。而后递归、重复的执行这个过程, 全部模块和和依赖分析完成后,执行
compilation
的
seal
方法对每一个 chunk 进行整理、优化、封装
__webpack_require__
来模拟模块化操做.
emit
:全部文件的编译及转化都已经完成,包含了最终输出的资源,咱们能够在传入事件回调的
compilation.assets
上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
// 修改或添加资源
compilation.assets['new-file.js'] = { source() { return 'var a=1'; }, size() { return this.source().length; } }; 复制代码
afterEmit
:文件已经写入磁盘完成
done
:完成编译
奉上一张滴滴云博客的WebPack
编译流程图,不喜欢看文字讲解的能够看流程图理解记忆web
WebPack 编译流程图 原图出自:blog.didiyun.com/index.php/2…chrome
看完以后,若是仍是看不懂或者对缕不清webpack构建流程的话,建议通读一下全文,再回来看这段话,相信必定会对webpack构建流程有很更加深入的理解。npm
webpack
本质上是一种事件流的机制,它的工做流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。
Webpack
的 Tapable
事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程当中会广播事件,插件只须要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运做,使得整个系统扩展性良好。
Tapable
也是一个小型的 library,是Webpack
的一个核心工具。相似于node
中的events
库,核心原理就是一个订阅发布模式。做用是提供相似的插件接口。
webpack中最核心的负责编译的Compiler
和负责建立bundles的Compilation
都是Tapable的实例,能够直接在 Compiler
和 Compilation
对象上广播和监听事件,方法以下:
/** * 广播事件 * event-name 为事件名称,注意不要和现有的事件重名 */ compiler.apply('event-name',params); compilation.apply('event-name',params); /** * 监听事件 */ compiler.plugin('event-name',function(params){}); compilation.plugin('event-name', function(params){}); 复制代码
Tapable
类暴露了tap
、tapAsync
和tapPromise
方法,能够根据钩子的同步/异步方式来选择一个函数注入逻辑。
tap
同步钩子
compiler.hooks.compile.tap('MyPlugin', params => {
console.log('以同步方式触及 compile 钩子。') }) 复制代码
tapAsync
异步钩子,经过callback
回调告诉Webpack
异步执行完毕 tapPromise
异步钩子,返回一个Promise
告诉Webpack
异步执行完毕
compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
console.log('以异步方式触及 run 钩子。') callback() }) compiler.hooks.run.tapPromise('MyPlugin', compiler => { return new Promise(resolve => setTimeout(resolve, 1000)).then(() => { console.log('以具备延迟的异步方式触及 run 钩子') }) }) 复制代码
const {
SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable"); 复制代码
class Hook{
constructor(args){ this.taps = [] this.interceptors = [] // 这个放在后面用 this._args = args } tap(name,fn){ this.taps.push({name,fn}) } } class SyncHook extends Hook{ call(name,fn){ try { this.taps.forEach(tap => tap.fn(name)) fn(null,name) } catch (error) { fn(error) } } } 复制代码
tapable
是如何将webapck/webpack
插件关联的?Compiler.js
const { AsyncSeriesHook ,SyncHook } = require("tapable");
//建立类 class Compiler { constructor() { this.hooks = { run: new AsyncSeriesHook(["compiler"]), //异步钩子 compile: new SyncHook(["params"]),//同步钩子 }; }, run(){ //执行异步钩子 this.hooks.run.callAsync(this, err => { this.compile(onCompiled); }); }, compile(){ //执行同步钩子 并传参 this.hooks.compile.call(params); } } module.exports = Compiler 复制代码
MyPlugin.js
const Compiler = require('./Compiler')
class MyPlugin{ apply(compiler){//接受 compiler参数 compiler.hooks.run.tap("MyPlugin", () => console.log('开始编译...')); compiler.hooks.complier.tapAsync('MyPlugin', (name, age) => { setTimeout(() => { console.log('编译中...') }, 1000) }); } } //这里相似于webpack.config.js的plugins配置 //向 plugins 属性传入 new 实例 const myPlugin = new MyPlugin(); const options = { plugins: [myPlugin] } let compiler = new Compiler(options) compiler.run() 复制代码
想要深刻了解tapable
的文章能够看看这篇文章:
webpack4
核心模块tapable
源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html
开发插件首先要知道compiler
和 compilation
对象是作什么的
Compiler
对象包含了当前运行Webpack
的配置,包括entry、output、loaders
等配置,这个对象在启动Webpack
时被实例化,并且是全局惟一的。Plugin
能够经过该对象获取到Webpack的配置信息进行处理。
若是看完这段话,你仍是没理解compiler
是作啥的,不要怕,接着看。 运行npm run build
,把compiler
的所有信息输出到控制台上console.log(Compiler)
。
// 为了能更直观的让你们看清楚compiler的结构,里面的大量代码使用省略号(...)代替。
Compiler { _pluginCompat: SyncBailHook { ... }, hooks: { shouldEmit: SyncBailHook { ... }, done: AsyncSeriesHook { ... }, additionalPass: AsyncSeriesHook { ... }, beforeRun: AsyncSeriesHook { ... }, run: AsyncSeriesHook { ... }, emit: AsyncSeriesHook { ... }, assetEmitted: AsyncSeriesHook { ... }, afterEmit: AsyncSeriesHook { ... }, thisCompilation: SyncHook { ... }, compilation: SyncHook { ... }, normalModuleFactory: SyncHook { ... }, contextModuleFactory: SyncHook { ... }, beforeCompile: AsyncSeriesHook { ... }, compile: SyncHook { ... }, make: AsyncParallelHook { ... }, afterCompile: AsyncSeriesHook { ... }, watchRun: AsyncSeriesHook { ... }, failed: SyncHook { ... }, invalid: SyncHook { ... }, watchClose: SyncHook { ... }, infrastructureLog: SyncBailHook { ... }, environment: SyncHook { ... }, afterEnvironment: SyncHook { ... }, afterPlugins: SyncHook { ... }, afterResolvers: SyncHook { ... }, entryOption: SyncBailHook { ... }, infrastructurelog: SyncBailHook { ... } }, ... outputPath: '',//输出目录 outputFileSystem: NodeOutputFileSystem { ... }, inputFileSystem: CachedInputFileSystem { ... }, ... options: { //Compiler对象包含了webpack的全部配置信息,entry、module、output、resolve等信息 entry: [ 'babel-polyfill', '/Users/frank/Desktop/fe/fe-blog/webpack-plugin/src/index.js' ], devServer: { port: 3000 }, output: { ... }, module: { ... }, plugins: [ MyWebpackPlugin {} ], mode: 'production', context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin', devtool: false, ... performance: { maxAssetSize: 250000, maxEntrypointSize: 250000, hints: 'warning' }, optimization: { ... }, resolve: { ... }, resolveLoader: { ... }, infrastructureLogging: { level: 'info', debug: false } }, context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',//上下文,文件目录 requestShortener: RequestShortener { ... }, ... watchFileSystem: NodeWatchFileSystem { //监听文件变化列表信息 ... } } 复制代码
源码地址(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
对象表明了一次资源版本构建。当运行 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
钩子 | 类型 | 何时调用 |
---|---|---|
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
表明了整个 Webpack
从启动到关闭的生命周期,而 Compilation
只是表明了一次新的编译,只要文件有改动,compilation
就会被从新建立。
插件能够用来修改输出文件、增长输出文件、甚至能够提高 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(); }); 复制代码
有些场景下插件须要修改、增长、删除输出的资源,要作到这点须要监听 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(); 复制代码
// 判断当前配置使用使用了 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/
作一个实验,若是你在 apply
函数内插入 throw new Error("Message")
,会发生什么,终端会打印出 Unhandled rejection Error: Message
。而后 webpack 中断执行。 为了避免影响 webpack
的执行,要在编译期间向用户发出警告或错误消息,则应使用 compilation.warnings 和 compilation.errors。
compilation.warnings.push("warning");
compilation.errors.push("error"); 复制代码
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
复制代码