源码定位实现与优化艺术分享~~🌹🌹🌹

734 阅读10分钟

前言

不知道各位小伙伴有没有遇到过这样的场景,就是面对一个庞大的前端企业级项目时,我们想定位到具体的组件时,多少会有点耗费时间,特别是想要修改别人的代码的时候,这个问题就尤为明显了...

那么我们常见的解决方法是什么呢?

其实大部分情况下,我们的思路是根据浏览器的url去检查前端的路由文件,然后再去定位到具体的项目目录,但是如果路由文件里面的文件还是比较多的时候(比如对应的文件夹下面不仅仅是一个index.vue文件,还有组件级别的components、常量定义、类型定义...),我们其实还是不容易很快定位到具体的组件,比如一些弹窗组件,通用的业务组件等等,小编在公司的大型微前端项目里面可谓是吃尽了这个苦头🤕,据leader统计,项目中的 .vue 文件已经接近8000个了!!,所以开发一款能够快速定位项目源码的调试系统迫在眉睫...😣😣😣

接下来就引入了小编本次的话题:项目源码定位系统👇👇👇

小编实现的这个源码定位系统是通过构建工具与框架协同工作实现的,按时间顺序完整流程如下,先简单介绍一下实现流程,后面再详细说明一下如何实现:

  1. 构建阶段(Vite插件)

    •  首先,cscMarkPreset作为Vite插件在文件转换阶段执行
    •  通过Vite的transform钩子拦截每个.vue文件
    •  解析SFC(单文件组件)并找到模板中的第一个根元素
    •  向根元素注入css-mark="[压缩的文件路径]"属性
    •  这一步完全由Vite构建工具控制,发生在Vue编译之前
  2. 编译阶段(Vue框架)

    •  接下来,Vue模板编译器接收到修改后的模板代码
    •  使用注册的cssMarkNodeTransform转换器处理AST
    •  转换器识别带有css-mark属性的元素
    •  将压缩的文件路径添加为css-vite-mark-XXX类名
    •  这一步由Vue框架的编译系统处理,属于模板编译环节
  3. 运行时阶段(浏览器)

    •  组件在浏览器中渲染,DOM元素包含特殊类名
    •  Tampermonkey脚本监听页面并识别这些特殊类名
    •  用户激活"Inspect"模式时,脚本高亮所有带标记的组件
    •  点击组件时,脚本解压类名中的路径信息并展示层级关系
    •  提供"打开"功能直接跳转到VS Code中的源文件

 整个过程是构建工具(Vite)→框架编译器(Vue)→运行时工具(Tampermonkey)的链式协作,通过这种多层次技术组合实现无侵入式的组件源码定位功能。

从整体实现看,我们是设计了一个完整的组件标记传递链路:

  1. Vite构建时注入压缩的文件路径作为标记

  2. Vue编译过程中将标记作为特殊类名传递给DOM

  3. 浏览器运行时通过扩展工具识别并解析这些类名

这种设计的主要优势如下:

  1. 性能考量:使用lz-string压缩路径,减少标记体积

  2. 无侵入性:整个过程不需要修改业务代码,纯粹通过构建和编译扩展实现

根据上述内容其实可以发现,小编还结合了油猴的自定义脚本实现的,以下也简要介绍一下工作原理~~

Tampermonkey的工作原理是在网页加载时注入用户脚本,使脚本能够访问和修改页面内容。这让用户能够自定义网络体验,实现浏览器本身不提供的功能。

具体实现

1. 前期准备

npm install lz-string @vue/compiler-sfc --save-dev

2. 创建目录结构

your-project/
├── vite-plugins/
│   └── css-mark.ts   # 核心插件文件
├── vite.config.ts    # Vite配置文件

接下来就是具体的实现步骤,小编在此过程中贴了相关的调试效果和核心代码,给大家简要看看效果就好,如果要复现我的实现思路的话直接去看小编的GitHub项目即可,小编用自己学校的小组作业项目做了这个源码定位功能,目录也比较简洁,相关的配置源码都在里面~~

源码传送门,需要的小伙伴自取即可

3. 实现构建阶段注入

首先创建 vite-plugins/css-mark.ts 文件,实现构建阶段的标记注入~~

效果如下:

Kim 2025-05-06 134037.png

核心代码及其解释:

  • 该vite插件处理所有.vue文件

  • 使用@vue/compiler-sfc解析Vue文件模板

  • 找到模板中的第一个元素节点

  • 使用LZString.compressToBase64压缩文件路径

  • 在元素标签中注入css-mark属性


 export const cssMarkPreset = (): Plugin => {
   return {
     name: 'vite-plugin-vue-css-mark-preset',
     transform(code, id) {
       if (!id.endsWith('.vue')) {
         return null;
       }
 
       const { template } = parse(code).descriptor;
 
       if (template && template.ast && template.ast.children) {
         const elm = template.ast.children.find(item => item.type === 1); // 1是元素节点
         if (elm) {
           const tagString = `<${elm.tag}`;
           const insertIndex = elm.loc.source.indexOf(tagString) + tagString.length;
           
           const newSource = 
             `${elm.loc.source.slice(0, insertIndex)} css-mark="${LZString.compressToBase64(id)}"${elm.loc.source.slice(insertIndex)}`;
           
           code = code.replace(elm.loc.source, newSource);
         }
       }
 
       return code;
     },
   };
 };

