likes
comments
collection
share

如何编写webpack插件

作者站长头像
站长
· 阅读数 23

一.什么是插件和插件的作用?

插件是 webpack 生态系统的重要组成部分,为社区用户提供了一种强大方式来直接触及 webpack 的编译过程(compilation process)。

插件能够 钩入(hook) 到在每个编译(compilation)中触发的所有关键事件。在编译的每一步,插件都具备完全访问 compiler 对象的能力,如果情况合适,还可以访问当前 compilation 对象。

总结说:webpack本质上是一种事件流机制,他的工作流程就是将各个插件串联起来。那么各个插件在这个过程中是怎么执行的,利用的就是tapable 库。

我们可以把webpack理解为一条生产线,需要经过一系列处理流程后才能将源文件转换成输出结果。

这条生产线上的每个处理流程的职责都是单一的,多个流程之间会存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

我们的插件就像一个插入到生产线中的一个功能,在特定的时机对生产线上的资源会做处理。webpack它是通过 Tapable来组织这条复杂的生产线的。

二.什么是tapable?

tapable 这个小型 library, 是 webpack 的一个核心工具,但也可用于其他地方,以提供类似的插件接口。

webpack 中许多对象扩展自 Tapable 类。这个类暴露 taptapAsync 和 tapPromise 注册事件方法,对应的调用方法是 call,callAsync,promise,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

请查看 文档 了解更多信息。理解三种 tap 方法以及提供这些方法的钩子至关重要。

tapable给我们暴露了很多钩子类,能为我们的插件提供挂载的钩子。那么这些钩子可以分为2个类别,即 "同步" 和 "异步", 异步又分为两个类别,"并行" 还是 "串行",同步的钩子它只有 "串行"。

如图: 如何编写webpack插件

例举tapable的用法

参考文档如下:

mayufo.gitbooks.io/webpack/con…

const  {SyncHook} = require('tapable')


class Lesson {
    constructor(){
        this.hook={
            arch:new SyncHook(['data'])
        }
    }
    tap(){
       this.hook.arch.tap('vue',(data)=>{
           console.log('vue', data);
       })
    }
    call(){
        this.hook.arch.call(123)
    }
}


const l = new Lesson()
l.tap()
l.call(

三.如何开发自定义的插件?

参考文档:www.cnblogs.com/tugenhua070…

模拟 compiler 的原理

compiler.js 中

const { SyncHook, AsyncParallelHook } = require('tapable');


class Compiler {
  constructor(options) {
    this.hooks = {
      kzSyncHook: new SyncHook(['name', 'age']),
      kzAsyncHook: new AsyncParallelHook(['name', 'age'])
    };
    let plugins = options.plugins;
    if (plugins && plugins.length > 0) {
      plugins.forEach(plugin => plugin.apply(this));
    }
  }
  run() {
    console.log('开始执行了---------');
    this.kzSyncHook('我是空智', 31);
    this.kzAsyncHook('我是空智', 31);
  }
  kzSyncHook(name, age) {
    this.hooks.kzSyncHook.call(name, age);
  }
  kzAsyncHook(name, age) {
    this.hooks.kzAsyncHook.callAsync(name, age);
  }
}


module.exports = Compiler

main.js 中

const Compiler = require('./compiler');


class MyPlugin {
  constructor() {
    
  }
  apply(compiler) {
    compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => {
      console.log(`同步事件eventName1: ${name} this year ${age} 周岁了, 可是还是单身`);
    });
    compiler.hooks.kzAsyncHook.tapAsync('eventName2', (name, age) => {
      setTimeout(() => {
        console.log(`异步事件eventName2: ${name} this year ${age}周岁了,可是还是单身`);
      }, 1000)
    });
  }
}


const myPlugin = new MyPlugin();


const options = {
  plugins: [myPlugin]
};


const compiler = new Compiler(options);
compiler.run

如上就是我们仿照Compiler和webpack的插件原理逻辑实现的一个简单demo。也就是说在webpack源码里面也是通过类似的方式来做的。

上面只是一个简单实现的基本原理,但是在我们的webpack当中我们要如何实现一个插件呢?

在我们的webpack官网中会介绍编写一个插件要满足如下条件, 官网地址从官网得知:编写一个webpack插件需要由以下组成:

  1. 一个javascript命名函数。

  2. 在插件函数的prototype上定义一个 apply 方法。

  3. 指定一个绑定到webpack自身的钩子函数。

  4. 处理webpack内部实列的特定数据。

  5. 功能完成后调用webpack提供的回调函数。

在开发Plugin时我们最常用的两个对象就是 Compiler 和 Compilation, 他们是Plugin和webpack之间的桥梁。

compiler 对象

