FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

[译]如何写一个webpack plugin? #264

Open FrankKai opened 2 years ago

FrankKai commented 2 years ago

原文: https://webpack.js.org/contribute/writing-a-plugin/ demo: https://github.com/FrankKai/webpack-plugin-demo

什么是插件

插件是webpack引擎开放给第三方开发者的一种潜力机制。使用分阶段构建callback,开发者可以引进自己的行为到webpack构建进程。比起构建loader,构建插件更加高级一些,因为你需要理解一些webpack底层内部的hook。做好看源码的准备!

新建一个插件

一个webpack插件,由以下这些内容组成:

class MyExampleWebpackPlugin {
    apply(compiler) {
        compiler.hooks.emit.tapAsync(
        'MyExampleWebpackPlugin',
        (compilation, callback) => {
            console.log('This is an example plugin!');
            console.log(Here’s the `compilation` object which represents a single build of assets:', compilation);

            compilation.addModule(/* ... */);

            callback();
        }
        )
    }
}

插件的基础架构

插件是原型链上有apply方法的实例化对象。这个apply方法会在webpack编译器安装插件时,调用一遍。 apply方法暴露出了webpack底层编译器的引用,可以赋权给编译器callback。

一个插件的结构如下:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
      'Hello World Plugin',
      (
        stats /* stats is passed as an argument when done hook is tapped.  */
      ) => {
        console.log('Hello World!');
      }
    );
  }
}

module.exports = HelloWorldPlugin;

为了使用这个插件,需要在你的webpack插件配置的plugins增加一个实例。

// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... configuration settings here ...
  plugins: [new HelloWorldPlugin({ options: true })],
};

可以通过schema-utils约束webpack插件配置的传参:

import { validate } from 'schema-utils';

// schema for options object
const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string',
    },
  },
};

export default class HelloWorldPlugin {
  constructor(options = {}) {
    validate(schema, options, {
      name: 'Hello World Plugin',
      baseDataPath: 'options',
    });
  }

  apply(compiler) {}
}

compiler和compilation

开发插件最重要的2个对象:compiler对象,compilation对象。

理解它们是扩展webpack引擎的最重要的一步。

class HelloCompilationPlugin {
  apply(compiler) {
    // Tap into compilation hook which gives compilation as argument to the callback function
    compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
      // Now we can tap into various hooks available through compilation
      compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
        console.log('Assets are being optimized.');
      });
    });
  }
}

module.exports = HelloCompilationPlugin;

更多compiler和compilation的hook,可以看https://webpack.js.org/api/plugins/

异步事件hook

有一些插件hook是异步的。我们可以使用tabAsync或者tabPromise去同步的使用tap。

tabAsync

当我们使用tabAsync方法去接进插件时,我们需要调用回调函数抛出来的callback函数。

class HelloAsyncPlugin {
    apply(compiler) {
        compiler.hooks.emit.tabAsync('HelloAsyncPlugin', (compilation, callback) => {
           setTimeout(function () {
               console.log('Done with async work...');
               callback();
            }, 1000);
        })
    }
}

module.exports = HelloAsyncPlugin;

tabPromise

使用tabPromise也可以处理异步tap,需要返回一个promise。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
      // return a Promise that resolves when we are done...
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log('Done with async work...');
          resolve();
        }, 1000);
      });
    });
  }
}

module.exports = HelloAsyncPlugin;

示例

了解了webpack compiler以及每个独立的编译步骤后,我们可以为引擎做的事,是无穷大的。我们可以重新格式化已经存在的文件,创建派生文件,或者生产全新的资源。

我们现在创建一个生成assets.md的文件,生成我们每次构建产生的静态文件。

class FileListPlugin {
  static defaultOptions = {
    outputFile: "assets.md",
  };

  // 所有的options都要传到插件的constructor
  // (这是你的插件的公共API部分)

  constructor(options = {}) {
    // 用户定义的options覆盖默认options
    // 与默认options合并,从而给插件的method使用
    // 你需要在这里验证options
    this.options = { ...FileListPlugin.defaultOptions, ...options };
  }

  apply(compiler) {
    const pluginName = FileListPlugin.name;

    // webpack模块实例,可以从compiler对象上解构下来
    // 这可以确保模块的正确部分被使用
    // (不要通过require / import或者其他方式引入webpack)
    const { webpack } = compiler;

    // compilation对象可以给我们一些常用常量的引用
    const { Compilation } = webpack;

    // RawSource用来在编译时代表资源class
    const { RawSource } = webpack.sources;

    // 使用thisCompilation hook使得构建处于较早的阶段
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      // 在指定阶段进入到静态资源的流水线
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,
          // 靠后一点的阶段,确保所有资源能被加入到插件的编译中
          stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // assets是包含所有静态资源的对象
          // 在编译阶段,对象的键是assets的路径,对象的值是assets的文件内容

          // 遍历所有静态文件,生成markdown文件
          const content =
            "# In this build:\n\n" +
            Object.keys(assets)
              .map((filename) => `- ${filename}`)
              .join("\n");
          // 把资源添加给compilation,从而自动由webpack生成到目标目录
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(content)
          );
        }
      );
    });
  }
}

module.exports = { FileListPlugin };