foreword
Hello everyone, I'm webfansplz . I have a Vite plugin in the past two days. This article mainly shares its functions and implementation ideas with you. If you think it is helpful to you, please give a star to support the author💗.
introduce
vite-plugin-vue-inspector 's function is to click a page element, automatically open the local IDE and jump to the corresponding Vue component. Similar to the Vue DevTools
function of Open component in editor
.
usage
vite-plugin-vue-inspector supports Vue2 & Vue3, and can be used with simple configuration.
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()],
})
The IDE also needs to be configured, so I won't be verbose here, 👉 Portal .
Realize ideas
Seeing this, if you think this plug-in is boring, don't run it first. The plug-in is boring. It's interesting to see how to write a plug-in! Next, I will introduce the implementation idea of this plug-in to you.
Let's first analyze what elements we need to implement this function:
Open IDE
: Enable editor function.Web
layer: Provides page elements and interactive functions required for this function.Server
layer: When the user interacts, the data is passed to theServer
layer, and theOpen IDE
function is called by theServer
layer.DOM
=>Vue SFC
mapping relationship: tellOPen IDE
which file to open and locate the corresponding row and column.
After clarifying what elements we need, we can further sort out its implementation and directly post pictures:
implementation details
Next, let's look at the specific implementation details. Before that, let's take a brief look at several Vite plugin APIs we need to use:
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) {
},
}
}
Parse SFC templates & inject custom properties
The implementation of this part is mainly divided into two steps:
SFC Template
=>AST
- Get the row and column numbers of the component where the element is located
- Get the position where the custom attribute is inserted
Inject custom properties
- file (SFC path, used to jump to the specified file)
- line (the line number where the element is located, used to jump to the specified line)
- column (the column number where the element is located, used to jump to the specified column)
- title (SFC name, for display)
// 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
}
The injected DOM element looks like this:
<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 service
Earlier we mentioned that the idea of creating a Server service is to inject middleware into the hook function of vite's 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()
}
}
Regarding the specific logic of launchEditor
, I directly forked the implementation of react-dev-utils , which supports many IDEs ( vscode
, atom
, webstorm
...), and its general principle is to maintain some process mapping tables and environment variables, and then Wake up the IDE by calling the child process of Node.js
:
child_process.spawn(editor, args, { stdio: 'inherit' });
Interactive function injection
The realization principle of this function is actually in the html,scripts,styles
required by the transformIndexHtml
injection function.
// 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",
}],
}
}
}
}
There are many kinds of interactive page implementations. The simplest is to write native js
, so that we can directly inject it into html
without any compilation, but using native js
to write pages is really slow and difficult to maintain, so I choose I used Vue
for development, and using Vue
means that it needs to be compiled before it can run in the browser. For this so-called R&D experience, I have tossed around again, probably the process is to compile the render function, style code, etc. through
compile-sfc
such as 06237ce2261680 , In order to be compatible with Vue2
, I have introduced the ancestral vue-template-compiler
... crackling crackling crackling crackling.. Interested children's shoes can click portal details. (u1s1, it is still interesting!!) Of course, this part of the compilation is all It is done when the plug-in is packaged, and users will not have this part of the runtime overhead when using the plug-in.
Thanks
This project is inspired by react-dev-inspector , use React
children's shoes to see.
Epilogue
When I made this plug-in, I also stepped on some pits, and solved it by checking the source code such as vue,vite
. Here is a suggestion for children's shoes who want to see the source code. From the perspective of practice and problems, there may be better results and more profound Impressions ( lessons ) :)
===, don't run, click star and then go, thank you old iron. 💗
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。