前言
项目多版本并行开发,版本合并解决冲突时,可能会出现很多重复轮子、无用代码遗留在项目里;开发过程中需求被砍掉,废弃文件保留在了项目里,成为多年积存的僵尸文件,比如图片、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();
}
}
复制代码
- 首先是初始化好options配置,
- 然后插件的apply方法被调用,传入
compiler
对象, 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写也就一百来行代码。代码无难度,但是插件的实用价值是非常高的。举两个真实例子,公司的项目有几十万行代码:
- 我们有个安全需求,将项目中所有的
data-hint
赋值处检视并修改加上htmlEncode
方法,同时要验收UI视图是否显示正常。这个工作量是十分庞大的,我们需要找出所有代码并根据代码找出对应的UI视图,这其中还包含了许多僵尸文件。 - 有个国际化需求,将项目中所有的下划线翻译函数替换为新的翻译函数,替换新的国际化key。与需求1类似,也是要找出僵尸文件。
类似的改项目全局的需求应该还有很多,都是需要找出僵尸文件的,与其一个一个猴年马月地找,通过工具扫描几分钟就完事了。一段简单的代码能够帮助我们节省许多工作量,提升工作效率,工具替代了人工,代码改变了世界。
最后
附上我的插件源码:github.com/brenner8023…
其实类似的插件实现,GitHub已经有非常多的项目,我的同事余架此前也基于本文原理实现了该插件。我是通过阅读这些优秀开发者写的代码,在他们的基础之上理解实现了该插件。阅读源码确实能够学到很多东西,阅读源码也是一种向大佬们学习的方式吧。