巧妙搞掂自定义vue-devtools的文件打开地址

207 阅读6分钟

前言

我们都知道浏览器插件 vue-devtools 上有个实用的功能,点击这里可以快速打开你本地的编码IDE对应这个vue组件所在的文件地址。

image.png

可适用于快速打开文件修改代码,以及对项目代码不是很熟悉的开发者快速打开对应文件的一个有利神器。

(如果你不知道,那不巧了,看我的文章又学习到了!)

但是这篇文章不是教你怎么用这个功能,怎么让你的项目接入这个功能,毕竟这些官网都已经有介绍了,互联网上也有很多类似的文章。 官网教程

这篇文章是让你更进阶地控制这个打开文件的所在地址。它更适用于以下项目人群:

  • 你需要动态地控制打开文件的url
  • 官方默认的打开行为无法满足你的需求
  • 你是 monorepo 项目,如果在加上是微前端,可能你目前这个工具的的打开文件功能已经失效了。

vue-devtools-editor-url

一上来直接丢出我处理好的解决方案,后面我再跟大家细说,为了让有些小伙伴着急想直接看结论的。

我封装了一个vue-devtools的插件脚本—— vue-devtools-editor-url

使用这个npm包,你可以直接修改vue-devtools上打开文件的地址。

通过包管理工具如npm, pnpm, yarn等下载本插件

pnpm add vue-devtools-editor-url -D

Usage

import { setupDevtools } from 'vue-devtools-editor-url'

// 先创建vue应用的实例
const app = new Vue({
 ...
})

// 在创建了vue实例后

// setupDevtools方法接受两个入参:
// - app. vue应用的实例
// - url. 指定的打开url,它可以是一个字符串,也可以是一个方法(适合动态url)

setupDevtools(app, 'your-custom-open-editor-url')
// 结果发起的最终请求会变成 "/__open-in-editor?file=your-custom-open-editor-url"

// 使用该插件更多是为了解决动态url的场景,所以你可以传一个方法,方法里的回调参数是一个点击devtool原本的url
setupDevtools(app, function (originUrl) {
    return originUrl + '/add-something'
})
// 结果发起的最终请求会变成 `/__open-in-editor?file=${originUrl}/add-something`

前因后果

上面丢出了方便大家实现的工具了。那么这里就聊聊,实现这个工具,或者说了为了解决什么问题,我决定实现这个工具,期间又遇到什么问题,作了什么思考。

也许下面的经历跟你目前所经受的问题不一样,但是解决问题的思路或许可以借鉴,也或者给予你灵感,但是最终解决问题的能力是相通的,反正能解决你想要自定义url的目的。

背景

我在一个用qiankun实现的微应用monorepo多工程项目中开发的,本地启动项目的时候,会一下子构建启动这个monorepo里所有子工程的本地服务,然后我访问项目是通过访问其中一个子项目本地服务器地址的,这个子项目工程就是微应用的主工程。

那在这样的工程背景下,发现浏览器上的vue-devtools工具打开组件文件的功能时而好时而失效。为了解决这个问题,我决定一探究竟。

问题现状

经过观察,发现以下问题表现:

  1. 作为主工程的组件,进行打开文件操作时,是可以正确被打开的;
  2. 但是非主工程的组件,打开结果存在两种——打不开 和 打开错误的文件
  3. 主工程引用了非写在主工程项目目录下的组件,也是无法被打开 或 打开错误

原因分析

Devtool 打开文件原理

Devtool 上的打开路径,本质上是发送一个请求:当前域名 /__open-in-editor?file=xxx 。 而谁来处理这个请求,就是项目工程启动的本地服务器来监听处理的,当监听到 __open-in-editor 开头的请求,会调用一些工具包来打开本地 IDE 指定 file 路径的文件。

现在很多构建工具,如 webpackrsbuild,会内置了一些工具或逻辑实现这个监听操作,但是旧版本或某些构建工具可能不具备这个能力,那么可以按照官网所述的自己写中间件使用launch-editor-middleware实现。

