webpack系列3-将plugin运用到项目中去

178 阅读8分钟

plugin

webpack打包是一种事件流的机制,在构建的过程当中埋下了很多hook,利用tapable,能让这些hook在webpack的事件流上有条不紊的运行。

也就是说,Webpack 在运行过程中会广播事件,plugin只需要监听它所关心的事件,就能加入到这条 webapck 机制中,在特定的时刻调用webpack提供的API执行相应的操作,去改变 webapck 的运作,使得整个系统扩展性良好。

结合我们已经用过的一些插件,比如HtmlWebpackPlugin,我们一般这么使用

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webapp'
    })
  ]
}

通过new去调用一个插件,所以很明显插件就是一个构造函数,或者是一个类,并且我们是可以传递参数进去的。

凡事都有门道,所以一个plugin的结构大概是下面这样的:

function MyPlugin (options) {
    // do something here...
}

class MyPlugin {
  constructor (options) {
    //  do something here...
  }
}

再进一步,plugin是怎么加入webpack的事件流里的呢?

在webpack源码里找答案

webpack打包前会将我们显式定义的插件,和内置插件合并, 然后调用每个插件的apply方法,并将compiler作为参数传递进去。

01.png

所以每个plugin都应该有一个apply方法,那么一个plugin的结构大概是下面这样的(以后用class举例):

class MyPlugin {
  constructor (options) {
      this.options = options
      // do something here...
  }
  apply(compiler) {
      // do something here...
  }
}

什么是compiler?

在apply调用时,接受了一个参数compiler,compiler在整个编译生命周期有着至关重要的作用。compiler继承自tapable。

tapable就是hook的核心库,发布,订阅,这些字眼是不是很熟悉,这些实际上类似发布订阅模式,都是注册一个事件,然后到了适当的时候执行。

以我们最常见的node.js的Event机制为例。通过on方法注册一个事件,然后通过emit方法进行触发。

const EventEmitter = require("event");
const myEmitter = new EventEmitter();

myEmitter.on("js",(..args) => {
    console.log(...args);
})
myEmitter.emit("js","新年好");

tapable的机制与Event类似,它可以用来定义各种各样的钩子。

大致分为四类

  • hook:普通钩子,监听器之间互相独立不干扰
  • BailHook:熔断钩子,某个监听返回非undefined时后续不执行
  • waterfallHook:瀑布钩子,上一个监听的返回值可传递给下一个
  • loopHook:循环钩子,如果当前未返回false则一直执行

具体有以下9种

tapable库同步钩子

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook

tapable库异步串行钩子

  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

tapable库异步并行钩子

  • AsyncParalleHook
  • AsyncparallBailHook

下面以同步钩子举个例子🌰

const { SyncHook } = require('tapable')

let hook = new SyncHook(['name', 'age'])

hook.tap('fn1', function (name, age) {
    console.log('fn1----', name, age)
})

hook.tap('fn2', function (name, age) {
    console.log('fn2----', name, age)
})

hook.call('luck', 18)

compiler身上有webpack编译过程中向外暴露的事件流名称,在MyPlugin的apply方法中通过compiler订阅webpack事件流,就可以实现自定义插件

具体有哪些事件呢?还是从源码中找答案

02.png

compiler.hooks 是钩子贯穿了整个webpack打包的生命周期,那么我们的插件就是注册到这些钩子(订阅钩子事件),当执行到这些钩子函数时,将会通知插件,并且通过回调返回参数给插件,就可以实现插件逻辑。

以compiler.hooks.done这个hook为例、继续完善plugin的结构:

class MyPlugin {
  constructor (options) {
      this.options = options
      // do something here...
  }
  apply(compiler) {
      compiler.hooks.done.tap(('MyPlugin'), (stats) => {
          console.log(stats)
          // do something here...
      })
  }
}

这里写几个重要的节点

webpack的各个阶段以及重要的钩子

关键钩子说明
environmenvt读取环境
afterEnvironment读取环境后触发
beforeRun运行前的准备活动,扩展了ompiler文件读取能力
run开始执行构建
beforeCompilebeforeCompile开始编译前的准备,创建的ModuleFactory,创建Compilation,并绑定ModuleFactory到Compilation上。同时处理一些不需要编译的模块,比如ExternalModule(远程模块)和DllModule(第三方模块)
compile进行编译
make编译的核心流程
afterCompile编译结束
shouldEmit确定编译时候成功,是否可以开始输出了。
emit输出文件
afterEmit输出完毕
done所有流程结束

