likes
comments
collection
share

多个webpack plugin 插件带你一块编写,这次一定学会!

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

plugin 的特征

  • 是一个独立的模块。
  • 模块对外暴露一个 js 函数。
  • 函数的原型 (prototype) 上定义了一个注入 compiler 对象的 apply 方法。
  • apply 函数中需要有通过 compiler 对象挂载的 hooks 来进行更改程序

Plugin在整个编译过程中,以"恰当的时机",来进行插件的运行,常用于优化、运行服务、dist文件处理等等。那么问题来了,恰当的时机是什么时机?在接下来的内容中,你将了解,plugin是什么、plugin在打包构建过程中的原理、如何仿写一个plugin。

Compiler 对象 (负责编译)

Compiler 对象包含了当前运行 Webpack 的配置,包括 entry、output、loaders 等配置,这个对象在启动 Webpack 时被实例化,而且是全局唯一的。Plugin 可以通过该对象获取到 Webpack 的配置信息进行处理。

全部compiler-hooks链接

常用的compiler-hooks如下:

  • run (再编辑器开始读取记录前执行)
  • compile (再一个新的compliation创建之前执行)
  • compilation (再一个新的compliation创建后执行)
  • make(compilation 结束之前执行)
  • emit(输出 asset 到 output 目录之前执行)
  • afterEmit(输出 asset 到 output 目录之后执行)
  • done(在 compilation 完成时执行)
  • failed(在 compilation 失败时调用)
  • shutdown(当编译器关闭时调用。)

实现HelloWordPulgin插件,可以看下面代码执行时机:


class HelloWordPulgin {
  constructor(options) {
    console.log(options, 'options....');
  }

  apply(compiler) {
    compiler.hooks.run.tap('myRun', () => {
      console.log('开始编译...run')
    });

    compiler.hooks.compile.tap('myCompile', () => {
      console.log('新的compliation创建...compile')
    });

    compiler.hooks.compilation.tap('myCompilation', () => {
      console.log('compliation创建后...compilation')
    });

    compiler.hooks.make.tap('myMake', () => {
      console.log('compliation结束之前...make')
    });

    compiler.hooks.emit.tap('myEmit', () => {
      console.log('输出之前...emit')
    });

    compiler.hooks.afterEmit.tap('myAfterEmit', () => {
      console.log('输出完成...afterEmit')
    });

    compiler.hooks.done.tap('myDone', () => {
      console.log('compliation完成...done')
    });

    compiler.hooks.failed.tap('myFailed', () => {
      console.log('compliation失败...failed')
    });

    compiler.hooks.shutdown.tap('myShutdown', () => {
      console.log('编译器关闭时...')
    });
  }
}

module.exports = HelloWordPulgin;

webpack应用:

const HelloWordPulgin = require('./pugins/hello-word-pulgin');
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js', // 打包后输出文件的文件名
  },
  plugins: [
    new HelloWordPulgin({ title: 'hello pulgin' }) // HelloWordPulgin
  ]
};

bash 输出信息

npm run build

> plugins@1.0.0 build D:\works\webpack-loader-plugin\plugins
> webpack

{ title: 'hello pulgin' } options....
开始编译...run
新的compliation创建...compile
compliation创建后...compilation
compliation结束之前...make
输出之前...emit
输出完成...afterEmit
compliation完成...done
编译器关闭时...
asset main.bundle.js 1.22 KiB [emitted] (name: main)
./src/index.js 38 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 154 ms

上面输出信息都按照咱们期望的输出了(failed除外,需要可以再代码执行错误去验证)。

Compilation 对象

Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

和 compiler 用法相同,取决于不同的钩子类型, 所以也可以在某些钩子上访问 tapAsynctapPromise

CompilerCompilation 的区别

  • Compiler 代表了整个 Webpack 从启动到关闭的生命周期
  • Compilation 只是代表了一次新的编译,只要文件有改动,compilation 就会被重新创建。

全部Compilation hooks链接

部分Compilation hooks如下:

实现copy-webpack-plugin插件

copy-webpack-plugin就是把部分文件copy到目标文件,咱们选用Compiler hooks done(compliation完成)去实现。

copy-webpack-plugin源码:

const fs = require('fs');

class CopyWebpackPlugin {
  constructor(options) {
    this.formPath = options.form;
    this.toPath = options.to;
    console.log(options, 'options....');
  }

