大家好,我是若川。持续组织了8个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列
前言
这两天肝了个Vite插件,本文主要跟大家分享一下它的功能和实现思路.如果你觉得它对你有帮助,请给一个star支持作者 💗.
介绍
vite-plugin-vue-inspector的功能是点击页面元素,自动打开本地IDE并跳转到对应的Vue组件.类似于Vue DevTools
的 Open component in editor
功能。(若川批注:关于原理可以看我写过的文章:据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘)
用法
vite-plugin-vue-inspector支持Vue2 & Vue3,并且只需要进行简单的配置就可以使用.
Vue2
// vite.config.tsimport { defineConfig } from "vite"
import { createVuePlugin } from "vite-plugin-vue2"
import Inspector from "vite-plugin-vue-inspector"export default defineConfig({plugins: [createVuePlugin(),Inspector({vue: 2,}),],
})
Vue3
// vite.config.tsimport { defineConfig } from "vite"
import Vue from "@vitejs/plugin-vue"
import Inspector from "vite-plugin-vue-inspector"export default defineConfig({plugins: [Vue(), Inspector()],
})
IDE也要进行配置,这里就不啰嗦了, 👉 传送门.
实现思路
看到这里,如果你觉得这个插件索然无味的话先别跑,插件没意思,看看怎么写插件还是有点意思的嘛 ! 接下来跟大家介绍一下这个插件的实现思路.
我们先来分析一下实现这个功能我们需要有哪些元素 :
Open IDE
: 打开编辑器功能.Web
层: 提供该功能所需的页面元素及交互功能.Server
层: 用户交互时传递数据到Server
层,由Server
层调用Open IDE
功能.DOM
=>Vue SFC
映射关系: 告诉OPen IDE
打开哪个文件并定位到对应的行列.
明确我们需要什么元素,我们就可以进一步来梳理它的实现方式,直接晒图:
实现细节
接下来,我们来看具体的实现细节.在这之前,我们先简单看下我们需要用到的几个Vite插件API:
function VitePluginInspector(): Plugin {return {name: "vite-plugin-vue-inspector",// 应用顺序enforce: "pre",// 应用模式 (只在开发模式应用)apply: "serve",// 含义: 转换钩子,接收每个传入请求模块的内容和文件路径// 应用: 在这个钩子对SFC模版进行解析并注入自定义属性transform(code, id) {},// 含义: 配置开发服务器钩子,可以添加自定义中间件// 应用: 在这个钩子实现Open Editor调用服务configureServer(server) {},// 含义: 转换index.html的专用钩子,接收当前HTML字符串和转换上下文// 应用: 在这个钩子注入交互功能transformIndexHtml(html) {},}
}
解析SFC模版 & 注入自定义属性
这部分的实现主要分为两步:
SFC Template
=>AST
获取元素所在组件的行和列的编号
获取自定义属性插入的位置
注入自定义属性
file (SFC路径,用于跳转到指定文件)
line (元素所在行编号,用于跳转到指定行)
column (元素所在列编号,用于跳转到指定列)
title (SFC名称,用于展示)
// vite.config.tsfunction VitePluginInspector(): Plugin {return {name: "vite-plugin-vue-inspector",transform(code, id) {const { filename, query } = parseVueRequest(id)// 只处理SFC文件if (filename.endsWith(".vue") && query.type !== "style") return compileSFCTemplate(code, filename)return code},}
}
// compiler.tsimport path from "path"
import MagicString from "magic-string"
import { parse, transform } from "@vue/compiler-dom"const EXCLUDE_TAG = ["template", "script", "style"]export async function compileSFCTemplate(code: string,id: string,
) {// MagicString是一个非常好用的字符串操作库,也如它的名字一样,非常的神奇 !// 有了它,我们可以直接操作字符串,避免操作AST,换来更好的性能. Vue3的实现也大量的用到了它.const s = new MagicString(code)// SFC => ASTconst ast = parse(code, { comments: true })const result = await new Promise((resolve) => {transform(ast, {// ast node节点访问器nodeTransforms: [(node) => {if (node.type === 1) {// 只解析html标签 if (node.tagType === 0 && !EXCLUDE_TAG.includes(node.tag)) {const { base } = path.parse(id)// 获取到相关信息,并进行自定义属性注入!node.loc.source.includes("data-v-inspecotr-file")&& s.prependLeft(node.loc.start.offset + node.tag.length + 1,` data-v-inspecotr-file="${id}" data-v-inspecotr-line=${node.loc.start.line} data-v-inspecotr-column=${node.loc.start.column} data-v-inspecotr-title="${base}"`,)}}},],})resolve(s.toString())})return result
}
注入后的DOM元素
长这样 :
<h3 data-v-inspector-file="/xxx/src/Hi.vue" data-v-inspector-line="3" data-v-inspector-column="5" data-v-inspector-title="Hi.vue">
</h3>
Open Editor Server服务
前面我们提到了创建Server服务的思路是在vite的configureServer
的钩子函数注入中间件:
// vite.config.tsfunction VitePluginInspector(): Plugin {return {name: "vite-plugin-vue-inspector",configureServer(server) {// 注册中间件// 请求Query参数解析中间件 server.middlewares.use(queryParserMiddleware)// Open Edito服务中间件server.middlewares.use(launchEditorMiddleware)},}
}
// middleware.ts// 请求Query参数解析中间件
export const queryParserMiddleware: Connect.NextHandleFunction = (req: RequestMessage & {query?: object},_,next,
) => {if (!req.query && req.url?.startsWith(SERVER_URL)) {const url = new URL(req.url, "http://domain.inspector")req.query = Object.fromEntries(url.searchParams.entries())}next()
}// Open Editor服务中间件
export const launchEditorMiddleware: Connect.NextHandleFunction = (req: RequestMessage & {query?: { line: number; column: number; file: string }},res,next,
) => {// 只处理Open Editor接口if (req.url.startsWith(SERVER_URL)) {// 解析SFC路径,行号,列号const { file, line, column } = req.queryif (!file) {res.statusCode = 500res.end("launch-editor-middleware: required query param \"file\" is missing.")}const lineNumber = +line || 1const columnNumber = +column || 1// 见下方链接launchEditor(file, lineNumber, columnNumber)res.end()}else {next()}
}
关于launchEditor
的具体逻辑我直接fork了react-dev-utils的实现,它支持很多IDE (vscode
,atom
,webstorm
...),它的大致原理就是通过维护一些进程映射表和环境变量,然后通过调用Node.js
的子进程唤醒IDE:
child_process.spawn(editor, args, { stdio: 'inherit' });
交互功能注入
这个功能的实现原理其实就在transformIndexHtml
注入功能所需要的html,scripts,styles
.
// vite.config.tsfunction VitePluginInspector(): Plugin {return {transformIndexHtml(html) {return {html,tags: [{tag: "script",children: ...,injectTo: "body",}, {tag: "script",attrs: {type: "module",},children: scripts,injectTo: "body",}, {tag: "style",children: styles,injectTo: "head",}],}}}
}
关于交互的页面实现有很多种,最简单的无非就是编写原生js
,这样我们无需任何编译就可以直接注入到html
中,但是用原生js
来写页面真的是慢又不好维护,于是我选择了Vue
进行开发,使用Vue
就意味着要进行编译才能在浏览器中跑起来.为了这个所谓的研发体验,又折腾了一波,大概过程就是通过compile-sfc
等包编译出render函数,样式代码等
,为了兼容Vue2
,我又引入了祖传的vue-template-compiler
...噼里啪啦噼里啪啦..感兴趣的童鞋可以点传送门详看. (u1s1,还是有点意思的!!) 当然了,这部分的编译都是在插件打包时完成的,用户在使用插件的时候并不会有这部分的运行时开销.
致谢
这个项目的灵感来自于react-dev-inspector,使用React
的童鞋可以看看.
结语
在做这个插件的时候也踩了一些坑,通过查看vue,vite
等源码排查解决.这里给想看源码的童鞋一个建议,从实践和带着问题的角度出发,也许会有更好的效果和更深刻的印象 (教训) :)
················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。
扫码加我微信 ruochuan02、拉你进源码共读群
今日话题
略。分享、收藏、点赞、在看我的文章就是对我最大的支持~