我的项目中

而回到我的项目中,正如背景所述,我会启动多个子工程项目本地服务,然后是通过访问主工程所在的本地服务 ip 达到访问整个项目网站的,在devtools的面板上能看到主应用和多个微应用的 app,我们点击不同的 app 进入到不能 app 的组件审查面板中。

不论我们审查哪个微应用上的组件,点击打开文件按钮,都是发主应用本地服务所在的 ip 下的请求(因为我们访问网站的域名就是这个),所以都是主应用的本地服务器在处理着这些请求,因此,要打开的路径也是基于主应用工程的根目录下匹配路径的,因此会遇到匹配错误(同名路径会误打开)和匹配不到的情况。

举例子说明:

  • A是主应用main-app上的一个组件
  • B是其中一个微应用micro-app上的一个组件

A所在地址是 main-app/src/A/index.vuemicro-app 的组件 B 所在地址是 micro-app/src/B/index.vue。但是点击 devtool 上的打开文件按钮,发的请求中 file 的路径是一个基于组件所在微应用根目录下的相对路径,因此是不包含根目录自身的。即点击打开 micro-app B 组件,请求是 main-app 服务ip/__open-in-editor?file=src/B/index.vue。然后 main-app 的本地服务接管这个请求,去找 main-app 目录下的 B 路径,显然会找不到。

技术方案

归根到底,就是 file 的地址值没对应上各自应用的路径。只要纠正地址即可。

一开始,我也并不是直接想到写个devtools的插件的,也是经过几种方案的对比选择。这里记录下,方便日后回顾以及你们可选择更便利的方案。

官网唯一设置方法

查阅官方文档,发现仅有一个支持修改路径的设置 devtools.vuejs.ac.cn/guide/open-…。 但是它仅仅是修改host的打开地址,就算每个微应用自己设置自己的ip地址,能生效的只有在微应用内的组件,对于一些公共组件,如ui组件,如果这类组件是单独写在非工程内目录下,则仍然无法顺利打开。

写中间件集中分发

曾想过,自己写nodejs,结合launch-editor,在主应用中用脚本集中管理再分发到各微应用正确的路径上,但是奈何点击打开文件按钮所提供出来的信息中,光判断file的路径,无法区分是属于哪个微应用,无法判断分发。

如果你遇到这样的问题,urlfile有唯一标识可区分,你选择这个方式也可。

自定义组件状态数据

vue-devtools官方文档中,有提供开发者自己自定义这个它自己的开放能力—— 自定义插件

我的想法是,既然官方提供的那个打开按钮无法正确使用,那我就干脆不用它了,我自己重新写个按钮,内部逻辑自己处理好就行了。

通读了一下官方文档,发现能添加自定义点击行为的地方,合适之处有两个

  1. 审查组件面板新增状态,状态中添加自定义点击行为
  2. 自己完全自定义一个面板,做到跟审查组件面板类似的功能,然后可以在这个面板中添加自定义点击行为(下面的解决方案简单介绍下)

在这里的解决方案,就是对应第一点,使用自定义状态然后监听这个状态的点击行为。

什么是组件状态,就是截图这里,就是状态面板,里面的就是每个状态分组和状态数据情况。

image.png

当然这个方案中,需要你具备开发 VueDevtools 插件的知识,如果你之前尚未接触过,可阅读我的这篇文章先简单了解下——冷门插件开发Vue Devtools

这里直接贴出最终的插件代码