  apply(compiler) {
    const { formPath, toPath } = this;
    const isDir = fs.statSync(formPath).isFile() ? false : true;

    compiler.hooks.done.tap('myDone', () => {
      // fs.cp is not a function 新增于: v16.7.0  
      // node >= 16.7.0 没有大于可以用其他插件实现复制功能
      fs.cp(formPath, toPath, { recursive: isDir }, (err) => { 
        if (err) {
          throw err;
        }
      })
      console.log('copy完成...done')
    });

    compiler.hooks.shutdown.tap('myShutdown', () => {
      console.log('编译器关闭时...')
    });
  }
}

module.exports = CopyWebpackPlugin;

webpack 应用

const path = require('path');
const HelloWordPulgin = require('./pugins/hello-word-pulgin');
const CopyWebpackPlugin = require('./pugins/copy-webpack-plugin');


module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js', // 打包后输出文件的文件名
  },
  plugins: [
    new HelloWordPulgin({ title: 'hello pulgin' }), // 执行时机
    new CopyWebpackPlugin({
      form: path.resolve(__dirname, 'static'),
      to: path.resolve(__dirname, 'dist', 'static'),
    })
  ]
};

bash输出信息:

$ npm run build

> plugins@1.0.0 build
> webpack

{ title: 'hello pulgin' } options....
{
  form: 'D:\\works\\webpack-loader-plugin\\plugins\\static',
  to: 'D:\\works\\webpack-loader-plugin\\plugins\\dist\\static'
} options....
开始编译...run
新的compliation创建...compile
compliation创建后...compilation
compliation结束之前...make
输出之前...emit
输出完成...afterEmit
compliation完成...done
copy完成...done
编译器关闭时...
编译器关闭时...
asset main.bundle.js 1.22 KiB [compared for emit] (name: main)
./src/index.js 38 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 408 ms

这个时候去看dist文件夹发现已经有咱们复制的目录了。

clean-webpack-plugin 实现

clean-webpack-plugin 一般用在输出前用来清空之前目录,咱们选用Compiler hooks emit去完成。

clean-webpack-plugin源码:

const path = require('path');
const shell = require('shelljs');


class CleanWebpackPlugin {
  constructor(options) {
    this.targetPath = options.target;
  }
  apply(compiler) {
    compiler.hooks.emit.tap('myEmit', () => {
      shell.cd(this.targetPath);
      shell.cd('..');
      shell.rm('-rf', './dist'); // 用什么方案做删除都行,这边用到了shelljs
      shell.pwd();
      shell.exec('mkdir dist');
      console.log('输出之前...emit')
    });
  }
}

module.exports = CleanWebpackPlugin;

webpack应用

const path = require('path');
const HelloWordPulgin = require('./pugins/hello-word-pulgin');
const CopyWebpackPlugin = require('./pugins/copy-webpack-plugin');
const CleanWebpackPlugin = require('./pugins/clean-webpack-plugin');


console.log(path.resolve('dist'))

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js', // 打包后输出文件的文件名
  },
  plugins: [
    new HelloWordPulgin({ title: 'hello pulgin' }), // 执行时机
    new CleanWebpackPlugin({
      target: path.resolve(__dirname, 'dist'),
    }),
    new CopyWebpackPlugin({
      form: path.resolve(__dirname, 'static'),
      to: path.resolve(__dirname, 'dist', 'static'),
    })
  ]
};

bash 输出日志

$ npm run build

> plugins@1.0.0 build D:\works\webpack-loader-plugin\plugins
> webpack

D:\works\webpack-loader-plugin\plugins\dist
{ title: 'hello pulgin' } options....
{
  form: 'D:\\works\\webpack-loader-plugin\\plugins\\static',
  to: 'D:\\works\\webpack-loader-plugin\\plugins\\dist\\static'
} options....
开始编译...run
新的compliation创建...compile
compliation创建后...compilation
compliation结束之前...make
输出之前...emit
remove target path success
create dist file success
输出之前...emit
输出完成...afterEmit
compliation完成...done
copy完成...done
编译器关闭时...
编译器关闭时...
asset main.bundle.js 1.22 KiB [emitted] (name: main)
./src/index.js 38 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 554 ms

