100行代码删除前端项目中的无用文件

4,342 阅读4分钟

一 背景

最近在做项目工程治理, 其中有一项是要清除项目中的无用文件, 因为项目经过发展迭代, 无数人的接手, 累积了许多用不到的文件, 这些文件平时没人会去主动删除, 久而久之累积起来, 对项目开发造成一些影响. 这次工程治理刚好集中清理一波.

二 思路分析

当着手做这件事的时候, 一开始的思路还是比较容易想到的, 主要分为三步:

  1. 整理出打包所依赖的文件列表
  2. 整理出项目中所有文件的列表
  3. 两者比较, 即可得出未使用文件列表

接下来是实现分析:

  1. 第二步可以利用 node 遍历出项目中所有的文件即可实现
  2. 第三步也比较简单, 操作两个数组比较
  3. 重要的是第二步, 如何获取依赖文件列表.

如何获取依赖文件列表:

我们知道打包是从入口文件开始一步一步分析依赖的, 如果我们要自己实现这个过程, 就需要实现模块遍历器, 而这是比较花功夫的, 由于这次的项目是基于 webpack 构建的, 而 webpack 已经实现了依赖分析, 接下来只需要知道怎么利用 webpack 去得到依赖列表即可.

同时还可以实现成一个 webpack 插件, 这样其他项目也可以使用.

接下来就是 webpack 插件的一些知识了, webpack plugin 的实现是利用 webpack 提供的钩子函数. 具体可参考 编写一个插件

在插件中, 最重要的就是 Compiler 和 Compilation 两个对象了, 我们所需要的依赖文件列表, 最终也是在 Compilation 中. 通过查看 webpack 文档, 最终找到 compilation.fileDependencies 这个对象

三 代码

定义插件基本结构和解析fileDependencies

首先我们实现一个简单的插件, 先把 fileDependencies 打印出来看一下

// unusedFilePingin.js

const fs = require('fs');
const path = require('path');

const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

class UnusedFilePingin {
  constructor(options = {}) {
    const defaultOptions = {};

    // 加载配置
    this.options = {};
  }

  apply(compiler) {
    const {} = this.options;
  
    // 注意此处是webpack5的插件写法, 4版本请自行查阅实现
    compiler.hooks.afterEmit.tap("UnusedFilePingin", (compilation) => {
      compilation.fileDependencies.forEach((filepath) => {
        console.log(filepath);
      });
    });
  }
}

module.exports = UnusedFilePingin;

webpack中引入一下

// webpack.config.js

const UnusedFilePingin = require("./unusedFilePingin");

...
plugins: [
  new UnusedFilePingin(),
],
...

运行 build 或 start, 可以看到打印出来一些文件信息, 这就是webpack分析出来的路径依赖

image.png

这里稍微解释一下fileDependencies对象 点进去fileDependencies源码, 可以看到它是一个webpack中的LazySet对象, 而LazySet中实现了遍历的方法和迭代器, 所以我们可以直接利用forEach遍历, 这样我们就得到依赖文件列表啦!

image.png

image.png

过滤fileDependencies和解析全部文件列表

最重要的部分已经得到, 但其实仔细观察fileDependencies可能还是不太符合我们的需求, 需要做一定的过滤. 比如最基本的node_modules我们肯定是不需要的. 需要排除一下.

我们把上面的打印fileDependencies的代码改造一下, 加上去重, 打印或者输出看看

compiler.hooks.afterEmit.tap("UnusedFilePingin", (compilation) => {
  // fileDependencies 利用set做一遍去重
  const fileDependenciesList = [...new Set(compilation.fileDependencies)];
  // 排除掉 node_module
  const filterFileDependenciesList = fileDependenciesList.filter(
    (file) => !file.includes(NODE_MODULES)
  );
  // console.log(filterFileDependenciesList);
});

对比会发现node_modules没了, 同时如果有重复的依赖, 也会去重

image.png

接下来就是得到全部文件列表, 其实就是一个文件遍历的递归操作

// 获取指定目录下所有文件
function getAllFiles(dirPath) {
  const allFiles = [];
  function fn(dirPath) {
    const files = fs.readdirSync(dirPath);
    files.forEach((item) => {
      const fp = path.resolve(dirPath, `./${item}`);
      const tmp = fs.lstatSync(fp);
      if (tmp.isDirectory()) {
        fn(fp);
      } else {
        allFiles.push(fp);
      }
    });
  }
  fn(dirPath);
  return allFiles;
}

也有可能我们需要排除掉某个文件目录不过滤, 所以增加exclude, 或者过滤某种文件类型, 增加excludeSuffix, includeSuffix, 将上面得到的全部文件列表传入下面的filterFiles函数得到 过滤后文件列表

function filterFiles(option) {
  const {
    allFiles = [],
    exclude = [],
    excludeSuffix = [],
    includeSuffix = [],
  } = option;

  // 过滤 exclude
  const allFileList1 = allFiles.filter((item) => {
    return !exclude.find((ex) => {
      const reg = new RegExp(ex);
      return reg.test(item);
    });
  });

  // 过滤 excludeSuffix
  const allFileList2 = allFileList1.filter((item) => {
    return excludeSuffix.indexOf(path.extname(item)) < 0;
  });

  // 过滤 includeSuffix
  const allFileList3 =
    includeSuffix.length > 0
      ? allFileList2.filter((item) => {
          return includeSuffix.indexOf(path.extname(item)) >= 0;
        })
      : allFileList2;

  return allFileList3;
}

对比全部文件列表

compiler.hooks.afterEmit.tap("UnusedFilePingin", (compilation) => {
      // fileDependencies 利用set做一遍去重
      const fileDependenciesList = [...new Set(compilation.fileDependencies)];
  // 排除掉 node_module
  const filterFileDependenciesList = fileDependenciesList.filter(
    (file) => !file.includes(NODE_MODULES)
  );

  // 扫描全部文件列表
  // 扫描规则, exclude, excludeSuffix, includeSuffix
  const allFileList = getAllFiles(resolveApp(root));
  const allFilterFiles = filterFiles({
    allFiles: allFileList,
    exclude,
    excludeSuffix,
    includeSuffix,
  });
  // console.log(allFilterFiles);

  // 对比两个文件列表
  const unUsedFiles = allFilterFiles.filter(
    (item) => !filterFileDependenciesList.includes(item)
  );
});

最后得到的unUsedFiles就是未使用文件啦! 最后可以将unUsedFiles输出到json文件中, 还可以添加删除的功能, 就不在这里展示了, 完整代码可以参考unused-file-pligun

四 总结

最后总结一下整个过程:

  1. 根据compilation.fileDependencies经过去重, 过滤node_modules之后得到依赖文件列表
  2. 利用node扫描目录, 得到所有文件列表, 经过exclude, excludeSuffix,includeSuffix过滤后得到目标文件列表
  3. 最终将两个列表对比, 得到的就是无用文件列表
  4. 最后输出无用文件列表, 删除无用文件

npm包在此: unused-file-pligun github代码在此: unused-file-pligun

至此, 这一个清理项目无用文件的webpack的插件就完成了, 瑕疵之处, 欢迎评论区交流!

参考