preface

Hello everyone, I am WebFansplz. These two days hepatic a Vite plug-in, this article mainly shares with you its function and implementation idea. If you find it helpful, please send a star support author to 💗.

introduce

Viet-plugin-vue-inspector automatically opens the local IDE and jumps to the corresponding VUE component by clicking on a page element. Similar to Vue DevTools’ Open Component in Editor feature.

usage

Viet-plugin-vue-inspector supports Vue2 & Vue3 and requires 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,})]})Copy the code

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()],
})

Copy the code

The IDE is also configured, without further ado here, 👉 portal.

Implementation approach

If you find this plugin boring, don’t run away, plugins are boring, see how to write plugins or some interesting! Let’s take a look at the implementation of this plugin.

Let’s first examine what elements we need to implement this feature:

  • Open IDE: Enables the editor function.
  • WebLayer: Provides the page elements and interactions required for this functionality.
  • ServerLayer: When the user interacts, data is transferred toServerLayer, byServerLayer callsOpen IDEFunction.
  • DOM= >Vue SFCMapping: tellOPen IDEWhich file to open and navigate to the corresponding row and column.

Knowing what elements we want, we can go a step further and tease out how it is implemented, directly:

Implementation details

Next, let’s look at the implementation details. Before we do that, let’s take a quick look at some of the Vite plugin apis we need to use:


function VitePluginInspector() :Plugin {
  return {
    name: "vite-plugin-vue-inspector".// Application sequence
    enforce: "pre".// Apply mode (apply only in development mode)
    apply: "serve".// Convert the hook to receive the contents and file paths of each incoming request module
    // Apply: This hook is used to parse the SFC template and inject custom properties
    transform(code, id){},// Configures the development server hooks that can add custom middleware
    // Application: Implement the Open Editor call service in this hook
    configureServer(server){},// Meaning: a special hook for converting index. HTML to receive the current HTML string and converting context
    // Apply: Inject interactivity into this hook
    transformIndexHtml(html){},}}Copy the code

Parse SFC templates & inject custom properties

The implementation of this part is mainly divided into two steps:

  • SFC Template= >AST
    • Gets the number of the row and column of the component in which the element resides
    • Gets the location where the custom property is inserted
  • Inject custom properties
    • File (SFC path, used to jump to a specified file)
    • Line (number of the element’s row to jump to the specified line)
    • Column (number of the column in which 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)
      // Only SFC files are processed
      if (filename.endsWith(".vue") && query.type ! = ="style") return compileSFCTemplate(code, filename)
      return code
    },
  }
}

Copy the 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 is a very useful string manipulation library, as its name, very magical!
  // With it, we can manipulate strings directly, avoiding the AST, for better performance. Vue3 implementations also make extensive use of it.
  const s = new MagicString(code)
  
  // SFC => AST
  const ast = parse(code, { comments: true })
  
  const result = await new Promise((resolve) = > {
    transform(ast, {
      // AST node accessor
      nodeTransforms: [
        (node) = > {
          if (node.type === 1) {
           // Only HTML tags are parsed
            if (node.tagType === 0 && !EXCLUDE_TAG.includes(node.tag)) {
              const { base } = path.parse(id)
              // Get the relevant information and perform custom attribute injection! 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
}

Copy the code

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>
Copy the code

Open Editor Server service

We mentioned earlier that the idea of creating a Server service is to inject middleware with the configureServer hook function in Vite:


// vite.config.ts

function VitePluginInspector() :Plugin {
  return {
    name: "vite-plugin-vue-inspector".configureServer(server) {
      // Register middleware
      
      // Request the Query parameter parsing middleware
      server.middlewares.use(queryParserMiddleware)
      // Open Edito server middleware
      server.middlewares.use(launchEditorMiddleware)
    },
  }
}

Copy the code
// middleware.ts

// Request the Query parameter parsing middleware
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 service middleware
export const launchEditorMiddleware: Connect.NextHandleFunction = (req: RequestMessage & { query? : { line:number; column: number; file: string }
  },
  res,
  next,
) = > {
    // Only the Open Editor interface is handled
  if (req.url.startsWith(SERVER_URL)) {
    // Resolve the SFC path, row number, column number
    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
    // See link below
    launchEditor(file, lineNumber, columnNumber)
    res.end()
  }
  else {
    next()
  }
}

Copy the code

For the logic of launchEditor, I directly fork the react-dev-utils implementation, which supports many ides (vscode,atom,webstorm…). The general idea is to wake up the IDE by maintaining some process mapping tables and environment variables, and then calling node.js child processes:

child_process.spawn(editor, args, { stdio: 'inherit' });
Copy the code

Interactive function injection

TransformIndexHtml injection requires HTML,scripts, and 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"}],}}}}Copy the code

Pages about the interaction to realize there are many, the most simple does not write native js, so we don’t need any compilation can be directly injected into the HTML, but written in native js to page is really slow and poor maintenance, so I chose the Vue for development, is compiled using the Vue means to run in the browser. For this so-called RESEARCH and development experience, I did another wave of thrashing, probably the process is to compile the render function, style code and so on through the compile-sFC package, in order to be compatible with Vue2, I also introduced the family vue-template-compiler… Crackle crackle crackle… If you are interested, please click on the portal to have a closer look. Of course, this part of the compilation is done when the plug-in is packaged, and there is no runtime overhead associated with using the plug-in.

Thank you

This project was inspired by the React -dev-inspector. Check out the react shoes.

conclusion

When doing this plug-in also stepped on some pits, by viewing vue, Vite and other source checks to solve. Here is a suggestion for those who want to see the source code, from the point of view of practice and problems, perhaps there will be better results and more profound impression (lesson) 🙂

===, don’t run first, point a star to walk again, thank old tie. 💗