Compiler 对象包含了Webpack环境所有的配置信息,包含options (loaders, plugins...) 这些项,这个对象在webpack启动时候被实例化,它是全局唯一的。我们可以把它理解为webpack的实列。

基本源码可以看如下:

/ webpack/lib/webpack.js
const Compiler = require("./Compiler")


const webpack = (options, callback) => {
  ...
  // 初始化 webpack 各配置参数
  options = new WebpackOptionsDefaulter().process(options);


  // 初始化 compiler 对象,这里 options.context 为 process.cwd()
  let compiler = new Compiler(options.context);


  compiler.options = options                               // 往 compiler 添加初始化参数new NodeEnvironmentPlugin().apply(compiler)              // 往 compiler 添加 Node 环境相关方法for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
  ...

如上我们可以看到,Compiler对象包含了所有的webpack可配置的内容。开发插件时,我们可以从 compiler 对象中拿到所有和 webpack 主环境相关的内容。

compilation 对象

compilation 对象包含了当前的模块资源、编译生成资源、文件的变化等。当webpack在开发模式下运行时,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被创建。从而生成一组新的编译资源。

Compiler对象 与 Compilation 对象 的区别是: Compiler代表了是整个webpack从启动到关闭的生命周期。Compilation 对象只代表了一次新的编译。

Compiler对象的事件钩子:下面是一些比较常见的事件钩子及作用:

钩子               作用                     参数               类型
after-plugins     设置完一组初始化插件之后    compiler          sync
after-resolvers   设置完 resolvers 之后     compiler          sync
run               在读取记录之前             compiler          async
compile           在创建新 compilation之前  compilationParams  sync
compilation       compilation 创建完成      compilation        sync
emit              在生成资源并输出到目录之前  compilation        async
after-emit        在生成资源并输出到目录之后  compilation        async
done              完成编译                  stats              sync

四:插件中常用的API

1. 读取输出资源、模块及依赖

在我们的emit钩子事件发生时,表示的含义是:源文件的转换和组装已经完成了,在这里事件钩子里面我们可以读取到最终将输出的资源、代码块、模块及对应的依赖文件。并且我们还可以输出资源文件的内容。比如插件代码如下:

class MyPlugin {
  apply(compiler) {
    compiler.plugin('emit', function(compilation, callback) {
      // compilation.chunks 是存放了所有的代码块,是一个数组,我们需要遍历
      compilation.chunks.forEach(function(chunk) {
        /*
         * chunk 代表一个代码块,代码块它是由多个模块组成的。
         * 我们可以通过 chunk.forEachModule 能读取组成代码块的每个模块
        */
        chunk.forEachModule(function(module) {
          // module 代表一个模块。// module.fileDependencies 存放当前模块的所有依赖的文件路径,它是一个数组
          module.fileDependencies.forEach(function(filepath) {
            console.log(filepath);
          });
        });
        /*
         webpack 会根据chunk去生成输出的文件资源,每个chunk都对应一个及以上的输出文件。
         比如在 Chunk中包含了css 模块并且使用了 ExtractTextPlugin 时,
         那么该Chunk 就会生成 .js 和 .css 两个文件
        */
        chunk.files.forEach(function(filename) {
          // compilation.assets 是存放当前所有即将输出的资源。// 调用一个输出资源的 source() 方法能获取到输出资源的内容
          const source = compilation.assets[filename].source();
        });
      });
      /*
       该事件是异步事件,因此要调用 callback 来通知本次的 webpack事件监听结束。
       如果我们没有调用callback(); 那么webpack就会一直卡在这里不会往后执行。
      */
      callback();
    })
  }
}

2. 监听文件变化

webpack读取文件的时候,它会从入口模块去读取,然后依次找出所有的依赖模块。当入口模块或依赖的模块发生改变的时候,那么就会触发一次新的 Compilation。

在我们开发插件的时候,我们需要知道是那个文件发生改变,导致了新的Compilation, 我们可以添加如下代码进行监听。

// 当依赖的文件发生改变的时候 会触发 watch-run 事件class MyPlugin {
  apply(compiler) {
    compiler.plugin('watch-run', (watching, callback) => {
      // 获取发生变换的文件列表
      const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
      // changedFiles 格式为键值对的形式,当键为发生变化的文件路径if (changedFiles[filePath] !== undefined) {
        // 对应的文件就发生了变化了      }
      callback();
    });


    /*
     默认情况下Webpack只会监听入口文件或其依赖的模块是否发生变化,但是在有些情况下比如html文件发生改变的时候,那么webpack
     就会去监听html文件的变化。因此就不会重新触发新的 Compilation。因此为了监听html文件的变化,我们需要把html文件加入到
     依赖列表中。因此我们需要添加如下代码:
    */
    compiler.plugin('after-compile', (compilation, callback) => {
      /*
       如下的参数filePath是html文件路径,我们把HTML文件添加到文件依赖表中,然后我们的webpack会去监听html模块文件,
       html模板文件发生改变的时候,会重新启动下重新编译一个新的 Compilation.
      */
      compilation.fileDependencies.push(filePath);
      callback();
    })
  }
}

