通过webpack删除项目中的废弃文件

1,528 阅读3分钟

现在做的产品是历经两个产品线的合并,n代开发经手的代码,项目文件很多,找起来很不方便,而且刚接手不熟悉逻辑的时候,上来一搜很多重复代码,向上查找引用发现根本没有用到这个文件。。。。所以就打算做个一键删除废弃代码的工具。。。

原理就是依赖于webpack的stats.json文件

stats.json

通过命令将依赖关系输出到stats.json中

webpack --env staging --config webpack.config.js --json > stats.json

来看一下输出到stats.json的内容,主要用到的分为三个模块assets,chunks,modules

asset

image.png

image.png

可以看到asset里是打出来的静态资源,只包含输出的文件名称,而像图片这些路径都是../, 有的图片甚至是../images,这是由于webpack的配置问题,将静态资源打到了我们的配置文件/assets/images文件夹中

image.png

image.png

modules

image.png

chunks

image.png

可以看圈出来的几块位置,chunks和module是有关联关系的。而且modules里的name是带有代码路径的

代码实现

本着宁愿不删,也不错删的原则下,有了下面版本的代码

bin目录下的入口文件,拿到用户传入的参数

#!/usr/bin/env node
const program = require('commander');
const UnusedFinder=require('../lib/UnusedFinder');

program.option('-f, --file','生成unused文件名')
program.option('-r, --remove','删除无用的文件');

program.parse(process.argv);

const args = process.argv.slice(2);

let fileName= '';
let remove = false

let arg;
while(args.length){
	arg=args.shift();
	switch(arg){
		case '-f':
		case '--file':
			fileName=args.shift()|| '';
			break;
		case '-r':
		case '--remove':
			remove=true;
			break;
	}
}

const finder = new UnusedFinder();
finder.start({fileName, remove});

unusedFinder实现

// 删除文件的时候,可以自定义忽略删除的目录,毕竟有些公共的hooks啥的现在没有引用可能也不想删除
function ask() {
    return inquirer.prompt([{
      type: "input",
      name: "ignore",
      message: "请输入忽略删除的文件路径,以逗号分割"
    }])
    .then(asw => asw);
  }
class UnusedFinder {
    constructor(config = {}) {
        this.statPath = path.join(process.cwd(), './stats.json');
        this.usedFile = new Set();
        this.assetsFile = new Set() // 存放静态资源
        this.allFiles = [];
        this.unUsedFile=[];
        // 默认删src文件夹底下的,也可以自己配置
        this.pattern = config.pattern || './src/**';
    }

    hasStats = () => {
        if (!existsSync(this.statPath)) {
            throw new Error("请检查在项目根目录执行,并生成stats.json文件");
        }
    }

    removeFiles = () => {
        ask().then(answer=> {
            let ignoreFile = []
            const {ignore} = answer
            if(ignore){
                ignoreFile = ignore.split(',').filter(Boolean)
            }
            const task = []
            spinner.start('文件删除中......');
            this.unUsedFile.forEach(item => {
                // 根目录下的不删除
                if(item.split('/').length <= 2) return
                // style里import的style没有在stats.json里体现,所以暂时不删除  
                if(/^\.\/styles/.test(item)) return;
                // md文档不删除
                if(item.substr(item.length - 3, 3) === '.md') return
                // 自定义忽略删除的文件夹
                if(ignoreFile.some(fileName => item.includes(`/${fileName}`))) return
                const url = item.replace('./', './src/')
                if(!existsSync(url)) return spinner.warn(`${url}文件不存在`)
                const promise = rm(url, val=>val)
                task.push(promise)
                })  
                Promise.all(task)
                    .then(() => {
                        spinner.succeed('文件删除成功')
                    }).catch((err)=>{
                        spinner.fail('存在文件删除失败')
                        err.forEach(item => {
                            error(item)
                        })
                    })
            })
    }

    findUsedModule = () => {
        // 格式化stats.json文件
        const statsData = JSON.parse(readFileSync(this.statPath));
        const chunks = statsData.chunks;
        const modules=statsData.modules;
        const assets=statsData.assets;
        chunks.forEach(chunk => {
            chunk.modules.forEach(value => {
                // name会有 + 1 modules的情况,为了避免取错,还是加下替换
                // ../node_modules/@antv/g-canvas/esm/canvas.js + 1 modules
                const name = value.name.replace(/ \+ [0-9]* modules/g, '').replace(/\?.+=./,'');
                // node_modules没必要放入
                if (name.indexOf("node_modules") === -1) {
                    this.usedFile.add(name);
                }
                value.modules && value.modules.forEach(subModule => {
                    if (subModule) {
                        const name = subModule.name.replace(/ \+ [0-9]* modules/g, '').replace(/\?.+=./,'');
                        if (name.indexOf("node_modules") === -1) {
                            this.usedFile.add(name);
                        }
                    }
                })
            })
        })
        // 生成的模块
        modules.forEach(value=>{
            const name = value.name.replace(/ \+ [0-9]* modules/g, '').replace(/\?.+=.+/,'');
            if (name.indexOf("node_modules") === -1) {
                this.usedFile.add(name);
            }
        });
        // 生成的静态资源
        assets.forEach(value=>{
            const name = value.name.split('/')
            // 因为静态资源是按照打包完后的dist文件的路径做的展示,所以我们只匹配文件名称
            if (name.indexOf("node_modules") === -1) {
                this.assetsFile.add(name[name.length - 1]);
            }
        });
    }

    findAllFiles = () => {
        const files=glob.sync(this.pattern, {
            nodir: true
        });
        this.allFiles = files.map(item => {
            return item.replace('./src', '.');
        })
    }

    findUnusedFile=()=>{
        this.unUsedFile=this.allFiles.filter(item=>!this.usedFile.has(item));
        this.unUsedFile = this.unUsedFile.filter(item => {
            const name = item.split('/')
            // 如果包含静态资源的名称,则去掉文件,当然也会存在重名的情况
            if(this.assetsFile.has(name[name.length - 1])){
                return false
            }
            return true
        })
    }

    start = ({fileName, remove}) => {
        this.hasStats();
        this.findUsedModule();
        this.findAllFiles();
        this.findUnusedFile();
        if(fileName){
            writeFileSync(fileName,JSON.stringify(this.unUsedFile))
        }else{
            warn(`未被使用的文件:\n${this.unUsedFile.join('\n')}`)
        }
        if(remove){
            this.removeFiles()
        }
    }

}

module.exports=UnusedFinder;

在现阶段的工具下,一键删除了将近500个无用的业务文件。当然先阶段的代码还是存在一定的问题,像静态资源,样式文件等都还没有找到好的方法去做匹配,只能通过文件名称去做模糊匹配,这样就可能存在有的无用同名文件被过滤而没有被删除,如果有更好的办法,希望大家可以在评论区交流一下。