前言
大家好,我是webfansplz.这两天肝了个Vite插件,本文次要跟大家分享一下它的性能和实现思路.如果你感觉它对你有帮忙,请给一个star反对作者 💗.
介绍
vite-plugin-vue-inspector的性能是点击页面元素,主动关上本地IDE并跳转到对应的Vue组件.相似于Vue DevTools
的 Open component in editor
性能.
用法
vite-plugin-vue-inspector反对Vue2 & Vue3,并且只须要进行简略的配置就能够应用.
Vue2
// vite.config.ts
import { 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.ts
import { 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.ts
function 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.ts
import 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 => AST
const 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.ts
function 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.query
if (!file) {
res.statusCode = 500
res.end("launch-editor-middleware: required query param \"file\" is missing.")
}
const lineNumber = +line || 1
const 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.ts
function 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
等源码排查解决.这里给想看源码的童鞋一个倡议,从实际和带着问题的角度登程,兴许会有更好的成果和更粗浅的印象 (教训) 🙂
===,先别跑,点个star再走,感激老铁. 💗
发表回复