手写一个webpack插件

本文示例源代码请戳
github博客,发起人人着手敲敲代码。

webpack本质上是一种事宜流的机制,它的事变流程就是将各个插件串连起来,而完成这一切的中心就是Tapable,webpack中最中心的担任编译的Compiler和担任建立bundles的Compilation都是Tapable的实例。Tapable暴露出挂载plugin的要领,使我们能 将plugin控制在webapack事宜流上运转(以下图)。
《手写一个webpack插件》

Tabable是什么?

tapable库暴露了很多Hook(钩子)类,为插件供应挂载的钩子。

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

《手写一个webpack插件》

Tabable 用法

1.new Hook 新建钩子

  • tapable 暴露出来的都是类要领,new 一个类要领取得我们须要的钩子。
  • class 接收数组参数options,非必传。类要领会依据传参,接收一样数目的参数。
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);

2.运用 tap/tapAsync/tapPromise 绑定钩子
tapable供应了同步&异步绑定钩子的要领,而且他们都有绑定事宜和实行事宜对应的要领。

Async*Sync*
绑定tapAsync/tapPromise/taptap
实行callAsync/promisecall

3.call/callAsync 实行绑定事宜

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)

举个例子

  • 定义一个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完毕异步回调。

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。

1、读取输出资本、代码块、模块及其依靠

有些插件能够须要读取 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();
    })

  }
}

2、监听文件变化

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();
});

3、修正输出资本
有些场景下插件须要修正、增添、删除输出的资本,要做到这点须要监听 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插件

    原文作者:Alan
    原文地址: https://segmentfault.com/a/1190000019010101
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