80行代码写一个Webpack插件并发布到npm

 

1. 前言

最近在学习 Webpack 相关的原理,以前只知道 Webpack 的配置方法,但并不知道其内部流程,经过一轮的学习,感觉获益良多,为了巩固学习的内容,我决定尝试自己动手写一个插件。

这个插件实现的功能比较简单:

  • 默认清除 js 代码中的 console.log 的打印输出;
  • 可通过传入配置,实现移除 console 的其它方法,如 console.warn、console.error 等;

 

2. Webpack 的构建流程以及 plugin 的原理

2.1 Webpack 构建流程

Webpack 的主要构建流程,可以分为三个阶段:

  • 初始化阶段:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
  • 编译阶段:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  • 生成阶段:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

如果 Webpack 打包生产环境文件时,只会执行一次构建,以上阶段会按顺序执行一遍。但是在开启监听模式时,如开发环境,Webpack 会持续的进行构建。

2.2 plugin 原理

Webpack 插件通常是一个带有 apply 函数的类,其中 constructor 可以接收传入的配置项。插件被安装时,apply 函数会被调用一次,并接收 Compiler 对象,然后我们可以在 Compiler 对象上监听不同的事件钩子,从而进行插件功能的开发。

// 定义一个插件
class MyPlugin {
// 构造函数,接收插件的配置项 options 
constructor(options) {
  // 获取配置项,初始化插件
}

// 插件安装时会调用 apply,并传入 compiler
apply(compiler) {
  // 获取 comolier 独享,可以监听事件钩子
  // 功能开发 ... 
}
}

2.3 compiler 和 compilation 对象

在开发 Plugin 过程中最常用的两个对象就是 Compiler 和 Compilation:

  • Compiler 对象在 Webpack 启动时被实例化,该对象包含了 Webpack 环境所有的配置信息,包括 options、loaders、plugins 等。在整个 Webpack 构建过程中,Compiler 对象是全局唯一的, 它提供了很多事件钩子回调供插件使用。
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象在 Webpack 构建过程中并不是唯一的,如果在开发模式下 Webpack 开启了文件检测功能,每当文件变化时,Webpack 会重新构建,此时会生成一个新的 Compilation 对象。Compilation 对象也提供了很多事件回调供插件做扩展。

 

3. 插件开发

3.1 项目目录

该插件实现的功能比较简单,文件目录也不复杂。首先新建一个空文件夹 remove-console-Webpack-plugin,并在该文件夹目录下运行 npm init,根据提示来填写 package.json 相关信息。然后再新建一个 src 文件夹,插件主要代码就放在 src/index.js 里面。如果你需要把项目放到 github 上,最好也添加一下 .gitignore、README.md 等文件。

// remove-console-Webpack-plugin
├─src
│  └─index.js  
├─.gitignore
├─package.json
└─README.md 

3.2 插件代码

插件代码逻辑也并不复杂,主要有几点:

  • 在构造函数中接收配置参数,并对参数进行合并,得到需要清除的 console 函数, 存放在 removed 数组中;
  • 在 apply 函数中监听 compiler.hook.compilation 钩子,该钩子触发后,拿到 compilation 后进一步监听它的钩子,这里 Webpack4 和 Webpack5 的钩子不一样,需要做兼容;
  • 定义 assetsHandler 方法来处理 js 文件,利用正则表达式清除 removed 中包括的 console 函数;
class RemoveConsoleWebpackPlugin {
// 构造函数接受配置参数
constructor(options) {
  let include = options && options.include;
  let removed = ['log']; // 默认清除的方法

  if (include) {
    if (!Array.isArray(include)) {
      console.error('options.include must be an Array.');
    } else if (include.includes('*')) {
      // 传入 * 表示清除所有 console 的方法
      removed = Object.keys(console).filter(fn => {
        return typeof console[fn] === 'function';
      })
    } else {
      removed = include; // 根据传入配置覆盖
    }
  }

  this.removed = removed;
}

// Webpack 会调用插件实例的 apply 方法,并传入compiler 对象
apply(compiler) {
  // js 资源代码处理函数
  let assetsHandler = (assets, compilation) => {
    let removedStr = this.removed.reduce((a, b) => (a + '|' + b));

    let reDict = {
      1: [RegExp(`\\.console\\.(${removedStr})\\(\\)`, 'g'), ''],
      2: [RegExp(`\\.console\\.(${removedStr})\\(`, 'g'), ';('],
      3: [RegExp(`console\\.(${removedStr})\\(\\)`, 'g'), ''],
      4: [RegExp(`console\\.(${removedStr})\\(`, 'g'), '(']
    }

    Object.entries(assets).forEach(([filename, source]) => {
      // 匹配js文件
      if (/\.js$/.test(filename)) {
        // 处理前文件内容
        let outputContent = source.source();

        Object.keys(reDict).forEach(i => {
          let [re, s] = reDict[i];
          outputContent = outputContent.replace(re, s);
        })

        compilation.assets[filename] = {
          // 返回文件内容
          source: () => {
            return outputContent
          },
          // 返回文件大小
          size: () => {
            return Buffer.byteLength(outputContent, 'utf8')
          }
        }
      }
    })
  }

  /**
   * 通过 compiler.hooks.compilation.tap 监听事件
   * 在回调方法中获取到 compilation 对象
   */
  compiler.hooks.compilation.tap('RemoveConsoleWebpackPlugin',
    compilation => {
      // Webpack 5
      if (compilation.hooks.processAssets) {
        compilation.hooks.processAssets.tap(
          { name: 'RemoveConsoleWebpackPlugin' },
          assets => assetsHandler(assets, compilation)
        );
      } else if (compilation.hooks.optimizeAssets) {
        // Webpack 4
        compilation.hooks.optimizeAssets.tap(
          'RemoveConsoleWebpackPlugin', 
          assets => assetsHandler(assets, compilation)
        );
      }
    })
}
}

// export Plugin
module.exports = RemoveConsoleWebpackPlugin;

 

4. 发布到npm

希望别人能使用到你的插件,就需要把插件发布到 npm 上,发布的主要流程:

首先在 npm 官网上注册账号,然后打开命令行工具,在任意目录下输入 npm login 并按提示登录;

登录后可用 npm whoami 查看是否登录成功;

发布前检查一下根目录下的 package.json 文件信息是否填写正确,主要字段:

  • name:决定用户下载你的插件时用的名称,不可与 npm 上已有的第三方包重名,否则无法发布;
  • main:插件主文件入口,Webpack 引入插件时,就从该目录导入;
  • version:每次更新发布时,需要与上一版本的版本号不一样,否则上传不成功;
  • repository:如果你的插件代码放在 github、gitee 等网站,可以填一下;
  • private:不能设置为 true,否则无法发布;

一切准备就绪后,切换到插件所在的目录下,运行 npm publish 即可上传插件;

上传成功后,到 npm 官网上搜索,看看是否能搜到插件;

 

5. 结尾

关于80行代码写一个Webpack插件并发布到npm的文章就介绍至此,更多相关Webpack插件发布到npm内容请搜索编程宝库以前的文章,希望以后支持编程宝库

 流畅动画的标准首先,理清一些概念。FPS 表示的是每秒钟画面更新次数。我们平时所看到的连续画面都是由一幅幅静止画面组成的,每幅画面称为一帧,FPS 是描述“帧”变化速度的物理量。理论上说,FPS 越 ...