vite-plugin-vue-devtools 点击跳转打开编辑器源码的实现原理
2026/1/9大约 3 分钟
自己手写实现一个devtool选择组件跳转打开编辑器源码的功能。
核心实现原理
经常开发Vue的朋友肯定都知道 vite-plugin-vue-devtools 是一个增强 Vue 开发体验的 Vite 插件,它通过在浏览器中嵌入一个可视化调试面板来工作,它的原理大概是:
- Vue 组件编译时保留 __file 属性:component.type.__file = '/src/components/Hello.vue'
- Vue 在 DOM 元素上挂载组件实例引用:element.__vueParentComponent = componentInstance
- 客户端通过 DOM 找到组件实例,获取 __file:const file = el.__vueParentComponent.type.__file
- 发送请求到 Vite 服务器:fetch('/__open-in-editor?file=xxx'),浏览器是沙箱环境,出于安全考虑无法直接操作本地文件系统或启动本地程序,所以需要通过 HTTP 请求让 Node.js 服务端来打开编辑器。
- 服务器使用 launch-editor 打开编辑器:launchEditor(filePath, line, column)
自己实现一个最小的demo
Vue 官方的 vite-plugin-vue-devtools 内置了这个功能。但我自己写一遍,就像是亲手做了一把锤子,虽然买一把更便宜,但只有当你亲手挥动它砸向钉子的那一刻,你才明白:“原理这东西,懂了才是自己的,不懂,永远只是个调包侠。”
package.json
{
"name": "vue-click-to-source",
"type": "module",
"scripts": {
"dev": "vite"
},
"dependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"vite": "^5.0.0",
"@vitejs/plugin-vue": "^5.0.0",
"launch-editor": "^2.6.1"
}
}plugins/click-to-source.js
import launchEditor from 'launch-editor'
export default function clickToSource() {
return {
name: 'click-to-source',
// 配置开发服务器,添加 API 路由
configureServer(server) {
server.middlewares.use('/__open-in-editor', (req, res) => {
const url = new URL(req.url, 'http://localhost')
const file = url.searchParams.get('file')
if (file) {
console.log(`\n📂 Opening: ${file}\n`)
// 调用 launch-editor 打开编辑器
launchEditor(file)
}
res.end('OK')
})
},
// 注入客户端脚本
transformIndexHtml() {
return [
{
tag: 'script',
attrs: { type: 'module' },
children: CLIENT_SCRIPT,
injectTo: 'body'
}
]
}
}
}
const CLIENT_SCRIPT = `
;(function() {
let enabled = false
let overlay = null
// 从 DOM 元素获取 Vue 组件的源文件路径
function getComponentFile(el) {
// Vue 3 在 DOM 元素上挂载了 __vueParentComponent
let instance = el.__vueParentComponent
while (instance) {
// Vue 组件在开发模式下有 __file 属性
if (instance.type?.__file) {
return {
name: instance.type.__name || 'Component',
file: instance.type.__file
}
}
instance = instance.parent
}
return null
}
// 创建高亮层
function showOverlay(el, name) {
if (!overlay) {
overlay = document.createElement('div')
overlay.style.cssText = 'position:fixed;background:rgba(66,184,131,0.35);border:2px solid #42b883;pointer-events:none;z-index:999999;transition:all 0.1s'
const label = document.createElement('div')
label.style.cssText = 'position:absolute;top:-22px;left:0;background:#42b883;color:#fff;padding:2px 6px;font:12px monospace;border-radius:3px'
overlay.appendChild(label)
document.body.appendChild(overlay)
}
const rect = el.getBoundingClientRect()
Object.assign(overlay.style, {
left: rect.left + 'px',
top: rect.top + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
display: 'block'
})
overlay.firstChild.textContent = '<' + name + '>'
}
function hideOverlay() {
if (overlay) overlay.style.display = 'none'
}
// 事件处理
function onMouseMove(e) {
const info = getComponentFile(e.target)
if (info) {
showOverlay(e.target, info.name)
e.target._file = info.file
} else {
hideOverlay()
}
}
function onClick(e) {
const file = e.target._file || getComponentFile(e.target)?.file
if (file) {
e.preventDefault()
e.stopPropagation()
// 👇 发送请求到 Vite 服务器,让服务器打开编辑器
fetch('/__open-in-editor?file=' + encodeURIComponent(file))
}
}
// 切换检查模式
function toggle() {
enabled = !enabled
document.body.style.cursor = enabled ? 'crosshair' : ''
if (enabled) {
document.addEventListener('mousemove', onMouseMove, true)
document.addEventListener('click', onClick, true)
console.log('🔍 Inspector ON - click component to open source')
} else {
document.removeEventListener('mousemove', onMouseMove, true)
document.removeEventListener('click', onClick, true)
hideOverlay()
console.log('🔍 Inspector OFF')
}
}
// 快捷键 Alt+Shift+C
document.addEventListener('keydown', e => {
if (e.altKey && e.shiftKey && e.key === 'C') toggle()
})
// 添加按钮
const btn = document.createElement('button')
btn.textContent = 'Dev'
btn.title = 'Toggle Inspector (Alt+Shift+C)'
btn.style.cssText = 'position:fixed;bottom:20px;right:20px;width:40px;height:40px;border-radius:50%;border:none;background:#42b883;font-size:18px;cursor:pointer;z-index:999999;box-shadow:0 2px 8px rgba(0,0,0,.2)'
btn.onclick = toggle
document.body.appendChild(btn)
console.log('✨ Click-to-Source ready! Press Alt+Shift+C')
})()
`在vite.config.js中添加插件:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import clickToSource from './plugins/click-to-source.js'
export default defineConfig({
plugins: [
vue(),
clickToSource()
]
})工作流程
- 按 Alt+Shift+C 开启检查模式
↓ - 鼠标悬停组件 → 高亮显示
↓ - 点击组件
↓ - 客户端: fetch('/__open-in-editor?file=/src/components/HelloWorld.vue')
↓ - Vite服务器: 收到请求,调用 launchEditor()
↓ - VS Code 打开 HelloWorld.vue 文件
实际效果
运行启动后
选择组件后跳转到vscode中的组件位置