export function setupDevtools (app, options) {
  setupDevtoolsPlugin({
    id: 'my-awesome-devtools-plugin',
    label: 'My Awesome Plugin',
    app,
    componentStateTypes: [ // 添加这个是给我们自定义的状态数据加入一个类别分组,在状态面板中可以看到这个数据分组
      '自定义状态类别',
    ],
  }, api => {
    // 拦截审查组件行为
    api.on.inspectComponent((payload) => {
      if (payload.instanceData) {
        // 拦截到状态数据,加入我们要自定义的状态数据。格式是按照官方的格式来的
        payload.instanceData.state.push({
          type: '自定义状态类别', // 跟上述的类别分组保持一致
          key: '点击打开组件',
          value: {
            _custom: {
              display: 'click ->',
              tooltip: '点击后面的图标打开',
              actions: [
                {
                  icon: 'input',
                  tooltip: '点击打开组件',
                  // 这个就是我们自定义的点击行为,可以做任何你想做的
                  // 我这个例子中就是模仿原来插件打开文件的打开逻辑,但是自己指定了 file 的值
                  action: () => {
                    fetch(`/__open-in-editor?file=../micro-app/${payload.instanceData.file}`)
                  },
                },
              ],
            },
          },
        })
      }
    })
  })
}

看下实际长的样子

image.png

如图点击该按钮就能打开我们本地文件 file

既然点击行为都是我们可控的,那么我们只需要每个微应用中正确配置 file 路径为相对主应用工程的地址,就能正确打开微应用中组件的文件了。当然,为了使用方便,达到插件共用,插件要提供个options入参,然后在定制的打开行为中拿到这个options,从中分析出当前属于哪个微应用,然后加上正确的 file 地址。

其次,对于公共组件(既不放在主工程中的也不在微应用工程中的),这种可以判断原来 payload.instanceData.file 上的路径前缀判别,然后赋予file路径为相对主应用工程的地址即可。

结论

最终没有采用这种方式,是因为,虽然它能达到目的,但是它会影响到开发者的使用习惯,开发者习惯打开文件的地方变成了状态数据里了,并且也不美观,在组件多状态数据的情况下,要找到这个自定义的状态不方便。

自定义检查器

上面说了,另外一种达到自定义点击行为的途径就是自己完全写个类似组件检查器的新检查器,然后定制状态面板上的 icon 和点击行为。这种方式也是完全可行的。

如果你尚未知晓如何自定义检查器,可阅读我的这篇 冷门插件开发Vue Devtools——自定义检查器

但是最终我也没采用这种方式,是因为它会影响到开发者的使用习惯,开发者习惯打开文件的地方在组件检查器中,跟检查组件一起使用的,切来切去检查器不友好。

直接改写原来打开文件地址

这个就是最终的方案,就是原封不动地,还是在原来打开文件的图标上。

既然官方没有可自定义打开地址的方式,那我就想办法达到这个目的。于是我特意去看了官方源代码,试图寻找有效的解决方案,因为我觉得它点击打开的地方肯定是读取某个地方的数据,那看看有没有什么尚未暴露的方式达到修改这个数据的目的。

其实这个方案的解决是有点意外性的,我在查阅源代码的时候还未追溯完,但是在自己调试打 log 的时候,无意发现个变量然后抱着尝试性的心态去修改,意外发现可行。

最终发现,只要在审查组件的时候,动态修改payload.instanceData.file就可以了。

值得一提的是,最终我封装好的插件,可以按照官方提供的思路那样,使用use方法注册插件,我的插件里已经兼容好了 vue2 和 vue3 了。

但是我觉得官方提供的那种方式,在 vue2 中有点不太优雅,它是使用全局mixin,会让每个组件实例都会触发一次,性能不是很好,但是实际上它只需要在根实例上执行一次即可。并且它得搭配 new Vue 实例选项加一个配置才行。

官方这么写的原因应该是利用 vue.use 插件的形式,但是实际上我们使用时,不一定非要用插件的形态,可以直接调一个方法。

因此在我的插件vue-devtools-editor-url里,在此基础上,直接导出了一个方法,可直接传Vue的根实例,也很好兼容 Vue2 和 Vue3

// Vue2
import Vue from 'vue'
import { setupDevtools } from 'vue-devtools-editor-url'
const app = new Vue({
  // options
})
setupDevtools(app, 'your-url')

// Vue3
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')
setupDevtools(app, 'your-url')

最终就采用此方案了,即最终成果 vue-devtools-editor-url 这个包