webpack之插件

207 阅读9分钟

我们想要了解 webpack 的插件的机制,需要了解这么几件事情:

  • webpack 插件形态
  • webpack 运行流程
  • Tapable & Tapable 实例,以及 Tapable 的实例方法
  • compiler 和 compilation 对象以及事件钩子

那么什么是webpack?

webpack可以干什么呢?

webpack应该怎么用呢?

首先我们需要了解一件有意思的事情,那就是webpack插件机制是整个webpack工具的骨架,并且webpack本身也是利用这套插件机制构建出来的。

听着有点难懂,这个就得从webpack的源码看webpack的具体实现才能解释的清楚了,我们暂时可以理解为webpack的核心是一个编译器(Compiler),而这个编译器也是作为一个插件提供给webpack这个插件平台使用的。

webpack插件

先简单明了的了解一下什么是webpack插件,如下面代码描述:

webpack-plugin-demo就是一个简单的webpack插件了,这个插件它就是专注于处理webpack在整个编译过程中的某个特定的任务。

const webpack = require('webpack');


// 假如有这么一个webpack plugin
const DemoWebpackPlugin = require('webpack-plugin-demo');


webpack({
  //...
  plugins: [
    new DemoWebpackPlugin(/* some plugin options */)
  ]
  // ...
}

上面是一个最简单的webpack插件,那么怎么样的一个东西可以称之为webpack插件呢,也就是插件是由哪些部分构成的呢?

一个完整的webpack插件需要满足以下几点规则和特征:

  • 它是一个独立的模块
  • 模块对外暴露一个js函数
  • 在函数的原型(prototype)上定义了一个注入compiler对象的apply方法
  • apply函数中需要有通过compiler对象挂载的webpack事件钩子,钩子的回调中能拿到当前编译的compilation对象,如果是异步编译插件的话可以拿到回调callback
  • 完成自定义编译流程并处理complition对象的内部数据
  • 如果是异步编译插件的话,数据处理完成后执行callback回调

以上我们就描述了一个webpack插件的基础形态,具体每个插件的差异基本上是在自定义子编译流程这一步。

现在我们用代码来深刻理解下webpack插件的具体形态:

// 1、demo-webpack-plugin.js --->  独立的模块


// 2、模块对外暴露的js函数
DemowebpackPlugin = (pluginOptions) => {
  this.options = pluginOptions;
}


// 3、原型定义一个apply函数,并注入了compiler对象
DomeWebpackPlugin.prototype.apply = (compiler) => {
  // 4、挂载webpack事件钩子(这里挂载的是emit事件)
  compiler.plugin('emit', (compilation, callback) => {
    // ... 内部进行自定义的编译操作
    // 5、操作compilation对象的内部数据
    console.log(compilation);
    // 6、执行callback回调
    callback();
  })
}


// 暴露js函数
module.exports = DemoWebpackPlugin;

compiler & compilation对象

上面我们通过对webpack从插件的初步了解,我们注意到了一个Webpack插件中出现了两个对象,一个是compiler对象,一个是compilation对象,第一次听到这两个名词,肯定 不太懂,这是个什么东东??

其实compiler和compilation是整个webpack中最核心的两个对象,它们是扩展webpack功能的关键,为了后面更加利于对webpack插件机制的理解,我们先重点介绍一下这两个对象。

compiler对象

compiler对象时webpack的编译器对象,前文已经提到,webpack的核心就是编译器,compiler对象会在启动webpack的时候被一次性初始化,compiler对象中包含了所有webpack可定义操作的配置,例如loader、plugin、entry的配置等各种原始webpack配置等

在webpack插件中的自定义自编译流程中,我们肯定会用到compiler对象中的相关配置信息,相当于可以通过compiler对象拿到webpack主环境所有的信息。

compilation对象

这里我们首先先了解下什么是编译资源?

定义:编译资源就是webpack通过配置生成的一份静态资源管理Map(一切都在内存中保存),以key-value的形式描述一个webpack打包后的文件,编译资源就是这一个个key-value组成的Map,而编译资源就是需要由compilation对象生成的。

compilation实例继承于compiler,compilation对象代表了一次单一的版本webpack构建和生成编译资源的过程。当运行webpack开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源以及新的compilation对象。

一个compilation对象包含了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用。

由此可见,如果开发者需要通过一个插件的方式完成一个自定义的编译工作的话,如果涉及到需要改变编译后的资源产物,必定是离不开这个compilation对象。

webpack插件机制

webpack以插件的形式提供了灵活强大的自定义api功能。使用插件,我们可以为webpack添加功能。另外,webpack提供生命周期钩子以便注册插件。在每个生命周期点,webpack会运行所有注册的插件,并提供当前webpack编译状态信息。

作为webpack的使用者和开发者,想要玩转webpack,自定义一些自己的webpack插件是非常有必要的的,而想要更好的写出更加完善的webpack插件,需要更加深刻的了解webpack插件机制,以及了解整个webpack插件机制是如何运作起来的,webpack插件机制为webpack平台带来了极大的灵活性,而这一插件机制追根溯源却离不开一个叫做Tapable的库。

Tapable & Tapable实例

webpack的插件架构主要是基于Tapable实现的,它是webpack项目组里的一个内部库,主要是抽象了一套插件机制。

Tapable是Webpack的一个核心工具,Webpack中许多对象扩展自Tapable类。Tapable类暴露了tap、tapAsync和tapPromise方法,可以根据钩子的同步或者异步方式来选择一个函数注入逻辑。

tap

tap是一个同步钩子,同步钩子在使用的时候不可以包含异步调用,因为函数返回时异步逻辑有可能未执行完毕导致问题。

compiler.hooks.compile.tap('MyWebpackPlugin', params => {  console.log('我是同步钩子');})

tapAsync

tapAsync是一个异步钩子,可以通过callback告知webpack异步逻辑已经执行完毕。

下面我们用一个在1秒后打印文件列表功能的例子来说明一下:

compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
    setTimeout(() => {
        console.log('文件列表',  Object.keys(compilation.assets).join(','));
        callback();
    }, 1000)
})

