手写一个webpack插件

本文示例源代码请戳 github博客,建议你们动手敲敲代码。

webpack本质上是一种事件流的机制,它的工做流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责建立bundles的Compilation都是Tapable的实例。Tapable暴露出挂载plugin的方法,使咱们能 将plugin控制在webapack事件流上运行(以下图)。
图片描述css

Tabable是什么?

tapable库暴露了不少Hook(钩子)类,为插件提供挂载的钩子。webpack

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

图片描述

Tabable 用法git

1.new Hook 新建钩子github

  • tapable 暴露出来的都是类方法,new 一个类方法得到咱们须要的钩子。
  • class 接受数组参数options,非必传。类方法会根据传参,接受一样数量的参数。
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);

2.使用 tap/tapAsync/tapPromise 绑定钩子
tapable提供了同步&异步绑定钩子的方法,而且他们都有绑定事件和执行事件对应的方法。web

- Async* Sync*
绑定 tapAsync/tapPromise/tap tap
执行 callAsync/promise call

3.call/callAsync 执行绑定事件segmentfault

const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);

//绑定事件到webapck事件流
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3

//执行绑定的事件
hook1.call(1,2,3)

举个例子api

  • 定义一个Car方法,在内部hooks上新建钩子。分别是同步钩子 accelerateaccelerate接受一个参数)、break、异步钩子calculateRoutes
  • 使用钩子对应的绑定和执行方法
  • calculateRoutes使用tapPromise能够返回一个promise对象。
//引入tapable
const { SyncHook, AsyncParallelHook } = require('tapable');

//建立类
class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            break: new SyncHook(),
            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
        };
    }
}

const myCar = new Car();

//绑定同步钩子
myCar.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));

//绑定同步钩子 并传参
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));

//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => {
    // return a promise
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log(`tapPromise to ${source} ${target} ${routesList}`)
            resolve();
        },1000)
    })
});

//执行同步钩子
myCar.hooks.break.call();
myCar.hooks.accelerate.call('hello');

console.time('cost');

//执行异步钩子
myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => {
    console.timeEnd('cost');
}, err => {
    console.error(err);
    console.timeEnd('cost');
})

运行结果数组

WarningLampPlugin
Accelerating to hello
tapPromise to i love tapable
cost: 1008.725ms

calculateRoutes也可使用tapAsync绑定钩子,注意:此时用callback结束异步回调。promise

myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
    // return a promise
    setTimeout(() => {
        console.log(`tapAsync to ${source} ${target} ${routesList}`)
        callback();
    }, 2000)
});

myCar.hooks.calculateRoutes.callAsync('i', 'like', 'tapable', err => {
    console.timeEnd('cost');
    if(err) console.log(err)
})

运行结果服务器

WarningLampPlugin
Accelerating to hello
tapAsync to i like tapable
cost: 2007.045ms

进阶一下~
到这里可能已经学会使用tapable了,可是它如何与webapck/webpack插件关联呢?
咱们将刚才的代码稍做改动,拆成两个文件:Compiler.jsMyplugin.js

Compiler.js

  • Class Car类名改为webpack的核心Compiler
  • 接受options里传入的plugins
  • Compiler做为参数传给plugin
  • 执行run函数,在编译的每一个阶段,都触发执行相对应的钩子函数。
const {
    SyncHook,
    AsyncParallelHook
} = require('tapable');

class Compiler {
    constructor(options) {
        this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            break: new SyncHook(),
            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
        };
        let plugins = options.plugins;
        if (plugins && plugins.length > 0) {
            plugins.forEach(plugin => plugin.apply(this));
        }
    }
    run(){
        console.time('cost');
        this.accelerate('hello')
        this.break()
        this.calculateRoutes('i', 'like', 'tapable')
    }
    accelerate(param){
        this.hooks.accelerate.call(param);
    }
    break(){
        this.hooks.break.call();
    }
    calculateRoutes(){
        const args = Array.from(arguments)
        this.hooks.calculateRoutes.callAsync(...args, err => {
            console.timeEnd('cost');
            if (err) console.log(err)
        });
    }
}

module.exports = Compiler

MyPlugin.js

  • 引入Compiler
  • 定义一个本身的插件。
  • apply方法接受 compiler参数。
  • compiler上的钩子绑定方法。
  • 仿照webpack规则,向 plugins 属性传入 new 实例。
webpack 插件是一个具备 apply 方法的 JavaScript 对象。 apply 属性会被 webpack compiler 调用,而且 compiler 对象可在整个编译生命周期访问。
const Compiler = require('./Compiler')

