likes
comments
collection
share

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

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

一 背景

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

二 思路分析

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

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

接下来是实现分析:

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

如何获取依赖文件列表:

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

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

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

在插件中, 最重要的就是 CompilerCompilation 两个对象了, 我们所需要的依赖文件列表, 最终也是在 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(),
],
...

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

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

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

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

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

过滤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没了, 同时如果有重复的依赖, 也会去重

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

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

// 获取指定目录下所有文件
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的插件就完成了, 瑕疵之处, 欢迎评论区交流!

参考