这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(二)

简介: 这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(二)

开发思路


网络异常,图片无法展示
|


💡:本章节思路可适用于编写nodejs脚本或者vscode插件。


代码行数与文件数


先来最简单的需求,总体思路如下:


网络异常,图片无法展示
|


  1. 第一步我先获取项目的业务文件列表(文件列表长度为文件个数)
  2. 读出每个文件内容并使用换行符分隔就可以得到代码行数


入口方法:


const fileList = getBusinessFileList();
const fileCount = fileList.length;
const lineCount = getFileLineCount(fileList);


获取文件列表(这里是一个简单递归,不多做赘述):


const getBusinessFileList = () => {
    const dirPath = getProjectRoot();
    if (!dirPath) return [];
    return getFileList(dirPath);
}
const getFileList = (dirPath) => {
    let dirSubItems = fs.readdirSync(dirPath);
    const fileList = [];
    for (const item of dirSubItems) {
        const childPath = path.join(dirPath, item);
        if (_isDir(childPath) && !excludedDirs.has(item)) {
            fileList.push(...getFileList(childPath));
        } else if (!_isDir(childPath) && includedFileSubfixes.has(path.extname(item))) {
            fileList.push(childPath);
        }
    }
    return fileList;
}


读文件并获取行数:


const getFileLineCount = (fileList) => {
    let count = 0;
    for (const file of fileList) {
        const content = fs.readFileSync(file, {
            encoding: 'utf-8'
        });
        count += content.split('\n').length;
    }
    return count;
}


无效文件

之后我们来说更加复杂的获取无效文件,思路如下:


网络异常,图片无法展示
|


  1. 第一步就是要获取到这些文件的列表

之前文章中有说到本插件暂时仅支持本公司框架下的项目,原因就是业务根文件的获取规则可能不同


那么,什么是业务根文件呢?


这是我在此次实现中提出的概念(言外之意就是很不官方的说法)。本项目中的所有的引用关系都应该以业务根文件作为根节点,比如我列出的:pagesexpose 目录下的文件,以及main.js,他们不被引用,且一定是有效的文件。


  1. 第二步就是要进行一个递归,获取到其引用的文件,以及其引用的文件所引用的文件(套娃)


这里注意要避免死循环


网络异常,图片无法展示
|


比如一个根文件引用了 A, A 引用了 B,那么根节点和 A,B 都是有用的文件,C 就是个没有被引用,即无效文件。


总结:一个文件如果既不是业务根文件又没有被业务根文件直接或者间接引用,则为无效文件


这里我列举出三种引用方式及其对应的 import 语法:


