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


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


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


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

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

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

🍉诸君共勉,加油 🍉

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

相关文章
|
5月前
|
前端开发 JavaScript 开发者
水墨代码:前端川的诞生——在夏日阴雨中启航
【前端川】网站于农历五月初一(2024年6月6日)上线,融合水墨画与现代前端技术,呈现独特的水墨代码美学。创建者陈川分享技术心得与实战经验,网站特色包括水墨风格界面、技术深度解析及实战案例。在夏日雨中启航,"前端川"致力于为开发者提供灵感与帮助,探索前端技术的无限可能。
103 17
|
1月前
|
前端开发 API 开发者
🥇前端宝藏:多项目掌握技能的冒险之旅🏆
在前端开发的学习旅程中,实践是提升技能的关键。本文介绍了多个前端项目,包括计算器、天气应用、经典游戏等,涵盖了从React到Svelte的各种技术栈。每个项目都附有在线演示和源代码,旨在帮助读者深入理解实现细节,激励更多人参与实际项目开发。通过这些项目,读者可以将理论知识转化为实践,拓展职业机会。
18 0
|
3月前
|
存储 Python 容器
"解锁编程奇迹,Python基础入门:一剑在手,编程江湖任你遨游,从零到英雄的超燃蜕变之旅!"
【8月更文挑战第12天】编程曾被视为复杂的技能,Python却让其变得异常亲和简单。作为优雅且强大的语言,Python以简洁的语法、丰富的库支持及广泛的应用领域,成为初学者首选。本文将引导你开启Python学习之旅,通过基础概念与示例代码,让你领略编程魅力。
50 0
|
6月前
|
开发者
作为微信小游戏开发者,这份白皮书不看可太吃亏了!
作为微信小游戏开发者,这份白皮书不看可太吃亏了!
191 1
|
6月前
|
前端开发 JavaScript API
拥抱华为,困难重重,第一天学习 arkUI,踩坑踩了一天
拥抱华为,困难重重,第一天学习 arkUI,踩坑踩了一天
234 0
|
数据可视化 搜索推荐 程序员
丝滑!用了这款开发工具,我成了整个公司代码写得最秀的码农
丝滑!用了这款开发工具,我成了整个公司代码写得最秀的码农
|
人工智能 程序员 Linux
【猿如意】CSDN推出的程序猿开发百宝箱
【猿如意】CSDN推出的程序猿开发百宝箱
219 0
|
设计模式 安全 关系型数据库
2w行代码、200个实战项目,助你修炼5大编程基本功
2w行代码、200个实战项目,助你修炼5大编程基本功
162 0
|
安全 Java 微服务
好家伙!阿里P8撰写的Java微服务架构全栈笔记GitHub一夜飞到榜首
Java微服务作为当下最常用的架构技术,快速实现编程开发而且维护起来十分的方便,可以简单是实现高可用,分布式开发而且也很安全!
好家伙!阿里P8撰写的Java微服务架构全栈笔记GitHub一夜飞到榜首
|
前端开发 API
这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(一)
这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(一)
154 0