12
头图

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 .

vite-plugin-vue-inspector.gif

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 the Server layer, and the Open IDE function is called by the Server layer.
  • DOM => Vue SFC mapping relationship: tell OPen 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:

vite-plugin-step.drawio (2).png

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. 💗


null仔
4.9k 声望3.7k 粉丝

总是有人要赢的,那为什么不能是我呢