前言
书接上回:这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(一)
在我上次给我的代码完成减肥之后,我有了一个新的想法:
💡:我既然已经具有了识别 export 与 import 的能力,为何不将项目内部完整的依赖关系可视化展现,以便发现项目结构中的不合理导入导出。
于是本周末我就开始引入 let-s-refactor
插件的第四个能力:可视化前端项目内部依赖。
⚠️:本插件暂时仅支持本公司框架下的项目,本文后续内容为设计与开发思路,以供各位参考。
设计思路
最初我打算把项目的完整引用全部展示,但是项目可能存在很多根节点:
- src/views/pages/...
- src/expose/...
- src/main.js
- ...
于是存在两个问题:
- 根节点过多导致图的展示混乱
- 当我要查看某个组件的依赖路径的时候,其实并不关心其他引用分支上的导入导出关系
所以我做了如下设计:
先选择左侧的根文件,之后在右侧的区域展示该根文件的引用关系。
之后,还需要根据文件或者内容定义的不同做区分:
- root:根文件
- vueComponent:vue 组件
- constant:常量
- config:配置信息
- service:api
- util:工具方法
- other:其他信息
当然,就算我们只在视窗显示一个根文件下的引用关系,也有可能极为复杂,于是,我有个想法:
💡:动态的根据每个节点(文件)的类型(vue组件/常量/api...)去计算每个节点的位置
于是我就会去思考,我们项目中的业务文件就类型区分,基本可以分为:
- .vue 文件
- .js 文件
而在我们常见的项目中:js 文件相对于 vue 文件基本处于引用关系的下游,即基本上为 vue 文件引用 js 文件,鲜有 js 引用 vue 文件的情况。
于是在我的设计中引用树自上而下的顺序为:
- root 根节点
- vueComponent 节点
- js(config/util/...)节点
- ...
即:
最终效果
当然依然可能存在数据量巨大的情况,我做了鼠标悬停的效果:
技术实现
上面我们已经看完了我对这个功能的设计,下面请各位跟随我的视角一起把这个功能实现出来。
获取业务根文件
入口:
const viewProjectArchitecture = vscode.commands.registerCommand('let-s-refactor.viewProjectArchitecture', () => { const businessRootFileList = getBusinessRootFileList(); ArchitectureViewProvider.initTreeView(businessRootFileList); });
getBusinessRootFileList 方法:
就是提取
pages
,expose
,main.js
文件列表
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; }
getBusinessRootFileList 方法使用了 getFileList 方法:
一个小递归,入参是目录路径
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; }
获取根节点引用的所有后代节点
获取到的节点就是所有展示在引用树上的所有节点~后续我们也需要读取这些文件内容拿到引用的
source
和target
入口:
const importedFileList = [...getImportedFileSet([path])];
getImportedFileSet 方法:
此方法在 这个夏天,给你的代码减个肥 🍉|let-s-refactor 插件开发之路(一) 中有介绍,主要是使用正则拿到所有引用路径的
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; }
获取内部引用关系
既然我们已经拿到了全部的文件信息,那么我们就可以读取他们,拿到引用的
source
和target
了~
入口:
const { links, fileValueMap } = getRelationshipMapLinkInfo(importedFileList);
getRelationshipMapLinkInfo 方法:
这里我们的目标是拿到引用的
source
和target
,以及引用的数量
const getRelationshipMapLinkInfo = (fileList) => { const links = []; const fileValueMap = new Map(); for (const targetFile of fileList) { const content = fs.readFileSync(targetFile, { encoding: 'utf-8' }); const regReferences = getImportPathRegs(); let value = 0; for (const reg of regReferences) { let matchResult; while ((matchResult = reg.exec(content))) { const { modulePath } = matchResult.groups; const sourceFile = speculatePath(modulePath, targetFile); if(!sourceFile) continue; value ++; links.push({ source: sourceFile, target: targetFile, }) } } fileValueMap.set(targetFile, value); } return { links, fileValueMap }; }
获取关系图节点信息
此处我们使用的是
echarts
进行图表渲染,再加上前文设计思路中所说的,我要动态计算节点的位置,于是诞生了这个方法
入口:
const nodes = getRelationshipMapNodes(importedFileList, path);
getRelationshipMapNodes 方法:
此处注意,我为图表左侧和顶部预留了 50px 宽度,以及每个节点都在我设计的 160 * 100 四边形中的随机一点
const getRelationshipMapNodes = (fileList, rootBusinessPath) => { const rootPath = getProjectRoot(); const bufferLeft = 50; const bufferTop = 50; let rootFile = ''; const componentFileList = []; const noncomponentFileList = []; fileList.forEach(file => { if (file == rootBusinessPath) { rootFile = file; } else if (path.extname(file) === '.vue') { componentFileList.push(file); } else { let category = 'Other'; if (file.startsWith(`${rootPath}/constant`)) { category = 'Constant'; } else if(file.startsWith(`${rootPath}/config`)) { category = 'Config'; } else if(file.startsWith(`${rootPath}/service`)) { category = 'Service'; } else if(file.startsWith(`${rootPath}/util`)) { category = 'Util'; } noncomponentFileList.push({ file, category }) } }); const nodes = []; if(!rootFile) return nodes; const columnCount = 10; const columnWidth = 1000 / columnCount; const rowHeight = 160; const getRandomPosition = (row, column) => { return { x: bufferLeft + (column + 0.3 + 0.4 * Math.random()) * columnWidth, y: bufferTop + (row + + 0.2 + 0.6 * Math.random()) * rowHeight } } let maxColumn = 0; let row = 1; let column = 0; for(const file of componentFileList) { nodes.push({ id: file, name: file.replace(rootPath, ''), path: file, category: 'VueComponent', ...getRandomPosition(row, column) }) if(column == columnCount - 1) row ++; maxColumn = Math.max(column, maxColumn); column = (column + 1) % columnCount }; if(column != 0) row ++; column = 0; for(const fileInfo of noncomponentFileList) { nodes.push({ id: fileInfo.file, name: fileInfo.file.replace(rootPath, ''), path: fileInfo.file, category: fileInfo.category, ...getRandomPosition(row, column) }) if(column == columnCount - 1) row ++; maxColumn = Math.max(column, maxColumn); column = (column + 1) % columnCount }; nodes.push({ id: rootFile, name: rootFile.replace(rootPath, ''), path: rootFile, category: 'Root', x: bufferLeft + maxColumn * columnWidth / 2, y: bufferTop + rowHeight / 2 }); return nodes; }
随后我们就可以根据返回的节点去渲染 echarts 的关系图了~
结束语
那么,这篇文章的全部内容就到此结束了。接下来打算先用语法分析优化 export
与 import
分析准确性,以及增加本插件的普遍适用性。
各位年薪百万的读者,我们下次再见!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
流波将月去,潮水带星来。
杨广《春江花月夜》
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