这个夏天,给你的代码减个肥 🍉|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


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


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


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

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

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

🍉诸君共勉,加油 🍉

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

相关文章
|
人工智能 自然语言处理 开发者
Copilot的基本原理
【2月更文挑战第13天】Copilot的基本原理
1113 3
Copilot的基本原理
|
存储 弹性计算 文件存储
Windows容器使用阿里云NAS SMB文件系统做持久化存储目录
随着Windows容器逐渐普及和发展,Windows容器持久化存储以及容器间共享的需求越来越高涨。 本文介绍如何让Windows主机正确配置NAS SMB文件系统,支持Windows容器让Docker镜像使用挂载NAS SMB文件系统的子目录作为持久化存储目录。
8133 0
Windows容器使用阿里云NAS SMB文件系统做持久化存储目录
|
2月前
|
调度 算法框架/工具
Mac mini M4 性能压榨:Flux.dev 与 Z-image 实测组合
本文实测Mac mini M4(24G统一内存)下Flux.dev与Z-image大模型的最优采样组合:Flux推荐Euler+Beta(25步/Guidance 3.5),兼顾速度与画质;Z-image首选euler_ancestral+Beta(2–8步/CFG 1.0),激发多样性。附内存优化、CFG避坑与步数阈值建议,助你在有限资源下榨干M4性能。(239字)
421 6
|
5月前
|
数据采集 人工智能 搜索推荐
智能体来了:降本增效的终极杀手锏,销售人必看的生存指南
内容摘要:AI智能体(AI Agents)正重塑销售底层逻辑。本文深度拆解智能体如何通过自动化获客、个性化触达及全天候线索转化,协助销售人员打破内卷僵局,实现业绩呈指数级增长。
441 3
|
11月前
|
传感器 人工智能 运维
吃得安心靠AI?聊聊AI在食品供应链安全里的“神操作”
吃得安心靠AI?聊聊AI在食品供应链安全里的“神操作”
486 6
|
SQL 存储 Apache
基于 Flink 进行增量批计算的探索与实践
本文整理自阿里云高级技术专家、Apache Flink PMC朱翥老师在Flink Forward Asia 2024的分享,内容分为三部分:背景介绍、工作介绍和总结展望。首先介绍了增量计算的定义及其与批计算、流计算的区别,阐述了增量计算的优势及典型需求场景,并解释了为何选择Flink进行增量计算。其次,详细描述了当前的工作进展,包括增量计算流程、执行计划生成、控制消费数据量级及执行进度记录恢复等关键技术点。最后,展示了增量计算的简单示例、性能测评结果,并对未来工作进行了规划。
1352 6
基于 Flink 进行增量批计算的探索与实践
|
人工智能 编解码 测试技术
TripoSG:3D生成新纪元!修正流模型秒出高保真网格,碾压传统建模
TripoSG 是 VAST AI 推出的基于大规模修正流模型的高保真 3D 形状合成技术,能够从单张图像生成细节丰富的 3D 网格模型,在工业设计、游戏开发等领域具有广泛应用前景。
716 15
TripoSG:3D生成新纪元!修正流模型秒出高保真网格,碾压传统建模
|
缓存 负载均衡 JavaScript
构建高效后端服务:Node.js与Express框架实践
在数字化时代的浪潮中,后端服务的重要性不言而喻。本文将通过深入浅出的方式介绍如何利用Node.js及其强大的Express框架来搭建一个高效的后端服务。我们将从零开始,逐步深入,不仅涉及基础的代码编写,更会探讨如何优化性能和处理高并发场景。无论你是后端新手还是希望提高现有技能的开发者,这篇文章都将为你提供宝贵的知识和启示。
|
编译器 Linux C语言
c语言的编译器vs2019的安装及简单实用
c语言的编译器vs2019的安装及简单实用
571 0
|
人工智能 自然语言处理 机器人
如何搭建一个智能对话机器人?
如何搭建一个智能对话机器人?
608 1