点击页面元素,这个Vite插件竟然帮我打开了Vue组件文件!超级好用!

大家好,我是若川。持续组织了8个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列

前言

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

介绍

vite-plugin-vue-inspector的功能是点击页面元素,自动打开本地IDE并跳转到对应的Vue组件.类似于Vue DevToolsOpen component in editor功能。(若川批注:关于原理可以看我写过的文章:据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘

29afeed3e991a9d018ea4794988d70e0.png

用法

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打开哪个文件并定位到对应的行列.

明确我们需要什么元素,我们就可以进一步来梳理它的实现方式,直接晒图:

4a9907aaedf8b53b9f12e14cff4d4fab.png
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等源码排查解决.这里给想看源码的童鞋一个建议,从实践和带着问题的角度出发,也许会有更好的效果和更深刻的印象 (教训) :)

858d2be00aa7548a2abc0bf9ca8b7697.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

f43695f7f26369d36a6f9db8b2d4940a.png

扫码加我微信 ruochuan02、拉你进源码共读

今日话题

略。分享、收藏、点赞、在看我的文章就是对我最大的支持~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
非常抱歉,我误解了你的问题。以下是一些常用的 Vite 插件,可以用于 Vite 项目: 1. `vite-plugin-vue-layouts`:可以助你更方便地实现页面布局。 2. `vite-plugin-svg`:可以助你更方便地使用 SVG 图标。 3. `vite-plugin-pwa`:可以助你将应用转化为一个渐进式 Web 应用。 4. `vite-plugin-components`:可以助你更方便地导入组件。 5. `vite-plugin-windicss`:可以助你更方便地使用 Tailwind CSS。 6. `vite-plugin-md`:可以助你更方便地使用 Markdown 文件。 7. `vite-plugin-eslint`:可以助你集成 ESLint。 8. `vite-plugin-style-import`:可以助你更方便地导入第三方样式库。 9. `vite-plugin-optimize-lodash`:可以助你更方便地使用 Lodash 库。 10. `vite-plugin-cdn-import`:可以助你更方便地导入 CDN 资源。 11. `vite-plugin-imp`:可以助你更方便地导入第三方库。 12. `vite-plugin-svg-sprite`:可以助你更方便地使用 SVG Sprite。 13. `vite-plugin-externals`:可以助你更方便地使用外部依赖。 14. `vite-plugin-compression`:可以助你更方便地压缩代码。 15. `vite-plugin-virtual-modules`:可以助你更方便地使用虚拟模块。 16. `vite-plugin-style-resources-loader`:可以助你更方便地共享样式资源。 17. `vite-plugin-define`:可以助你更方便地定义全局变量。 18. `vite-plugin-html`:可以助你更方便地定制 HTML 模板。 19. `vite-plugin-ssr`:可以助你更方便地实现服务器端渲染。 20. `vite-plugin-mock`:可以助你更方便地模拟数据。 以上是一些常用的 Vite 插件,可以根据你的具体需求选择安装。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值