4

点击页面元素,这个Vite插件帮我打开了Vue组件 🚀

 2 years ago
source link: https://segmentfault.com/a/1190000041578690
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

点击页面元素,这个Vite插件帮我打开了Vue组件 🚀

大家好,我是webfansplz.这两天肝了个Vite插件,本文主要跟大家分享一下它的功能和实现思路.如果你觉得它对你有帮助,请给一个star支持作者 💗.

vite-plugin-vue-inspector的功能是点击页面元素,自动打开本地IDE并跳转到对应的Vue组件.类似于Vue DevToolsOpen component in editor功能.

vite-plugin-vue-inspector.gif

vite-plugin-vue-inspector支持Vue2 & Vue3,并且只需要进行简单的配置就可以使用.

// 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,
    }),
  ],
})
// 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-plugin-step.drawio (2).png

接下来,我们来看具体的实现细节.在这之前,我们先简单看下我们需要用到的几个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再走,感谢老铁. 💗


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK