Webpack 自定义插件学习笔记

130 阅读5分钟

引子

Webpack 是一个强大的模块打包工具,它可以处理各种类型的资源,并将它们转换为浏览器可以理解的代码。在这个过程中,Webpack 使用了一种叫做 "插件" 的机制,它允许你自定义如何处理特定类型的资源。

在这篇文章中,我们将深入探讨如何创建和使用自定义的 webpack 插件。我们将通过一系列的例子,展示如何使用插件来实现各种常见的任务,如移除注释、添加版权声明、转换 Markdown 到 HTML 等等。这些例子将帮助你理解插件的工作原理,并教你如何创建自己的插件来满足特定的需求。

Lets Go!

image.png

几个最常用的钩子

  • entryOption: 在 webpack 选项中的 entry 配置项被处理过后,立即执行。这个钩子可以用来修改、添加或删除 entry 配置。

  • compilation: 每次新的编译创建时,都会触发此钩子。编译对象包含了当前的模块资源、编译生成资源、变化的文件等。这个钩子可以用来在编译过程中添加自定义的功能。

  • emit: 在生成资源并输出到目录之前。这个钩子可以用来修改输出资源,或者在输出资源到目录之前执行一些操作。

  • afterEmit: 在生成资源并输出到目录之后。这个钩子可以用来执行一些清理或者其他的收尾工作。

  • done: 编译完成时。这个钩子可以用来报告编译结果,或者执行一些编译完成后的操作。

  • failed: 编译失败时。这个钩子可以用来处理编译错误,例如记录错误日志。

image.png

插件实例

@构建开始之前

ConsoleLogOnBuildWebpackPlugin

这个插件在每次构建开始时在控制台打印一条消息。

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap('ConsoleLogOnBuildWebpackPlugin'compilation => {
      console.log('The webpack build process is starting!!!');
    });
  }
}

@单个模块编译时

ReplaceTextPlugin

这个插件会替换源代码中的特定文本,你可以通过参数指定要替换的文本和替换内容。

class ReplaceTextPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('ReplaceTextPlugin'(compilation) => {
      compilation.hooks.optimizeChunkAssets.tap('ReplaceTextPlugin'(chunks) => {
        chunks.forEach((chunk) => {
          chunk.files.forEach((file) => {
            const source = compilation.assets[file].source();
            const newSource = source.replace(this.options.searchthis.options.replace);
            compilation.assets[file].source = () => newSource;
          });
        });
      });
    });
  }

}

使用方法:

const ReplaceTextPlugin = require('./ReplaceTextPlugin');

module.exports = {
  // ...
  plugins: [
    new ReplaceTextPlugin({
      search/Hello/g,
      replace'Hi',
    }),
  ],

};

@资源输出之前

FileListPlugin

这个插件在每次构建完成后,会在输出目录中生成一个 filelist.md 文件,其中列出了所有的输出文件。

const fs = require('fs');
const path = require('path');

class FileListPlugin {

  apply(compiler) {
  
    // emit 是异步 hook,使用 tapAsync 触发它
    compiler.hooks.emit.tapAsync('FileListPlugin'(compilation, callback) => {
      let filelist = 'In this build:\n\n';

      // 遍历所有编译过的资源文件,
      // 对于每个文件名称,都添加一行内容。
      for (let filename in compilation.assets) {
        filelist += '- ' + filename + '\n';
      }

      // 将这个列表作为一个新的文件资源,插入到 webpack 构建中:
      compilation.assets['filelist.md'] = {
        sourcefunction() {
          return filelist;
        },
        sizefunction() {
          return filelist.length;
        },
      };

      callback();
    });
  }
}

module.exports = FileListPlugin;

使用方法:

const FileListPlugin = require('./FileListPlugin');
module.exports = {
  // ...
  plugins: [
    new FileListPlugin(),
  ],
};

CopyWebpackPlugin

这个插件用于将单个文件或整个目录复制到构建目录。

const fs = require('fs-extra');
const path = require('path');

class CopyWebpackPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyWebpackPlugin'(compilation, callback) => {
      this.options.forEach(option => {
        fs.copySync(option.from, path.join(compiler.options.output.path, option.to));
      });
      callback();
    });
  }
  
}

module.exports = CopyWebpackPlugin;

使用方法:

const CopyWebpackPlugin = require('./CopyWebpackPlugin');
module.exports = {
  // ...
  plugins: [
    new CopyWebpackPlugin([
      { from'src/assets'to'assets' },
    ]),
  ],
};

@资源输出以后

ZipWebpackPlugin

这个插件用于将输出文件打包为一个 zip 文件。

const fs = require('fs');
const path = require('path');
const archiver = require('archiver');

class ZipWebpackPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.afterEmit.tapAsync('ZipWebpackPlugin'(compilation, callback) => {
      const outputPath = compiler.options.output.path;
      const output = fs.createWriteStream(path.join(outputPath, this.options.filename));

      const archive = archiver('zip', {
        zlib: { level9 }, // 设置压缩级别。
      });

      output.on('close'() => {
        callback();
      });

      archive.pipe(output);
      archive.directory(outputPath, false);
      archive.finalize();
    });
  }

}

module.exports = ZipWebpackPlugin;

使用方法:

const ZipWebpackPlugin = require('./ZipWebpackPlugin');

module.exports = {
  // ...
  plugins: [
    new ZipWebpackPlugin({
      filename'output.zip',
    }),
  ],
};

AssetVersioningPlugin

这个插件会为所有的输出资源添加版本号,以解决缓存问题。

class AssetVersioningPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('AssetVersioningPlugin'(compilation) => {
      const version = new Date().getTime();
      for (let filename in compilation.assets) {
        compilation.assets[`${filename}?v=${version}`] = compilation.assets[filename];
        delete compilation.assets[filename];
      }
    });
  }
}

@打包完成后

LogOnFinishPlugin

这是一个简单的插件,它在每次构建完成后在控制台打印 "Build Finished!"。

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
        'LogOnFinishPlugin'(stats /* 在处理完所有模块后触发 */) => {
          console.log('Build Finished!');
        });
  }
}

module.exports = HelloWorldPlugin;

FriendlyErrorsWebpackPlugin

这个插件会改善 webpack 的错误信息和日志,使其更友好。

class FriendlyErrorsWebpackPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('FriendlyErrorsWebpackPlugin'(stats) => {
      const errors = stats.compilation.errors;
      
      if (errors && errors.length) {
        console.log('\n\n');
        console.log('Oops, something went wrong :(');
        console.log('===========================');
        errors.forEach((error) => {
          console.log(error.message || error);
          console.log('\n');
        });
      } else {
        console.log('\n\n');
        console.log('Build completed successfully!');
        console.log('=============================');
      }
    });
  }

}

PerformanceBudgetPlugin

这个插件会检查你的应用是否超出了预设的性能预算。

class PerformanceBudgetPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.done.tap('PerformanceBudgetPlugin'(stats) => {
      const assets = stats.toJson().assets;

      assets.forEach((asset) => {
        if (asset.size > this.options.maxSize) {
          console.warn(`Asset ${asset.name} is over performance budget of ${this.options.maxSize} bytes.`);
        }
      });
    });
  }

}

小结

自定义 Webpack 插件是一个强大的工具,它可以让你在构建过程中执行自定义的操作,以满足特定的需求。以下是关于自定义 Webpack 插件的一些关键点:

  • 生命周期钩子:Webpack 提供了一系列的生命周期钩子,你可以在插件中使用这些钩子来执行自定义的操作。这些钩子包括 entryOption、afterPlugins、compilation、emit、afterEmit、done、failed、invalid 和 watchRun 等。

  • 插件结构:一个 Webpack 插件通常是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 Webpack 编译器调用,并可以在其中访问到编译器实例。

  • 访问编译资源:在插件中,你可以通过编译器实例访问到所有的编译资源,包括模块资源、编译生成资源、变化的文件等。

  • 异步处理:Webpack 插件支持异步处理,你可以在插件中执行异步操作,例如读写文件、网络请求等。

  • 参数化插件:你可以通过插件的构造函数接收参数,以创建参数化的插件。这使得插件可以更灵活地适应不同的需求。

  • 错误处理:在插件中,你应该正确处理可能出现的错误,并通过编译器实例报告这些错误。

自定义 Webpack 插件是一个非常强大的工具,它可以让你深入到构建过程的每一个环节,执行自定义的操作,以满足你的特定需求。理解并掌握如何创建和使用自定义插件,将极大地提升你的 Webpack 使用效率!


镇场名画:拯救世界的任务就交给你了好吗~

image.png