3. 修改输出资源

我们在第一点说过:在我们的emit钩子事件发生时,表示的含义是:源文件的转换和组装已经完成了,在这里事件钩子里面我们可以读取到最终将输出的资源、代码块、模块及对应的依赖文件。因此如果我们现在要修改输出资源的内容的话,我们可以在emit事件中去做修改。那么所有输出的资源会存放在 compilation.assets中,compilation.assets是一个键值对,键为需要输出的文件名,值为文件对应的内容。如下代码:

class MyPlugin {
  apply(compiler) {
    compiler.plugin('emit', (compilation, callback) => {
      // 设置名称为 fileName 的输出资源
      compilation.assets[fileName] = {
        // 返回文件内容
        source: () => {
          // fileContent 即可以代表文本文件的字符串,也可以是代表二进制文件的bufferreturn fileContent;
        },
        // 返回文件大小
        size: () => {
          return Buffer.byteLength(fileContent, 'utf8');
        }
      };
      callback();
    });
    // 读取 compilation.assets 代码如下:
    compiler.plugin('emit', (compilation, callback) => {
      // 读取名称为 fileName 的输出资源
      const asset = compilation.assets[fileName];
      // 获取输出资源的内容      asset.source();
      // 获取输出资源的文件大小      asset.size();
      callback();
    });
  }
}

编写插件实战

假如现在我们的项目的目录结构如下:

|--- webpack-plugin-demo
| |--- node_modules
| |--- js
| | |--- main.js               # js 的入口文件
| |--- plugins
| | |--- logWebpackPlugin.js   # 编写的webpack的插件,主要作用是打印日志功能
| |--- styles
| |--- index.html
| |--- package.json
| |--- webpack.config.js

1. 实现一个打印日志的LogWebpackPlugin插件

代码如下:

class LogWebpackPlugin {
  constructor(doneCallback, emitCallback) {
    this.emitCallback = emitCallback
    this.doneCallback = doneCallback
  }
  apply(compiler) {
    compiler.hooks.emit.tap('LogWebpackPlugin', () => {
      // 在 emit 事件中回调 emitCallbackthis.emitCallback();
    });
    compiler.hooks.done.tap('LogWebpackPlugin', (err) => {
      // 在 done 事件中回调 doneCallbackthis.doneCallback();
    });
    compiler.hooks.compilation.tap('LogWebpackPlugin', () => {
      // compilation('编译器'对'编译ing'这个事件的监听)
      console.log("The compiler is starting a new compilation...")
    });
    compiler.hooks.compile.tap('LogWebpackPlugin', () => {
      // compile('编译器'对'开始编译'这个事件的监听)
      console.log("The compiler is starting to compile...")
    });
  }
}


// 导出插件
module.exports = LogWebpackPlugin;

下面我们在webpack中引入该插件;如下代码:

// 引入LogWebpackPlugin 插件
const LogWebpackPlugin = require('./public/plugins/logWebpackPlugin');


module.exports = {
  plugins: [
    new LogWebpackPlugin(() => {
      // Webpack 模块完成转换成功
      console.log('emit 事件发生啦,所有模块的转换和代码块对应的文件已经生成好~')
    } , () => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
      console.log('done 事件发生啦,成功构建完成~')
    })
  ]
}

2.实现去掉注释

class MyPlugin {
  constructor(options) {
    this.options = options;
    this.externalModules = {};
  }
  apply(compiler) {
    var reg = /("([^\"]*(\.)?)*")|('([^\']*(\.)?)*')|(/{2,}.*?(\r|\n))|(/*(\n|.)*?*/)|(/******/)/g;
    compiler.hooks.emit.tap('CodeBeautify', (compilation) => {
      Object.keys(compilation.assets).forEach((data) => {
        console.log(data);
        let content = compilation.assets[data].source(); // 获取处理的文本
        content = content.replace(reg, function (word) { // 去除注释后的文本return /^/{2,}/.test(word) || /^/*!/.test(word) || /^/*{3,}//.test(word) ? "" : word;
        });
        compilation.assets[data] = {
          source() {
            return content;
          },
          size() {
            return content.length;
          }
        }
      });
    });
  }
}
module.exports = MyPlugin;

以上就是编写一个webpack插件需要了解的内容,欢迎一起探讨,谢谢大家!

转载自:https://juejin.cn/post/7039166507330486308
评论
请登录