同时也需要知道 compilation 这个重要的概念。complier和compilation都是tabable的实例对象,ompilation身上也有很多重要的hook、

03.png

Compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。

简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。

值得注意的是,compile是编译过程中创建了一个compilation,我们在一开始并不能获取到compilation,需要在compile创建compilation后再获取compilation身上的hooks。

// compiler.compile 阶段内生成compilation,  在compiler.hooks.make阶段作为参数传入

compiler.hooks.make.tapAsync(
	"MyPlugin",
		(compilation, callback) => {
            // 可以获取到compilation
            ccompilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
                // 对ccompilation上的hooks进行操作
            })
        }
);
// 或者后续在compiler的compilation hooks上获取compilation对象
compiler.hooks.compilation.tap("MyPlugin", compilation => {

})

Compiler 和 Compilation 的区别?

  • compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

在开发过程中,我们主要也是用到compiler和compilation身上的hooks来开发plugin,实现一些小而美的功能.

自定义插件

根据以上,一般一个具体的plugin 由下面部分组成:

  • 一个具名的JavaScript函数
  • 在它的原型上定义apply方法,会接受一个compiler参数
    • ( Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。)
  • 指定一个触及到webpack本身的事件钩子
  • 操作webpack内部的实例特定数据,实现自己的功能
  • 在实现功能后调用webpack提供的callback(必要的情况下,有些hooks不需要callback)

小试牛刀,编写一个插件,在每个产出的js头部添加注释

开发

// 插件输出类
class AddCommentPlugin {
    constructor(opts) {
        this.opts = opts;
    }
    apply(compiler) {
        // 注册自定义插件钩子到生成资源到 output 目录之前,拿到compilation对象
        compiler.hooks.emit.tap('AddCommentPlugin', compilation => {
            // 遍历构建产物
            Object.keys(compilation.assets).forEach(item => {
                // .source()是获取构建产物的文本
                // .assets中包含构建产物的文件名
                let content = compilation.assets[item].source();
                content = content.slice(0,0).concat(this.opts.comment, content);
                // console.info(content);
                // 更新构建产物对象
                compilation.assets[item] = {
                    source: () => content,
                    size: () => content.length
                }
            });
        });
    }
};

module.exports = AddCommentPlugin;

webpacl.config.js

...
plugins: [
    ...
    new AddCommentPlugin({
        comment : '/** 没错,这是一段注释 */'
    }),
    ...
]

结果

/** 没错,这是一段注释 *//******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};

官方插件推荐

BannerPlugin

在每个编译产出文件头部添加banner,当然也可以用来做一些变量提示

new webpack.BannerPlugin({
    banner: `if ( !window.xxxx ) {throw Error('请引入变量 window.xxxx ');}
    `, // 其值为字符串,将作为注释存在
    raw: true, // 如果值为 true,将直出,不会被作为注释
    entryOnly: false, // 如果值为 true,将只在入口 chunks 文件中添加
}),
CircularDependencyPlugin

检测项目中是否存在循环依赖

const CircularDependencyPlugin = require('circular-dependency-plugin');

new CircularDependencyPlugin({
    // exclude: /a\.js|node_modules/,
    // include specific files based on a RegExp
    include: /node_modules/,
    // add errors to webpack instead of warnings
    failOnError: true,
    // allow import cycles that include an asyncronous import,
    // e.g. via import(/* webpackMode: "weak" */ './file.js')
    allowAsyncCycles: false,
    // set the current working directory for displaying module paths
    cwd: process.cwd()
})
clean-webpack-plugin

用于每次打包时,将上次编译产出的dist目录内文件删除

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

new CleanWebpackPlugin()

等等,还有很多插件点击查看官方推荐

DefinePlugin

用于注入全局环境变量

new Webpack.DefinePlugin({
    "process.env": JSON.stringify(process.env)
})

常用的plugin我们都可以在官方上找到合适的插件,但是了解原理,我们也可以去根据项目自定义合适的插件帮助我们更好的服务项目,仔细想想,你的项目里有哪些小功能可以使用plugin,自己封装一个,将其用到项目中去。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