打包工具之Webpack篇(六) | 青训营笔记

132 阅读8分钟

这是我参与第四届青训营笔记创作活动的第十三天

今天这篇我们开始对Plugin"下手",了解Plugin原理以及用法,最后试着简单实现一些简易的Plugin。话不多说,我们开始吧!

Plugin是什么?

很多知名工具,如:VS Code.Web Storm、ChromeFirefox、Babel、Webpack、Rollup、Eslint、Vue、Redux、Quill、Axios等等,都设计了所谓“插件”架构,为什么?

让我们用webpack来具体展开说说。

image.png

如图所示,这是webpack编译的一个过程,这是一个特别复杂的过程,那么新人如果需要了解整个流程细节的话上手成本高,加上功能迭代成本高,牵一发动全身,没有插件会导致功能僵化,作为开源项目而言缺乏成长性也总起来也就是心智成本高、可维护性低、生命力弱。而通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力,而且插件架构有一个精髓就是:对扩展开发,对修改封闭

由于插件的优势,甚至webpack本身的很多功能也是基于插件实现的。

Plugin 工作原理

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

——「深入浅出 Webpack」

站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

Webpack 内部的钩子

什么是钩子

钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。

image.png

钩子的核心信息:

时机:编译过程的特定节点,Webpack 会以钩子形式通知插件此刻正在发生什么事情;

上下文:通过 tapable 提供的回调机制,以参数方式传递上下文信息;

交互:在上下文参数对象中附带了很多存在side effect 的交互接口,插件可以通过这些接口改变

举个例子:

image.png

时机:compier.hooks.compilation

参数:compilation

交互:dependencyFactories.set

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");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");

Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:可以注册同步钩子和异步钩子。

  • tapAsync:回调方式注册异步钩子。

  • tapPromise:Promise 方式注册异步钩子。

Plugin 构建对象

Compiler

compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。

这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。

它有以下主要属性:

  • compiler.options 可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
  • compiler.inputFileSystemcompiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。
  • compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。

参考: compiler hooks 文档

Compilation

compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。

一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

它有以下主要属性:

  • compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。

  • compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。

  • compilation.assets 可以访问本次打包生成所有文件的结果。

  • compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。

参考: compilation hooks 文档

生命周期简图

开发一个插件

最简单的插件

  • plugins/test-plugin.js
class TestPlugin {constructor() {
    console.log("TestPlugin constructor()");}
    // 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
    // 2. webpack创建 compiler 对象
    // 3. 遍历所有插件,调用插件的 apply 方法apply(compiler) {
    console.log("TestPlugin apply()");}
}

module.exports = TestPlugin;

注册 hook

const { Compilation } = require("webpack");

/* 
    1. webpack加载webpack.config.js中所有的配置,此时就会new TestPlugin(),执行插件的constructor
    2.webpack创建compiler对象
    3.遍历所有的plugins插件,调用插件的apply方法
    4.执行剩下编译流程(触发各个hooks事件)
*/
class TestPlugin {
    constructor() {
        console.log(" TestPlugin constructor");
    }
    apply(compiler) {
        console.log(" TestPlugin apply");
        // 由文档可知,environment是同步钩子,所以需要使用tap注册
        compiler.hooks.environment.tap(" TestPlugin", () => {
            console.log(" TestPlugin environment");
        })
        // 由文档可知,emit是异步串行钩子
        compiler.hooks.emit.tap(" TestPlugin", (compilation) => {
            console.log("TestPlugin emit 111");
        })
        compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
            setTimeout(() => {
                console.log("TestPlugin emit 222");
                callback();
            }, 2000);
        });
        compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log("TestPlugin emit 333");
                    resolve();
                }, 1000);
            });
        });
        // 由文档可知,make是异步并行钩子 AsyncParallelHook
        compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
            // 需要在compilation hooks触发前才能使用
            compilation.hooks.seal.tap("TestPlugin", () => {
                console.log("TestPlugin seal");
            });
            setTimeout(() => {
                console.log("TestPlugin make 111");
                callback();
            }, 3000);
        });
        compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
            setTimeout(() => {
                console.log("TestPlugin make 222");
                callback();
            }, 1000);
        });
        compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
            setTimeout(() => {
                console.log("TestPlugin make 333");
                callback();
            }, 2000);
        })
    }

}
module.exports = TestPlugin;

运行结果

启动调试

通过调试查看 compilercompilation 对象数据情况。

  1. package.json 配置指令
 "scripts": {
    "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
  },
  1. 运行指令
npm run debug

此时控制台输出以下内容:

PS C:\Users\86176\Desktop\source> npm run debug

> source@1.0.0 debug
> node --inspect-brk ./node_modules/webpack-cli/bin/cli.js

Debugger listening on ws://127.0.0.1:9229/629ea097-7b52-4011-93a7-02f83c75c797
For help, see: https://nodejs.org/en/docs/inspecto
  1. 打开 Chrome 浏览器,F12 打开浏览器调试控制台。

此时控制台会显示一个绿色的图标

  1. 点击绿色的图标进入调试模式。

  2. 在需要调试代码处用 debugger 打断点,代码就会停止运行,从而调试查看数据情况。

思考题

1、Loader与插件有什么区别?

答:

(1)Loader主要是用来解析和检测对应资源,负责源文件从输入到输出的转换,它专注于实现资源模块加载,数据格式的转换,因为webpack只能处理js数据因此通过部分loader可以将webpack不能识别的数据转换成可识别的数据,在一定程度上可以将其视为管道,一个loader可以视为一个处理环节;Plugin主要是通过webpack内部的钩子机制,在webpack构建的不同阶段执行一些额外的工作,它的插件是一个函数或者是一个包含apply方法的对象,接受一个compile对象,通过webpack的钩子来处理资源。

(2) loader开发方式是通过module.exports导出一个函数,该函数默认参数source(即要处理的资源文件),在函数体中处理资源(loader里配置响应的loader后),通过return返回最终打包后的结果(这里返回的结果需为字符串形式)。plugin开发方式是通过钩子机制实现,插件必须是一个函数或包含apply方法的对象,在方法体内通过webpack提供的API获取资源做响应处理 将处理完的资源通过webpack提供的方法返回该资源。

2、“钩子”有什么作用?如何监听钩子函数?

答: (1)钩子(Hook)是Windows消息处理机制的一个平台,应用程序可以在上面设置子程序以监听定窗口的某种消息,而且所监听的窗口可以是由其他他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息或特定事件。钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。

(2)这是webpack提供的编译函数模板,监听webpack的钩子事件,然后会触发 compilation,和插件执行完成的回调。

    compiler.plugin('webpacksEventHook', function(compilation,callback) {
        console.log('this is customer plugins');
        callback();
    });

参考:

《Webpack插件架构深度讲解》

《[万字总结]一文吃透Webpack核心原理》

至此,关于插件的就介绍到这里。最后的最后,谢谢大家这么厉害还来看我,如果发现问题或者需要补充的点麻烦大家通过评论告诉我。博取众长,共同进步!