背景

SCRM 项目需要交接给另外一个部门。领导出于一些考虑,需要把对方只需要的功能保留,其他功能删除,然后把代码上传到新的仓库地址,再作交接。

和产品经理沟通之后,明确了以下需求:

  1. 以页面(功能)为单位,保留或者删除
  2. 个别页面需要删除一些功能

问题分析

以页面(功能)为单位,保留或者删除。也就是说,按照粒度从大到细,一个路由对应着一个页面,一个页面可能包含多个tab,一个页面有多个资源(导入进来的组件、api文件、图片、utils方法等等),而它们也分全局和局部的。

粒度越大,人工删除会比较很方便。粒度越小,如果是人工删除,需要先确认这个资源是否别其他页面引用,是的话则需要保留,否的话可以删除。

碍于人工删除细粒度的资源需要比较耗费时间和精神。希望写一个脚本来实现这个功能。想起文件依赖,就很容易联想到 webpack 原理的 构建阶段module 的依赖分析。

webpack 在初始化编译环境之后:

  1. 内置插件 EntryPlugin 根据 entry 配置找到入口 main.js 文件,调用 compilation.addEntry 函数触发构建流程
  2. 调用对应的 loaders 转译成 javascript 文本
  3. 再通过 acorn 解析成 AST 树,进行遍历 AST 树,监听 import 对应的钩子,得到对应的资源依赖,调用 moduleaddDependency 将依赖添加到当前 module 的依赖列表
  4. 对于新增的依赖,回到第 2 步继续处理

如果我们可以拿到 webpack 帮我们做好的 依赖文件列表,再对比 src 目录下面的文件,如果文件不在 依赖文件列表,就收集起来,然后删除掉。

代码实现

const glob = require('glob');
const path = require('path');
const fs = require('fs')
class FileShaking {
    constructor(options) {
        this.options = {
            excludeRegex: [
                /readme\.md/i, // 不删除readme文件
                /utils/ // 不删除工具方法目录下的文件
            ],
            delete: false,
            ...options
        };
        this.fileDependencies = [];
        this.srcFiles = [];
        this.toDelFiles = [];
    }
    apply (compiler) {
        compiler.hooks.afterEmit.tap("FileShaking", (compilation) => {
            this.fileDependencies = Array.from(compilation.fileDependencies).filter(path => !path.includes('node_modules'));
            this.deleteIndependence();
        });
    }
    async deleteIndependence () {
        this.srcFiles = await this.getSrcFiles();
        this.srcFiles.forEach(filePath => {
            if (!this.fileDependencies.includes(filePath) && !this.matchExclude(filePath)) {
                this.toDelFiles.push(filePath)
            }
        })
        if (this.options.delete) {
            this.delFiles();
            this.delEmptyDir('./src', (err) => {
                if (err) {
                    console.log(err)
                } else {
                    console.log('删除空文件夹DONE')
                }
            });
        }
    }
    getSrcFiles () {
        return new Promise((resolve, reject) => {
            glob('./src/**/*', {
                nodir: true
            }, (err, files) => {
                if (err) {
                    reject(err)
                } else {
                    let out = files.map(file => {
                        let tmpFilePath = path.resolve(file);
                        return tmpFilePath.slice(0, 1).toUpperCase() + tmpFilePath.slice(1);
                    });
                    resolve(out)
                }
            })
        })
    }
    matchExclude (pathname) {
        let matchResult = false;
        if (this.options.excludeRegex.length) {
            for (let i = 0; i < this.options.excludeRegex.length; i++) {
                if (matchResult = this.options.excludeRegex[i].test(pathname)) {
                    return matchResult
                }
            }
        }
        return matchResult;
    }
    delEmptyDir (dir, cb) {
        fs.stat(dir, (err, stat) => {
            if (err) {
                cb(err)
                return;
            }
            if (stat.isDirectory()) {
                fs.readdir(dir, (err, objs) => {
                    objs = objs.map(item=>path.join(dir,item));
                    if (err) {
                        cb(err)
                        return
                    }
                    if (objs.length === 0) {
                        return fs.rmdir(dir, cb)
                    } else {
                        let count = 0
                        function done(...rest) {
                            count++;
                            if (count === objs.length) {
                                cb(...rest);
                            }
                        }
                        objs.forEach(obj => {
                            this.delEmptyDir(obj, done)
                        })
                    }
                })
            }
        })
    }
    delFiles () {
        this.toDelFiles.forEach(item => {
            fs.unlink(item, (err) => {
                console.log(err)
            });
        })
        console.log('删除文件DONE')
    }
}

module.exports = FileShaking;

以上代码已放到我的github: https://github.com/Rockergmai...

结果

  1. 在删除路由文件之后,跑一遍,得到结果:删除162个文件
    企业微信截图_16463881787647.png

剩余的需求点,人工调整即可。

Reference

https://segmentfault.com/a/11...
https://github.com/Viyozc/use...


RockerLau
363 声望11 粉丝

Rocker Lau