实现flie-count-webpack-plugin插件

flie-count-webpack-plugin 用于统计文件数量注入到一个markdown里面。可以再输出前同上(Compiler hooks emit)

flie-count-webpack-plugin源码

class FileCountWebpackPlugin {
  constructor(options) {
    this.targetName = `${(options?.targetName || 'file-count')}.md`;
  }

  apply(compiler) {
    compiler.hooks.emit.tap('myEmit', (compilation) => {
      const webpack = compiler.webpack;
      // compilation.assets 获取文件信息
      const compilationAssets = compilation.assets;

      const assetsKeys = Object.keys(compilationAssets);
      const assetsLength = assetsKeys.length;
      let content = `## count files (${assetsLength}) \n`;
      assetsKeys.forEach(item => {
        content += `- ${item} \n`;
      });

      compilation.assets[this.targetName] = {
        source: function () {
          return content
        },
        size: function () {
          return content.length
        },
      };
      console.log('输出之前...emit');
    });
  }
}

module.exports = FileCountWebpackPlugin;

webpack 应用

const path = require('path');
const HelloWordPulgin = require('./pugins/hello-word-pulgin');
const CopyWebpackPlugin = require('./pugins/copy-webpack-plugin');
const CleanWebpackPlugin = require('./pugins/clean-webpack-plugin');
const FileCountWebpackPlugin = require('./pugins/flie-count-webpack-plugin');


console.log(path.resolve('dist'))

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js', // 打包后输出文件的文件名
  },
  plugins: [
    new HelloWordPulgin({ title: 'hello pulgin' }), // 执行时机
    new CleanWebpackPlugin({
      target: path.resolve(__dirname, 'dist'),
    }),
    new FileCountWebpackPlugin(),
    new CopyWebpackPlugin({
      form: path.resolve(__dirname, 'static'),
      to: path.resolve(__dirname, 'dist', 'static'),
    })
  ]
};

bash输出日志

$ npm run build

> plugins@1.0.0 build D:\works\webpack-loader-plugin\plugins
> webpack

D:\works\webpack-loader-plugin\plugins\dist
{ title: 'hello pulgin' } options....
{
  form: 'D:\\works\\webpack-loader-plugin\\plugins\\static',
  to: 'D:\\works\\webpack-loader-plugin\\plugins\\dist\\static'
} options....
开始编译...run
新的compliation创建...compile
compliation创建后...compilation
compliation结束之前...make
输出之前...emit
remove target path success
create dist file success
输出之前...emit
输出之前...emit
(node:49132) [DEP_WEBPACK_COMPILATION_ASSETS] DeprecationWarning: Compilation.assets will be frozen in future, all modifications are deprecated.
BREAKING CHANGE: No more changes should happen to Compilation.assets after sealing the Compilation.
        Do changes to assets earlier, e. g. in Compilation.hooks.processAssets.
        Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_*.
(Use `node --trace-deprecation ...` to show where the warning was created)
输出完成...afterEmit
compliation完成...done
copy完成...done
编译器关闭时...
编译器关闭时...
asset main.bundle.js 1.22 KiB [emitted] (name: main)
asset file-count.md 38 bytes [emitted]
./src/index.js 38 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 408 ms

DeprecationWarning: Compilation.assets will be frozen in future, all modifications are deprecated.(DeprecationWarning:编译。未来资产将被冻结,所有修改不赞成) 期望值达到了,给我生成file-count.md文件了。但是有警告信息,这个时候百度发现都是npm i -D html-webpack-plugin@next 安装最新版本的,应该是一个问题新版本html-webpack-plugin都解决了,我来借鉴下。

参考html-webpack-pluginv5源码更改后:

class FileCountWebpackPlugin {
  constructor(options) {
    this.targetName = `${(options?.targetName || 'file-count')}.md`;
  }

  apply(compiler) {
    compiler.hooks.thisCompilation.tap('myEmit', (compilation) => {
      compilation.hooks.processAssets.tapAsync({
        name: 'process',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
      }, (assets, callback) => {

        let count = 0;
        let content = '';
        for (const asset in assets) {
          count++;
          content += `- ${asset} \n`;
        }
        const title = `## count files (${count}) \n`;
        compilation.emitAsset(this.targetName, new webpack.sources.RawSource(title + content, false));
        callback();
        callback();
      })
      console.log('输出之前...thisCompilation');
    });
  }
}