tapPromise

tapPromise也是一个异步钩子,它和tapAsync的区别在于tapPromise是通过Promise来告知Webpack异步逻辑执行完毕。

下面我们用给一个将生成结果上传到CDN来实例下:

compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', compilation => {
    return new Promise((resolve, reject) => {
        const fileList = Object.keys(compilation.assets);
        uploadToCDN(filelist, err => {
            if (err) {
                reject(err);
                return;
            }
            resolve();
        });
    });
})

webpack运行流程

在这里我们需要先深入的了解一下webpack的整个运行机制。下面是一张webpack运行流程示意图:

大体来说:webpack的基本构建流程如下:

  • 检验配置文件
  • 生成Compiler对象
  • 初始化默认插件
  • run/watch:如果运行在watch模式则执行watch方法,否则执行run方法
  • compilation:创建compilation对象回调compilation相关钩子
  • emit:文件内容准备完成,准备生成文件,这是最后一次修改最终文件的机会
  • afterEmit:文件已经写入磁盘完成
  • done:完成编译

webpack 插件相关的事件钩子

通过以上的了解,webpack 插件中的自定义子编译流程都是需要配合 webpack 主编译流程发挥功效的,我们如何保证我们的插件中所定义的编译逻辑能够准确的在合适的时机运行呢?

其实,之前已经了解过 webpack 的两个重要的对象,compiler 和 compilation 对象在这里发挥了重要的作用,我们也已经了解到这两个对象都是 Tapable 实例,webpack 通过继承的 Tapable 实例的方法,分别在 compile 对象和 compilation 对象都注册了一系列的事件钩子,这样可以使得开发者能够在 webpack 编译的任何过程中都能够插入自己的自定义处理逻辑。

webpack 的做法就是使用 Tapable 实例的 applyPlugins* 方法来预先设定好这些事件钩子,当然,webpack 在一些其他的 Tapable 实例对象中也定义了一些内部或外部的事件钩子,在这里我们主要了解和插件相关的 compiler 对象和 compilation 对象一共有哪些事件钩子。

这里介绍一些compiler的事件钩子:

// 前提是先要拿到 compiler 对象,apply 方法的回调中就能拿到,这里假设能拿到 compiler 对象
compiler.plugin('emit', function (compilation, callback) {
    // 可以得到 compilation 对象,如果是异步的事件钩子,能拿到 callback 回调。
    // 做一些异步的事情
    setTimeout(function () {
        console.log("Done with async work...");
        callback();
    }, 1000);
});

可以明显的看出,compiler 的事件钩子是建立在整个编译过程的基础上的,粒度较粗,通常对编译的结果要做细粒度的处理的时候,少不了 compilation 对象上定义的事件钩子。

compilation事件钩子

compilation对象可以访问所有的模块和它们的依赖,在编译阶段,模块被加载、封闭、优化、分块、哈希、重建等,这将是编译中任何操作主要的生命周期。

normal-module-loader

普通模块 loader,真实地一个一个加载模块图(分析之后的所有模块一种数据结构)中所有的模块的函数。

optimize

优化编译,这个事件钩子特别重要,很多插件的优化工作都是基于这个事件钩子,表示 webpack 已经进入优化阶段。

optimize-chunks

这是个重要的事件钩子,webpack 的 chunk 优化阶段。可以拿到模块的依赖,loader 等,并进行相应的处理。