4. 实现编译阶段转换

继续在css-mark.ts文件中添加编译阶段的转换函数

关键点解释:

  • cssMarkNodeTransform函数作为Vue编译器的节点转换器

  • 检测并处理元素节点

  • 从父级获取css-mark属性值

  • 通过addClass函数将属性值转换为类名添加到组件

xport const cssMarkNodeTransform = (node, context) => {
   // NodeTypes: ROOT=0, ELEMENT=1, IF_BRANCH=10
   if (node.type === 1 && context.parent) {
     if ([0, 10].includes(context.parent.type)) {
       const firstElm = context.parent.children.find(item => item.type === 1);
       const markAttr = firstElm && firstElm.props && firstElm.props.find(item => item.name === 'css-mark');
       const markValue = markAttr && markAttr.value && markAttr.value.content;
       
       if (markValue) {
         // 统一处理,不区分组件类型
         addClass(node, markValue, 'class');
       }
     } else if (context.parent.props) {
       const parentMarkAttr = context.parent.props.find(item => item.name === 'css-mark');
       const parentMarkValue = parentMarkAttr && parentMarkAttr.value && parentMarkAttr.value.content;
       
       if (parentMarkValue) {
         // 为子组件添加标记
         addClass(node, parentMarkValue, 'class');
       }
     }
   }
 };
 
 // 辅助函数,添加类名
 function addClass(node, markValue, className) {
   const prefix = 'css-vite-mark-';
   const addInfo = `${prefix}${markValue}`;
   const classAttr = node.props && node.props.find(prop => prop.type === 6 && prop.name === className);
   if (classAttr) {
     classAttr.value.content += ` ${addInfo}`;
   } else {
     node.props.push({
       type: 6, // ATTRIBUTE
       name: className,
       value: {
         type: 2, // TEXT
         content: addInfo,
         loc: node.loc,
       },
       loc: node.loc,
     });
   }
 }

效果如下:

a. 处理过程

1746508944250.png

b. 页面效果

1746509142589.png

5. 配置Vite插件

在vite.config.ts中引入并配置插件:

关键点解释:

  • 引入并启用cssMarkPreset插件

  • 在Vue编译选项中添加cssMarkNodeTransform转换器

  • 确保cssMarkPreset在vue插件之前注册

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { cssMarkNodeTransform, cssMarkPreset } from './vite-plugins/css-mark';

export default defineConfig({
  plugins: [
    // 其他插件...
    cssMarkPreset(),
    vue({
      template: {
        compilerOptions: {
          nodeTransforms: [
            cssMarkNodeTransform,
          ],
        },
      },
    }),
    // 其他插件...
  ],
  // 其他配置...
});

6. 实现浏览器端交互脚本

这里面的代码比较长,大家去我的项目里面查看即可,接下来只对一些重要模块贴一下代码以及作简要的解释,重点放在理解怎么实现即可~~😌😌😌

关键点解释:

  • 添加一个悬浮的检查按钮

  • 使用LZString.decompressFromBase64解码压缩的文件路径

  • 高亮显示所有带有css-vite-mark-类名的元素

  • 点击元素时,收集并显示组件层次结构

  • 提供复制路径和在VSCode中打开文件的功能

  • 允许配置项目根路径

相关核心代码:

  1. 元素识别与查找

这部分代码负责在DOM中搜索带有css-vite-mark-前缀的类名,这些类名包含了压缩过的源文件路径。

// 查找带有css-mark类名的元素
function getCssMarkClassElement(doc) {
    const elements = [];
    const allElements = doc.querySelectorAll('*');
    for (let i = 0; i < allElements.length; i++) {
        const element = allElements[i];
        if (getCssMarkClass(element)) {
            elements.push(element);
        }
    }
    return elements;
}

// 获取元素上的css-mark类名
function getCssMarkClass(element) {
    const classList = element.classList;
    for (let i = 0; i < classList.length; i++) {
        const className = classList[i];
        if (className.startsWith('css-vite-mark-')) {
            return className;
        }
    }
    return '';
}
  1. 组件层次结构收集

这个函数从点击的元素开始向上遍历DOM树,收集所有带有标记的父元素,构建组件层次结构。