class MyPlugin{
    constructor() {

    }
    apply(conpiler){//接受 compiler参数
        conpiler.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
        conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
        conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
            setTimeout(() => {
                console.log(`tapAsync to ${source}${target}${routesList}`)
                callback();
            }, 2000)
        });
    }
}


//这里相似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例

const myPlugin = new MyPlugin();

const options = {
    plugins: [myPlugin]
}
let compiler = new Compiler(options)
compiler.run()

运行结果

Accelerating to hello
WarningLampPlugin
tapAsync to iliketapable
cost: 2009.273ms

改造后运行正常,仿照Compiler和webpack插件的思路慢慢得理顺插件的逻辑成功。
更多其余Tabable方法

Plugin基础

Webpack 经过 Plugin 机制让其更加灵活,以适应各类应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 能够监听这些事件,在合适的时机经过 Webpack 提供的 API 改变输出结果。

一个最基础的 Plugin 的代码是这样的:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.hooks.compilation.tap('BasicPlugin', compilation => {
     
    });
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

在使用这个 Plugin 时,相关配置代码以下:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

Compiler 和 Compilation
在开发 Plugin 时最经常使用的两个对象就是 Compiler Compilation,它们是 Plugin Webpack 之间的桥梁。 CompilerCompilation 的含义以下:

  • Compiler 对象包含了 Webpack 环境全部的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局惟一的,能够简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被建立。Compilation 对象也提供了不少事件回调供插件作扩展。经过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 表明了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是表明了一次新的编译。

经常使用 API

插件能够用来修改输出文件、增长输出文件、甚至能够提高 Webpack 性能、等等,总之插件经过调用 Webpack 提供的 API 能完成不少事情。 因为 Webpack 提供的 API 很是多,有不少 API 不多用的上,又加上篇幅有限,下面来介绍一些经常使用的 API。

一、读取输出资源、代码块、模块及其依赖

有些插件可能须要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便作下一步处理。

在 emit 事件发生时,表明源文件的转换和组装已经完成,在这里能够读取到最终将输出的资源、代码块、模块及其依赖,而且能够修改输出资源的内容。 插件代码以下:

class MyPlugin {
  apply(compiler) {

    compiler.hooks.emit.tabAsync('MyPlugin', (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();

读取 compilation.assets 的代码以下:

// 读取名称为 fileName 的输出资源
  const asset = compilation.assets[fileName];
  // 获取输出资源的内容
  asset.source();
  // 获取输出资源的文件大小
  asset.size();
  callback();

实战!写一个插件

怎么写一个插件?参照webpack官方教程Writing a Plugin。 一个webpack plugin由一下几个步骤组成:

  • 一个JavaScript类函数。
  • 在函数原型 (prototype)中定义一个注入compiler对象的apply方法。
  • apply函数中经过compiler插入指定的事件钩子,在钩子回调中拿到compilation对象
  • 使用compilation操纵修改webapack内部实例数据。
  • 异步插件,数据处理完后使用callback回调

下面咱们举一个实际的例子,带你一步步去实现一个插件。
该插件的名称取名叫 EndWebpackPlugin,做用是在 Webpack 即将退出时再附加一些额外的操做,例如在 Webpack 成功编译和输出了文件后执行发布操做把输出的文件上传到服务器。 同时该插件还能区分 Webpack 构建是否执行成功。使用该插件时方法以下:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    new EndWebpackPlugin(() => {
      // Webpack 构建成功,而且文件输出了后会执行到这里,在这里能够作发布文件操做
    }, (err) => {
      // Webpack 构建失败,err 是致使错误的缘由
      console.error(err);        
    })
  ]
}

要实现该插件,须要借助两个事件:

  • done:在成功构建而且输出了文件后,Webpack 即将退出时发生;
  • failed:在构建出现异常致使构建失败,Webpack 即将退出时发生;

实现该插件很是简单,完整代码以下:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在构造函数中传入的回调函数
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.hooks.done.tab('EndWebpackPlugin', (stats) => {
      // 在 done 事件中回调 doneCallback
      this.doneCallback(stats);
    });
    compiler.hooks.failed.tab('EndWebpackPlugin', (err) => {
      // 在 failed 事件中回调 failCallback
      this.failCallback(err);
    });
  }
}
// 导出插件
module.exports = EndWebpackPlugin;

从开发这个插件能够看出,找到合适的事件点去完成功能在开发插件时显得尤其重要。 在 工做原理归纳 中详细介绍过 Webpack 在运行过程当中广播出经常使用事件,你能够从中找到你须要的事件。

参考
tapable
compiler-hooks
Compilation Hooks
writing-a-plugin
深刻浅出 Webpack
干货!撸一个webpack插件

相关文章
相关标签/搜索