前几天改一个老项目,需要对页面进行简单调整,花了五分钟才找到它是哪个组件渲染的。这种事儿不是第一次了。
现代前端项目都是组件套组件,页面上的 DOM 和源码文件之间没有直接对应关系。每次要定位一个元素,基本就是 DevTools 看 class、猜组件名、编辑器里搜字符串,来回折腾。
于是我写了 DOM XRay:一个开发模式下运行的插件,按住快捷键点击页面元素,就能弹出源码面板,或者直接打开编辑器跳到对应文件和行号。
它支持 Vite、Webpack、Rspack、Next.js、Nuxt 3 和 Angular。
实际效果
装好插件后,页面加载完控制台会打印这样一段提示:
vbnet
DOM XRay is active
Usage:
Hold Option + click → Open overlay to inspect source
Hold Option + Command + click → Open directly in VSCode
Press Esc while hovering → Cancel inspect mode
按住 Option 点击按钮,弹窗左侧直接显示这个按钮的源码文件和行号;再按一下"打开",编辑器就跳过去了。如果按住 Option + Command 点击,连弹窗都不出现,直接打开编辑器。
核心思路:把源码位置写进 DOM
DOM XRay 的做法并不复杂------在编译阶段把 data-source="filePath:line" 注入到每个元素上。运行时点击元素,向上找最近的 data-source,就能拿到源码位置。
真正麻烦的是怎么在不同框架里做这件事。
JSX / TSX
用 Babel 解析 AST,遍历 JSXOpeningElement,追加一个属性:
js
node.attributes.push(
t.jsxAttribute(
t.jsxIdentifier("data-source"),
t.stringLiteral(`${filePath}:${line}`)
)
);
然后再用 @babel/generator 输出。React、SolidJS、Vue 的 JSX 都这么处理。
Vue 3 SFC
Vue 单文件组件的 template 有自己的编译流程。我用 @vue/compiler-sfc 拿到 template AST,再用 htmlparser2 把属性插回去。
Svelte
Svelte 的模板语法不太一样,用 svelte/compiler 解析后用 magic-string 做精准插入,避免破坏原有格式。
Nuxt 3
Nuxt 3 本质还是 Vue,但更深层。我在 Vue 编译器的 nodeTransforms 阶段注入属性,这样所有页面模板都会自动带上 data-source。
Angular
Angular 的模板是独立 HTML 文件,编译器读取模板时才能处理。我用了一个不太优雅但有效的办法:monkey-patch fs.readFileSync,在 Angular 编译器读取 HTML 模板之前把属性注入进去。
项目结构
代码用 pnpm workspace 管理:
bash
packages/
├── core # 配置加载 + 源码转换
├── overlay-ui # Web Components 弹窗
├── vite # Vite 插件
├── webpack # Webpack 5 插件
├── rspack # Rspack 插件
├── nextjs # Next.js 插件
├── nuxt # Nuxt 3 模块
└── angular # Angular 支持
core 负责所有框架的转换逻辑,各适配器只负责接入自己的构建流程。
几个有意思的实现细节
Web Components 弹窗
弹窗用原生 Web Components 实现,挂在 Shadow DOM 里。这样样式完全隔离,不会和用户页面的 CSS 互相影响。毕竟谁也不希望自己的调试工具被业务样式顶歪。
向上查找
点击的往往是按钮里的文字、卡片里的标题这种深层子元素,它自己不一定有 data-source。所以点击时我会从目标元素开始向上遍历,找到最近的带 data-source 的父元素。这样基本不会点空。
编辑器跳转
弹窗里的"打开"按钮会根据配置生成对应的 URL:
js
vscode://file/Users/.../Button.tsx:14
cursor://file/Users/.../Button.tsx:14
用 window.open 打开就行。浏览器会唤起对应的编辑器并定位到行号。
Next.js 16 + Turbopack 的坑
Next.js 16 默认开 Turbopack,而 Turbopack 的 webpack loader 兼容不是对所有文件生效,尤其是 App Router 的 Server Components。
我试了几种方案,最后决定在 @dom-xray/core 的 Babel transform 里加一个 scriptContent 选项:遇到 layout.tsx 时,不只是在模块里插一段副作用代码,而是直接把 init 脚本作为 <script dangerouslySetInnerHTML> 插到 <body> 里。
因为 RSC 的模块代码在服务端运行,副作用代码不会真正在浏览器执行。但 <body> 里的 <script> 会被 SSR 渲染到 HTML 里,浏览器解析时自然会执行。
这样用户在 Next.js App Router 项目里完全不用手动改 layout.tsx,配置好 next.config.js 就行。
怎么用
Vite:
ts
import domXray from "@dom-xray/vite";
export default defineConfig({
plugins: [domXray({ editor: "vscode" })],
});
Next.js:
js
const { withDomSelector } = require("@dom-xray/nextjs");
module.exports = withDomSelector(
{ reactStrictMode: true },
{ editor: "vscode" }
);
也可以写 dom-xray.config.json:
json
{
"hotkey": { "mac": "option", "win": "alt" },
"editor": "vscode",
"enabled": true
}
还加了 AI Agent
弹窗右侧有一个输入面板,可以把当前元素的源码和提问一起发给 LLM。目前支持 Cursor、OpenCode 和 Claude Code,返回结果是 SSE 流式显示。
这个功能的出发点很简单:定位到源码之后,下一步往往是"这段代码是干什么的"或者"怎么改"。如果能直接在弹窗里问 AI,就不用再复制粘贴了。
写在最后
DOM XRay 不是那种解决大问题的工具,它解决的是一个很小的痛点:页面和源码之间的跳转。
写完之后我自己先用了一段时间,确实省了不少事。如果你也经常被这个问题烦到,可以试试看。
GitHub: github.com/ALittleFox/...