// 收集元素层次结构中的css-mark
function collectCssMarkHierarchy(element) {
    const cssMarkList = [];
    let currentElement = element;
    
    while (currentElement) {
        const cssClassName = getCssMarkClass(currentElement);
        if (cssClassName) {
            cssMarkList.push({ 
                element: currentElement, 
                originMark: cssClassName 
            });
        }
        currentElement = currentElement.parentElement;
    }
    
    return cssMarkList;
}
  1. 路径解码与显示

这部分代码从类名中提取压缩的路径部分,然后使用LZString.decompressFromBase64解码还原为实际文件路径。

// 处理源码路径部分代码
cssMarkList.forEach(item => {
    const tag = item.element.tagName.toLowerCase();
    try {
        const encodedPath = item.originMark.substring(prefix.length);
        const filePath = LZString.decompressFromBase64(encodedPath);
        decodedPaths.push({ tag, filePath });
    } catch (e) {
        console.error('解码路径失败:', e);
    }
});
  1. 交互处理机制

这部分代码监听Shift+点击事件,当用户按住Shift键点击元素时,收集组件层次并显示对话框。

// 处理元素点击事件
function handleElementClick(event) {
    if (event.shiftKey) {
        event.preventDefault();
        event.stopPropagation();
        
        const cssMarkList = collectCssMarkHierarchy(event.target);
        if (cssMarkList.length > 0) {
            createDialog(cssMarkList);
        } else {
            console.log('未找到带有css-mark的元素');
        }
    }
}

// 激活检查功能
function activateInspection() {
    highlightCssMarkElements();
    document.addEventListener('click', handleElementClick, true);
}

效果图:

1746512134866.png

7. 测试与验证

  1. 启动开发服务器:

  2. 在浏览器中打开你的应用

    可以看看上面第一张图,就是启动脚本后的效果

  3. 点击悬浮的"启动 Inspect"按钮启用检查模式

    每一条红框包含的内容都是一个独立的组件,单击之后就可以弹窗显示这个组件对应的源码路径及其父级组件

1746513039036.png

  1. 点击页面上的元素,查看对话框中显示的组件层次结构和对应的源码路径

1746509402199.png

Kim 2025-05-06 143544.png

  1. 点击"复制"按钮并在在编辑器中搜索对应的源文件

    各位小伙伴可以扩展一下,如何实现定位到对应组件后直接打开编辑器,只是这个话题不在本次的博客讨论范围~~,我们复制之后在编辑器中使用command + P 搜索,刚刚复制的值,点击回车即可,源码定位速度也是杠杠的😜😜😜

1746513600721.png

1746513651766.png

8. 调试与问题排查

如果你遇到问题,可以尝试以下调试方法:

  1. 检查浏览器控制台是否有错误信息

  2. 确认css-mark属性是否正确注入到Vue组件中:

    • 在浏览器中查看元素,检查是否有css-mark属性

    • 可以在构建阶段添加更多调试日志

  3. 确认css-vite-mark-类名是否正确添加:

    • 在浏览器中查看元素类列表

    • 可以在编译阶段添加更多调试日志

  4. 如果Tampermonkey脚本无效,检查匹配规则是否正确:

    • 尝试手动激活脚本

    • 确认@match规则与你的应用URL匹配,注意你想要让这个自定义脚本在哪个页面生效就一定要配置网页的URL,如下:

1746516361225.png

总结

通过上述的过程其实就可以实现一个基本的源码定位系统了,小编个人认为实用性还是比较高的,不过这个功能还有很多地方可以优化一下,比如:

  1. 便捷性层面:定位到对应组件后支持一键打开到编辑器的对应组件
  2. 扩展性问题:对于webpack等其他构件工具如何使用,以及要如何适配到react等其他框架中,或者这个工具如何实现与框架解耦
  3. 实现方式上:可以考虑一下不使用油猴脚本怎么实现,还有不通过给根组件添加样式名去识别dom的方式能不能实 现,就是考虑有没有更加简洁的实现方式呢
  4. 样式结构上:小编觉得我这个选中后的样式还是有点丑的,不过可以通过去脚本里面去调整样式,但是小编没有这个绘制美的细胞哈哈~~,留给全能的网友吧😌😌😌
  5. 正向映射层面:我们只是通过标识去反向映射出源码组件,如果别的开发者在项目中想正向找一下这个组件在页面中哪些地方使用到了要怎么实现呢...
  6. 细节处理方面:可以区分一下常规的业务组件,弹窗组件以及一些hover展示的组件等等,细分一下组件类别

受篇幅影响,上述的优化方向就不再赘述,而且所谓的优化方向也仅仅代表我个人的想法,不能保证没有问题,这部分欢迎大家发表意见~~

以上就是小编这次的知识分享啦,大家可以理解这个实现过程的话,小编这篇博客就产生了它的意义喽,不用去纠结某个模块的问题,因为可能是小编描述的问题,给大伙带来了歧义,而且我只是从我个人在企业项目中的实现中抽离出核心部分分享一番,所以也欢迎大佬在评论区发表您的高见~~👏👏👏