module.exports = FileCountWebpackPlugin;

更改后bash输入日志:

$ npm run build

> plugins@1.0.0 build D:\works\webpack-loader-plugin\plugins
> webpack

D:\works\webpack-loader-plugin\plugins\dist
{ title: 'hello pulgin' } options....
{
  form: 'D:\\works\\webpack-loader-plugin\\plugins\\static',
  to: 'D:\\works\\webpack-loader-plugin\\plugins\\dist\\static'
} options....
开始编译...run
新的compliation创建...compile
输出之前...emit
compliation创建后...compilation
compliation结束之前...make
输出之前...emit
remove target path success
create dist file success
输出之前...emit
输出完成...afterEmit
compliation完成...done
copy完成...done
编译器关闭时...
编译器关闭时...
asset main.bundle.js 1.22 KiB [emitted] (name: main)
asset file-count.md 38 bytes [emitted]
./src/index.js 38 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 386 ms

实现banner-webpack-plugin插件

banner-webpack-plugin咱们就开发基础功能再静态资源添加信息, 跟上面实现方法大概一致。

简易banner-webpack-plugin源码:

class BannerWebpackPlugin {
  constructor(options = { banner() { } }) {
    this.prefixBanner = options?.banner() || '';
  }

  apply(compiler) {
    compiler.hooks.thisCompilation.tap('myThisCompilation', (compilation) => {
      const webpack = compiler.webpack;

      compilation.hooks.processAssets.tapAsync({
        name: 'process',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
      }, (assets, callback) => {
        for (const asset in assets) {
          if (asset.endsWith('.js')) {
            compilation.updateAsset(asset, source => {
              const sourceContent = source.source();
              const newContent = this.prefixBanner + sourceContent;
              return new webpack.sources.RawSource(newContent, false)
            });
          }
        }
        callback();
      })
    })
  }
}
module.exports = BannerWebpackPlugin;

webpack应用:

const path = require('path');
const HelloWordPulgin = require('./plugins/hello-word-pulgin');
const CopyWebpackPlugin = require('./plugins/copy-webpack-plugin');
const CleanWebpackPlugin = require('./plugins/clean-webpack-plugin');
const FileCountWebpackPlugin = require('./plugins/flie-count-webpack-plugin');
const BannerWebpackPlugin = require('./plugins/banner-webpack-plugin');

console.log(path.resolve('dist'))

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js', // 打包后输出文件的文件名
  },
  plugins: [
    new HelloWordPulgin({ title: 'hello pulgin' }), // 执行时机
    new CleanWebpackPlugin({
      target: path.resolve(__dirname, 'dist'),
    }),
    new BannerWebpackPlugin({
      banner() {
        return `/**
        * 这是是XXX系统代码
        * 我们是XXX团队
        * 打包时间${new Date().toLocaleString()}
        */`
      }
    }),
    new FileCountWebpackPlugin(),
    new CopyWebpackPlugin({
      form: path.resolve(__dirname, 'static'),
      to: path.resolve(__dirname, 'dist', 'static'),
    }),
  ]
};
...
输出正常,达到期望效果

remove-console-webpack-plugin删除console插件开发

remove-console-webpack-plugin 去掉生产环境无用的console

remove-console-webpack-plugin源码

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

  apply(compiler) {
    compiler.hooks.thisCompilation.tap('myThisCompilation', (compilation) => {
      const webpack = compiler.webpack;
      // 生产环境删除注释
      if (process.env.NODE_ENV !== 'production') return;

      compilation.hooks.processAssets.tapAsync({
        name: 'process',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
      }, (assets, callback) => {
        for (const asset in assets) {
          if (/(.js|.ts|.jsx|.ts|.vue)$/.test(asset)) {
            compilation.updateAsset(asset, source => {
              const sourceContent = source.source();
              const newSource = sourceContent.replace(/console.log\(.*\);?/g, '');
              return new webpack.sources.RawSource(newSource, false)
            });
          }
        }

        callback();
      })
    })
  }
}

module.exports = RemoveConsoleWebpackPlugin;

webpack应用:

...
new RemoveConsoleWebpackPlugin(),
...
转载自:https://juejin.cn/post/7231896581477007419
评论
请登录