图解Webpack实现Plugin

4次阅读

共计 4274 个字符,预计需要花费 11 分钟才能阅读完成。

关注公众号“执鸢者 ”,回复“ 书籍 ”获取大量前端学习资料, 回复“ 前端视频”获取大量前端教学视频。

面试工作加分项!
Plugin 是 webpack 生态系统的重要组成部分,其目的是解决 loader 无法实现的其他事,可用于执行范围更广的任务,为 webpack 带来很大的灵活性。目前存在的 plugin 并不能完全满足所有的开发需求,所以定制化符合自己需求的 plugin 成为学习 webpack 的必经之路。下面将逐步阐述 plugin 开发中几个关键技术点并实现 plugin。

一、Webpack 构建流程

在实现自己的 Plugin 之前,需要了解一下 Webpack 的构建流程(下图)。Webpack 的构建流程类似于一条生产线(webpack 的事件流机制),在特定时机会广播对应的事件,插件就可以监听这些事件的发生,从而在特定的时机做特定的事情。

上图中展示了 Webpack 构建流程的三个阶段及每个阶段广播出来的事件。

  1. 初始化阶段:启动构建;紧接着从配置文件和 Shell 语句中读取并合并参数,得到最终参数;用上一步得到的参数初始化 Compiler 对象并加载所有配置的插件。
  2. 从 Entey 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理,得到每个模块被翻译后的最终内容及它们之间的依赖关系。
  3. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入到输出列表中,最终将所需输出文件的内容写入文件系统中。

二、编写 Plugin

编写 Plugin 主要分为四个步骤:

  • 创建一个具名 JavaScript 函数
  • 在其原型上定义 apply 方法
  • 给 webpack 构建流程中相关事件挂载事件钩子进行监听
  • 钩子函数触发后,利用 compiler、compilation 等进行相关操作,达到需要的效果

2.1 基本结构

class MyPlugin {constructor(options) { }

    apply(compiler) {console.log(compiler);
        compiler.hooks.emit.tap('MyPlugin', () => {console.log('plugin is used!!!');
        })
    }
}

module.exports = MyPlugin;

2.2 apply 方法

apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。

2.3 钩子函数

在 webpack 整个编译过程中暴露出来大量的 Hook 供内部 / 外部插件使用,将 Hook 注册后即可实现对整个 webpack 事件流中对应事件的监听。这些钩子函数的核心是 Tapable, 该包暴露出很多钩子类,利用这些类创建了上述钩子函数。常见的钩子主要有以下几种:

暴露在 compiler 和 compilation 上的钩子函数均是基于上述钩子构建。

2.4 钩子函数注册方式

在 plugin 中,钩子注册方式有三种:tap、tapAsync、tapPromise,根据钩子函数的类型采用相应的方法进行注册。同步钩子函数利用 tap 注册,异步钩子函数利用 tap、tapAsync、tapPromise 进行注册。

  1. tap

tap 可以用来注册同步钩子也能用来注册异步钩子。

compiler.hooks.compile.tap('MyPlugin', compilationParams => {console.log('以同步方式触及 compile 钩子')
});
  1. tapAsync

tapAsync 能够用来注册异步钩子,并通过 callback 告知 Webpack 异步逻辑执行完毕。

compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {console.log('tapAsync 异步');
    callback();});
  1. tapPromise

tapPromise 能够用来注册异步钩子,其通过返回 Promise 来告知 Webpack 异步逻辑执行完毕。(实现方式有两种,一种是通过返回 Promse 函数,另一种是利用 async 实现)。

// 方式一
compiler.hooks.run.tapPromise('MyPlugin', (compiler) => {return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {console.log('tapPromise 异步')
    })
})

// 方式二
compiler.hooks.run.tapPromise('MyPlugin', async (compiler) => {await new Promise(resolve => setTimeout(resolve, 1000));
    console.log('tapPromise 异步 async')
})

2.5 Compiler 和 Compilation

Compiler 和 Compilation 是 Plugin 和 Webpack 之间的桥梁,所以了解其具体含义至关重要,其含义如下:

  • Compiler 对象包含了 Webpack 环境的所有配置信息,包含 options、loaders、plugins 等信息。这个对象在 Webpack 启动时被实例化,它是全局唯一的,可以简单地将它理解为 Webpack 实例。
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件发生变化,便有一次新的 Compilation 被

创建。Compilation 对象也提供了很多事件回调供插件进行扩展。通过 Compilation 也能读取到 Compiler 对象。

三、自定义钩子函数

难道钩子函数只有官方给的哪些吗?肯定不是的,我们能够自己按照自己的需求实现一个钩子函数,实现该钩子函数主要分为以下几个步骤:

  1. 调用 tabable 包,选择合适的钩子
  2. 将自己定义的钩子函数挂载到 compiler 或 compilation 上
  3. 在需要的位置注册该钩子函数
  4. 在你所需要的时机触发对应的钩子函数
// 该代码中有注册的同步钩子函数和异步钩子函数和其触发过程
const {SyncHook, AsyncSeriesHook} = require('tapable');

class MyHookPlugin {constructor() {}

    apply(compiler) {
        // 挂载
        compiler.hooks.myHook = new SyncHook(['arg1', 'arg2']);
        compiler.hooks.myAsyncHook = new AsyncSeriesHook(['arg1', 'arg2']);

        // 注册
        // 同步
        compiler.hooks.myHook.tap('myHook', (arg1, arg2) => {console.log('自己定义的钩子函数被触发', arg1, arg2);
        })

        // 异步 1
        compiler.hooks.myAsyncHook.tapAsync('myHook', (arg1, arg2, callback) => {console.log('异步钩子 tapAsync', arg1, arg2);
            callback();});
        // 异步 2
        compiler.hooks.myAsyncHook.tapPromise('myHook', (arg1, arg2) => {return new Promise((resolve) => {resolve({arg1, arg2});
            }).then((context) => {console.log('异步钩子 tapPromise', context)
            })
        });

        compiler.hooks.environment.tap('myHookPlugin', () => {
            // 触发
            // 同步
            compiler.hooks.myHook.call(1, 2);
            // 异步 1
            compiler.hooks.myAsyncHook.callAsync(1, 2, err => {console.log('触发完毕……   callAsync')
            })
            // 异步 2
            compiler.hooks.myAsyncHook.promise(1, 2).then(err => {console.log('触发完毕……   promise')
            })
        })
    }
}

module.exports = MyHookPlugin;

四、实现 Plugin

本节是 Plugin 实战,在生成资源到 output 目录之前生成资源清单。

class MyPlugin {constructor(options) { }

    apply(compiler) {compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {console.log('plugin is used!!!', "emit 事件");
            const mainfest = {}
            for (const name of Object.keys(compilation.assets)) {// compilation.assets[name].size()获取输出文件的大小;compilation.assets[name].source()获取内容
                mainfest[name] = compilation.assets[name].size();
                console.log(compilation.assets[name].source())
            }

            compilation.assets['mainfest.json'] = {source() {return JSON.stringify(mainfest);
                },
                size() {return this.source().length;
                }
            };

            callback();});
    }
}

module.exports = MyPlugin;

注:本文只是起到抛砖引玉的作用,希望各位大佬多多指点。

相关章节 <br/>
图解 Webpack————基础篇 <br/>
图解 Webpack————优化篇 <br/>
图解 Webpack————实现 Loader

欢迎大家关注公众号(回复“深入浅出 Webpack”获取深入浅出 Webpack 的 pdf 版本,回复“webpack05”获取本节的思维导图, 回复“书籍”获取大量前端学习资料, 回复“前端视频”获取大量前端教学视频)

正文完
 0