[
// import * from './example'
/(?<statement>import\s+.*?\s+from\s+['"](?<modulePath>.+?)['"])/g,
// import('./example')
/(?<statement>import\(['"](?<modulePath>.+?)['"]\))/g,
// import './example'
/(?<statement>import\s+['"](?<modulePath>.+?)['"])/g
]


这里有关于正则语法的细节:


  • <>:可以通过尖括号中的变量名获取到匹配的内容,比如上面代码中的 modulePath 就是文件路径。
  • ?:启用非贪婪模式
  1. 第三步就是获取全量的业务文件
  2. 最后就是将全量的业务文件与根文件及其直接或间接引用的文件做差集,最终就得到了项目中的无效文件


下面开始进行代码展示,首先是入口方法:


const businessFileList = getBusinessFileList();
const businessRootFileList = getBusinessRootFileList();
const importedFileSet = getImportedFileSet(businessRootFileList);
const unusedFileList = businessFileList.filter(file => !importedFileSet.has(file));


之后获取业务根文件:


const getBusinessRootFileList = () => {
    const projectRoot = getProjectRoot();
    if (!projectRoot) return [];
    const fileList = [];
    const pagePath = path.join(projectRoot, '/views/pages');
    if (fs.existsSync(pagePath)) {
        fileList.push(...getFileList(pagePath));
    }
    const exposePath = path.join(projectRoot, '/expose');
    if (fs.existsSync(exposePath)) {
        fileList.push(...getFileList(exposePath));
    }
    const mainPath = path.join(projectRoot, '/main.js');
    if (fs.existsSync(mainPath)) {
        fileList.push(mainPath);
    }
    return fileList;
}
const getFileList = (dirPath) => {
    let dirSubItems = fs.readdirSync(dirPath);
    const fileList = [];
    for (const item of dirSubItems) {
        const childPath = path.join(dirPath, item);
        if (_isDir(childPath) && !excludedDirs.has(item)) {
            fileList.push(...getFileList(childPath));
        } else if (!_isDir(childPath) && includedFileSubfixes.has(path.extname(item))) {
            fileList.push(childPath);
        }
    }
    return fileList;
}


再之后获取根文件直接或者间接(递归)引用的文件:


这里我使用了 set,进行重复性检测,避免循环引用引起插件报错。


const getImportPathRegs = () => {
    // TODO: 无法检测运行时生成的路径
    return [
        // import * from './example'
        /(?<statement>import\s+.*?\s+from\s+['"](?<modulePath>.+?)['"])/g,
        // import('./example')
        /(?<statement>import\(['"](?<modulePath>.+?)['"]\))/g,
        // import './example'
        /(?<statement>import\s+['"](?<modulePath>.+?)['"])/g
    ]
}
const getImportedFileSet = (fileList, set = new Set([])) => {
    const _fileList = [];
    for (const file of fileList) {
        if (set.has(file)) {
            continue;
        }
        set.add(file);
        const content = fs.readFileSync(file, {
            encoding: 'utf-8'
        });
        const regReferences = getImportPathRegs();
        for (const reg of regReferences) {
            let matchResult;
            while ((matchResult = reg.exec(content))) {
                const { modulePath } = matchResult.groups;
                const filePath = speculatePath(modulePath, file);
                if (filePath && !set.has(filePath)) {
                    _fileList.push(filePath);
                }
            }
        }
    }
    if (_fileList.length) getImportedFileSet(_fileList, set);
    return set;
}


最后就是做差集了:


const unusedFileList = businessFileList.filter(file => !importedFileSet.has(file));


无效导出


最复杂的就是这个无效导出,大家写的 export 真的可能千奇百怪。实现的整体思路如下:


网络异常,图片无法展示
|


我们做这个需求,就要知道两个信息:


  • 我们引用了哪些(必须是有效文件中的引用)
  • 我们导出了什么(必须是业务根文件以外的文件中导出的)


什么是export提供者(exportProvider)?


根文件因为不会被引用所以不是 exportProvider,所以业务根文件以外的业务文件是exportProvider


获取 exportProvider


const businessFileList = getBusinessFileList();
const businessRootFileList = getBusinessRootFileList();
const businessRootFileSet = new Set(businessRootFileList);
const exportProviderList  = businessFileList.filter(file => !businessRootFileSet.has(file));


获取 export 信息


现在的获取规则还是比较 low,未来可能改为进行词法分析与语法分析,以及需要注意处理 as 语法(import中也需要考虑)。


export 规则如下:


  • export const/class/function/var/default/- moduleName
  • export const/class/function/var/default/- { moduleName }
  • export default/function 匿名

如有遗漏后续再进行处理...确实有点多


const exportInfo = getExportInfo(exportProviderList);
const getExportRegs = () => {
    // TODO: 无法检测运行时生成的路径
    return [
        // export const/class/function/var/default/- {xxx}/{xxx as yyy}
        /export\s+(const|var|let|function|class|default)?\s*{(?<provide>[\w\W]+?)}/g,
        // export const/class/function/var/default/- xxx
        /export\s+(const|var|let|function|class|default)?\s*(?<provide>[\w-]+)/g
    ]
}
// TODO: 未来改用词法分析 + 语法分析
const getExportInfo = (fileList) => {
    const exportInfo = {};
    for (const file of fileList) {
        if (path.extname(file) === '.js') {
            const content = fs.readFileSync(file, {
                encoding: 'utf-8'
            });
            const provideList = [];
            const regReferences = getExportRegs();
            for (const reg of regReferences) {
                let matchResult;
                while ((matchResult = reg.exec(content))) {
                    let { provide } = matchResult.groups;
                    // const|var|let|function|class|default
                    if (provide == 'default') {
                        provide = UNNAMED_DEFAULT;
                    } else if (provide == 'function') {
                        provide = UNNAMED_FUNCTION;
                    } else if (DECONSTRUCTION_STATEMENT_SYMBOLS.has(provide)) {
                        continue;
                    }
                    provideList.push(...provide.split(',').map(item => {
                        const temp = item.split(' as ');
                        if (temp[1]) {
                            return temp[1].replace(/\s/g, '');
                        } else {
                            return temp[0].replace(/\s/g, '');
                        }
                    }));
                }
            }
            exportInfo[file] = provideList;
        } else if (path.extname(file) === '.vue') {
            exportInfo[file] = VUE_MODULE;
        }
    }
    return exportInfo;
}


获取有效文件


复用已有方法


const importedFileSet = getImportedFileSet(businessRootFileList);
const usedFileList = businessFileList.filter(file => importedFileSet.has(file));


获取 import 信息


💡:import 很特殊,有解构引用的方式,也有全量的引用


const getImportInfo = (fileList) => {
    const importInfo = {};
    for (const file of fileList) {
        const content = fs.readFileSync(file, {
            encoding: 'utf-8'
        });
        let matchResult;
        const deconstructionReg = /import\s+{(?<provide>[\w\W]+?)}\s+from\s+['"](?<modulePath>.+?)['"]/g;
        // 解构
        while ((matchResult = deconstructionReg.exec(content))) {
            const { provide, modulePath } = matchResult.groups;
            const filePath = speculatePath(modulePath, file);
            if (filePath) {
                const provideList = provide.split(',').map(item => item.split(' as ')[0].replace(/\s/g, ''))
                if (!importInfo[filePath]) {
                    importInfo[filePath] = new Set(provideList);
                } else if (importInfo[filePath] != IMPORT_ALL) {
                    importInfo[filePath].add(...provideList);
                }
            }
        }
        const constructionRegs = [
            /import\s+(?<provide>[^{}]+?)\s+from\s+['"](?<modulePath>.+?)['"]/g,
            // import('example')
            /import\(['"](?<modulePath>.+?)['"]\)/g,
            // import './example'
            /import\s+['"](?<modulePath>.+?)['"]/g
        ]
        for (const reg of constructionRegs) {
            let matchResult;
            while ((matchResult = reg.exec(content))) {
                const { modulePath } = matchResult.groups;
                const filePath = speculatePath(modulePath, file);
                if (filePath) {
                    importInfo[filePath] = IMPORT_ALL;
                }
            }
        }
    }
    return importInfo;
}


最后根据 import 和 export 信息作处理,得到无效export


这里我的代码写得好丑...


const unusedExport = {};
    Object.keys(exportInfo).forEach(key => {
        if(exportInfo[key] === VUE_MODULE) {
            if(importInfo[key] !== IMPORT_ALL) unusedExport[key] = [VUE_MODULE];
        } else {
            if(!importInfo[key]) {
                unusedExport[key] = exportInfo[key];
            } else if(importInfo[key] != IMPORT_ALL) {
                const unusedExportList =  exportInfo[key].filter(exportItem => {
                    return !importInfo[key].has(exportItem);
                })
                if(unusedExportList.length > 0) unusedExport[key] = unusedExportList; 
            }
    }
});


import 的缺省匹配


众所周知,我们在写 import 的时候经常不写完整,比如:

  • import 写到一个目录,这时候会匹配目录下的 index.js
  • 不写引用文件的后缀名,这时候会默认匹配 xxx.js
  • ...


以及我们可能会存在相对路径和绝对路径两种方式:

  • @/xxxx 可能对应 src 目录
  • ../../ 等相对路径


于是这里还需要一个路径推测方法:


const speculatePath = (source, basicPath) => {
    let _source;
    if (source.startsWith('@/')) {
        const srcPath = getProjectRoot();
        _source = `${srcPath}${source.replace('@', '')}`
    } else {
        _source = path.join(path.dirname(basicPath), source);
    }
    if (fs.existsSync(_source) && !_isDir(_source)) {
        return _source;
    }
    let speculatePath;
    if (fs.existsSync(_source) && _isDir(_source)) {
        speculatePath = path.join(_source, '/index.js');
        if (fs.existsSync(speculatePath)) {
            return speculatePath;
        }
        speculatePath = path.join(_source, '/index.vue');
        if (fs.existsSync(speculatePath)) {
            return speculatePath;
        }
        return null;
    }
    if (!fs.existsSync(_source)) {
        speculatePath = `${_source}.js`;
        if (fs.existsSync(speculatePath)) {
            return speculatePath;
        }
        speculatePath = `${_source}.vue`;
        if (fs.existsSync(speculatePath)) {
            return speculatePath;
        }
        return null;
    }
    return null;
}


Ending,is also beginning


网络异常,图片无法展示
|


这样,本篇文章的内容到这里就全部结束了,希望对各位有所帮助,下一篇文章(或者视频)我们再见!


✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

世之奇伟、瑰怪、非常之观,
常在于险远,而人之所罕至焉,
故非有志者不能至也。

— 王安石《游褒禅山记》—

🍉诸君共勉,加油 🍉

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

相关文章
|
3月前
|
前端开发 JavaScript API
拥抱华为,困难重重,第一天学习 arkUI,踩坑踩了一天
拥抱华为,困难重重,第一天学习 arkUI,踩坑踩了一天
|
9月前
|
数据可视化 搜索推荐 程序员
丝滑!用了这款开发工具,我成了整个公司代码写得最秀的码农
丝滑!用了这款开发工具,我成了整个公司代码写得最秀的码农
|
9月前
|
Java 数据安全/隐私保护 索引
Java实训项目一一拼图游戏(附源码)
Java实训项目一一拼图游戏(附源码)
191 1
|
10月前
|
人工智能 程序员 Linux
【猿如意】CSDN推出的程序猿开发百宝箱
【猿如意】CSDN推出的程序猿开发百宝箱
152 0
|
安全 Java 微服务
好家伙!阿里P8撰写的Java微服务架构全栈笔记GitHub一夜飞到榜首
Java微服务作为当下最常用的架构技术,快速实现编程开发而且维护起来十分的方便,可以简单是实现高可用,分布式开发而且也很安全!
好家伙!阿里P8撰写的Java微服务架构全栈笔记GitHub一夜飞到榜首
|
前端开发 API
这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(一)
这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(一)
125 0
|
程序员
三行代码献礼教师节
你有三行情书,我有三行代码。
CocosCreator游戏开发---菜鸟学习之路(三)如何在CocosCreator中使用Pomelo
PS(废话): 这段时间都在研究网易的Pomelo框架,作为新手小白,自然遇到了不少坑爹的事情。(当然也有可能是因为自己技术不过关的原因所以导致在很多基础的问题上纠结了很久。)网上也搜索了好久,但是基本都是复制黏贴的教程,按照教程做肯定没有问题,但是对于新手来说个别细节上还是有很多无法理解或者说新手容易犯错的地方。
1860 0