《Webpack5 核心原理与应用实践》学习笔记-> 提升插件健壮性

289 阅读4分钟

其实从loader开始,个人已经没有多少想法了,自己基本上用不到loaderplugin的开发, 目前的情况也不需要开发或者是调试这些玩意,而且课程里面干货嗯,所以差点弃坑,但是反正就剩几章了,咬咬牙吧,这是写给自己的,后面把一个技术栈弄得七七八八了再来写专栏。

插件的健壮性其实和自己写的项目类似,要排查问题,需要记录日志,分析性能瓶颈等,这些webpack都替你考虑过了, 所以会有一些工具来帮助你做这些事。

日志处理

在之前的loader中写过,可以通过this.getLogger()来使用日志功能,它是内置了infrastructureLogging,还不了解可以翻翻之前写的或者去webpack官网搜索一下这个。

loader中使用是通过注入的上下文this来获取log,在plugin中是通过apply中传入的compiler或者compilation来使用的,如下:

class MyPlugin {
    apply(compiler) {
        // 通过compiler.getInfrastructureLogger获取一个logger
        const logger = compiler.getInfrastructureLogger('MyPlugin');
        logger.info('MyPlugin is starting');

        compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {

            // 通过compilation.getLogger获取一个logger
            const logger = compilation.getLogger('MyPlugin');
            logger.info('Hello from MyPlugin');

            callback();
        })
    }
}

module.exports = MyPlugin;

log的日志分级就不用讲了吧,接下来就是通过log上报异常,通过log上报的异常不会中断webpack的构建:

class MyPlugin {
    apply(compiler) {
        compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {

            // 通过compilation.getLogger获取一个logger
            const logger = compilation.getLogger('MyPlugin');

            // log的分级
            logger.info('Hello from MyPlugin');
            logger.log('Log from MyPlugin');
            logger.debug('Debug from MyPlugin');
            logger.warn('Warning from MyPlugin');
            logger.error('Error from MyPlugin');

            // 可以通过 logger.warn() logger.error() 等方法输出异常日志,这不会影响webpack的编译结果
            logger.warn('Warning');
            logger.error('Error');

            // 还可以通过compilation.errors和compilation.warnings添加错误和警告
            compilation.errors.push(new Error("Emit Error From FooPlugin"));
            compilation.warnings.push("Emit Warning From FooPlugin");

            callback();
        })
    }
}

module.exports = MyPlugin;

这里可以通过loggerwarnerror方法输出异常日志,也可以通过compilationwarningserrors添加错误警告,他们都不会中断webpack的构建。

他们的区别在于,使用log仅仅是出入错误信息到控制台,而使用compilation.warningscompilation.errors还会将错误信息汇总到 stats 统计对象:

image.png

处理错误信息

上面讲到通过log来记录编译过程中的一些信息,包括错误信息,但是这个错误信息并不会影响构建,其实除了通过log来处理异常还有其他的方式来处理异常。

  1. 直接抛出异常
class MyPlugin {
    apply(compiler) {
        compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
            if (Math.random() > 0.5) {
                // 直接抛出异常会中断webpack的构建
                throw new Error('MyPlugin error');
            }

            callback();
        })
    }
}

module.exports = MyPlugin;
  1. 向下透传错误信息
class MyPlugin {
    apply(compiler) {
        compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
            let error = undefined;
            if (Math.random() > 0.5) {
                error = new Error('MyPlugin error');
            }

            // callback的第一个参数是错误信息,错误信息会被webpack捕获,如果有错误信息,webpack会停止打包
            callback(error);
        })
    }
}

module.exports = MyPlugin;

上报统计信息

有时候处理一些文件会特别耗时,用户可能需要对此进行一些优化,这个时候就需要插件的开发者提供统计信息了,上报统计信息有两种方式:

  • 使用 ProgressPlugin 插件的 reportProgress 接口上报执行进度;
    • 简单用法:npx webpack --progress

      这种方式最终输出的进度和真实的构建进度差别很大(虽然进度条都是假的),因为有些插件可能没有提交任何构建进度相关的信息。

    • 使用webpack提供的ProgressPlugin插件

      • 首先在配置文件中配置一下
      const MyPlugin = require('./src/plugin/index')
      const {ProgressPlugin} = require('webpack')
      
      module.exports = {
          // 省略其他配置
          plugins: [
              new MyPlugin(),
              new ProgressPlugin({
                  activeModules: false,
                  entries: true
              })
          ]
      }
      
      • 然后在插件中编写上报进度的代码
      const {ProgressPlugin} = require('webpack');
      const wait = (misec) => new Promise((r) => setTimeout(r, misec));
      
      class MyPlugin {
          apply(compiler) {
              compiler.hooks.emit.tapAsync('MyPlugin', async (compilation, callback) => {
                  // 获取 Reporter ,这个有可能为 null,我这里没有写兜底的空函数
                  const reportProgress = ProgressPlugin.getReporter(compiler);
                  for (let i = 0; i < 100; i++) {
                      await wait(100);
                      reportProgress(i / 100, `Our plugin is working ${i}%`);
                  }
                  callback()
              })
          }
      }
      
      module.exports = MyPlugin;
      
    • 使用 stats 接口汇总插件运行的统计数据。

      • 编写代码
      class MyPlugin {
          apply(compiler) {
              compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
                  const statsMap = new Map();
                  // buildModule 钩子将在开始处理模块时触发
                  compilation.hooks.buildModule.tap('MyPlugin', (module) => {
                      const ident = module.identifier();
                      const startTime = Date.now();
      
                      // 模拟5秒钟的构建时间
                      while (new Date().getTime() - startTime < 5000);
      
                      const endTime = Date.now();
                      // 记录处理耗时
                      statsMap.set(ident, endTime - startTime);
                  });
      
                  compilation.hooks.statsFactory.tap('MyPlugin', (factory) => {
                      factory.hooks.result
                          .for("module")
                          .tap('MyPlugin', (module, context) => {
                              const { identifier } = module;
                              const duration = statsMap.get(identifier);
                              // 添加统计信息
                              module.fooDuration = duration || 0;
                          });
      
                  });
              })
          }
      }
      
      module.exports = MyPlugin;
      
      • 输出结果:npx webpack --profile --json=compilation-stats.json

      这个命令会在根目录创建一个compilation-stats.json的文件,可以找到module.fooDuration就是构建时间,也是上面自己记录的信息,不出意外应该是5000

总结

插件其实就是一个工程项目,提高项目的健壮性是很有必要的一件事,上面的对于插件的开发没有任何帮助,但是对一个软件系统质量提升有很大的帮助,这有益于问题的排查,软件的运行状态,性能等有一个可靠的输出结果来查看,让软件更加可靠。

课程中还讲到了参数校验、测试环境搭建、编写测试用例,但是这些在之前我写loader的时候都有讲过,这里我也就不做记录了。