Webpack项目优化之删除僵尸文件

前言

项目多版本并行开发,版本合并解决冲突时,可能会出现很多重复轮子、无用代码遗留在项目里;开发过程中需求被砍掉,废弃文件保留在了项目里,成为多年积存的僵尸文件,比如图片、css、js文件。

当开发者搜索代码时,搜出来一大堆代码重复相似的文件,还得一个个排查某文件是否是僵尸文件,僵尸文件成为开发者的阻力,增加了开发者的工作量。

问题来了,我们如何分析并删除项目里的僵尸文件呢?

解决思路

因为项目是基于Webpack5构建的,我们可以编写webpack插件,借助Webpack之手帮我们分析出项目构建依赖到的所有文件。

首先介绍下,webpack插件是webpack生态系统的重要组成部分,插件能够hook到在每个compilation中触发的所有关键事件,在编译的每一步,插件都具备完全访问compiler对象的能力。compiler对象向开发者暴露了许许多多的生命周期钩子函数,可以通过compiler.hooks.someHook.tap(...)访问。

本文插件的执行流程

插件在webpack.config.js的使用示例:

module.exports = {
  mode: "production",
  entry: "./src/index.js",
  // ...
  plugins: [
    new SxfDeadfilePlugin({
      include: ["src/components/**/*.(js|ts|vue)", "src/style/**/*"],
      exclude: ["node_modules/**/*"],
    }),
  ],
};
复制代码

直接上代码:

export default class SxfDeadfilePlugin {
  constructor(options) {
    this.options = {
      delete: !!options.delete,
      include: options.include ?? ['src/**/*'],
      exclude: options.exclude ?? ['node_modules/**/*'],
      globOptions: options.globOptions,
    };
  }

  apply(compiler) {
    if (compiler.hooks) {
      compiler.hooks.afterEmit.tapAsync('SxfDeadfilePlugin', this.onAfterEmit.bind(this, this.options));
    }
  }

  onAfterEmit(options, compilation, doneFn) {
    // applyAfterEmit为接下来要实现的处理逻辑:分析删除僵尸文件
    applyAfterEmit(options, compilation);
    doneFn();
  }
}
复制代码
  1. 首先是初始化好options配置,
  2. 然后插件的apply方法被调用,传入compiler对象,
  3. afterEmit钩子会在Webpack构建生成资源到output目录之后执行,tapAsync表示以异步的方式执行afterEmit钩子,调用doneFn表示异步执行完成。

实现的核心逻辑

function applyAfterEmit(options, compilation) {
  // 获取webpack构建所需依赖文件,记为a1集合
  const usedFileDeps = getFileDepsMap(compilation);
  // 扫描获取指定路径的所有文件,记为a2集合
  const includeFiles = getIncludeFiles(options);

  // a2与a1对比,属于a2且不属于a1的文件就是僵尸文件
  const deadFiles = includeFiles.filter(file => !usedFileDeps.has(file));

  if (options.delete) {
    // 遍历,通过fs.unlink删除即可
    removeFiles(deadFiles);
  }
复制代码

compilation.fileDependencies存放了Webpack构建分析出来的所有文件依赖,通过它我们就可以获取到文件依赖。

function getFileDepsMap(compilation) {

  const resMap = Array.from(compilation.fileDependencies)
    .reduce((total, usedFilePath) => {
      total.set(usedFilePath, true);
      return total;
    }, new Map());

  return resMap;
}
复制代码

借助第三方库fast-glob,我们可以很容易地根据options配置扫描出我们的目标文件

function getIncludeFiles(options) {
  const { include, exclude } = options;
  // !感叹号表示排除
  const fileList = include.concat(exclude.map(item => `!${item}`));
  return glob.sync(fileList, options.globOptions)
    .map(filePath => path.resolve(process.cwd(), filePath));
}
复制代码

最后将两者进行对比,就能获取到僵尸文件了。

缺点

这个解决思路存在两个缺点,第一个缺点就是插件依赖了compilation.fileDependencies去实现,假如项目中有引用其它插件,而其它插件又对compilation.fileDependencies进行了操作,这会导致僵尸文件扫描出来的结果与预期存在不符。

第二个缺点是只能用于webpack项目,无法用于其它项目,比如基于vite构建的项目。而且webpack3、4、5不同大版本之间,api存在差异,需要根据实际项目的webpack版本信息进行api兼容修改。

上价值

通篇了解下来,插件的实现其实十分简单易懂,使用ts写也就一百来行代码。代码无难度,但是插件的实用价值是非常高的。举两个真实例子,公司的项目有几十万行代码:

  1. 我们有个安全需求,将项目中所有的data-hint赋值处检视并修改加上htmlEncode方法,同时要验收UI视图是否显示正常。这个工作量是十分庞大的,我们需要找出所有代码并根据代码找出对应的UI视图,这其中还包含了许多僵尸文件。
  2. 有个国际化需求,将项目中所有的下划线翻译函数替换为新的翻译函数,替换新的国际化key。与需求1类似,也是要找出僵尸文件。

类似的改项目全局的需求应该还有很多,都是需要找出僵尸文件的,与其一个一个猴年马月地找,通过工具扫描几分钟就完事了。一段简单的代码能够帮助我们节省许多工作量,提升工作效率,工具替代了人工,代码改变了世界。

最后

附上我的插件源码:github.com/brenner8023…

其实类似的插件实现,GitHub已经有非常多的项目,我的同事余架此前也基于本文原理实现了该插件。我是通过阅读这些优秀开发者写的代码,在他们的基础之上理解实现了该插件。阅读源码确实能够学到很多东西,阅读源码也是一种向大佬们学习的方式吧。

image.png

おすすめ

転載: juejin.im/post/7068864692344586276