冷门插件开发Vue Devtools

2,598 阅读4分钟

前言

VueDevtools浏览器插件,它官方提供开放 api 供开发者写这个插件的插件,以达到自定义 Devtools更多能力的目的。
image.png

本篇文章的主要目的:

  • 介绍Devtools能让开发者做到哪些程度上的定制化。
  • 咀嚼了官方文档,从一名开发者角度更好地去理解各种定制化场景和 api,没官方那么难以理解。
  • 方便日后自己不再需要去查阅官方文档,从头梳理

概览

Devtools 能让我们定制哪些部分?

image.png

我框选的区域就是可以我们自定义的。

  1. 添加你写的插件的设置项。这个设置项,可应用与其他定制能力中进行获取,配合做更多定制化能力;
  2. 定制检查组件时的信息和处理行为。比如你可以在检查组件的时候,加入一些自定义的检查项,或者是在检查的时候,加入自定义的处理行为,以达到更多的定制化能力。
  3. 添加自定义的检查器。让你完全实现一套跟内置能力不一样的全新检查能力。
  4. 添加时间轴的自定义追踪能力。

新建插件

在开始每个具体定制化能力介绍之前,我们要先了解下,如何写一个 Devtools 的插件,后续四部分的定制化能力介绍,都是基于这个插件代码基础上描述的,不再累赘写插件部分重复代码。

首先使用官方提供的开放 api —— @vue/devtools-api (使用前请先下载)

import { setupDevtoolsPlugin } from '@vue/devtools-api'

一个基本插件结构。

export function setupDevtools (app) {
  setupDevtoolsPlugin({
    id: 'my-awesome-devtools-plugin',
    label: 'My Awesome Plugin',
    app // 每个插件都绑定到一个 Vue 应用程序。你需要将你的应用程序传递到这里。
  }, api => { // 这个api 就是Vue Devtools API,我们可以用这个 api 做监听等定制化功能,后续重点就是讲如何使用 api 达到各种定制化能力
    // Logic...
  })
}

如果你想你的插件最终提供出去给开发者使用,是使用 use 方法进行插件注册的,那么可继续阅读下去,否则,其实直接使用上面导出的setupDevtools方法进行调用即可。官方文档是预期你提供的插件最终是使用 use 方法进行插件注册的

Vueuse 方法本质上就是调用 install 方法,因此,我们的插件要导出一个 install 方法

export default {
  install (app) {
    setupDevtools(app)
  }
}

但是在vue2 中,上述写法不生效,官方文档中有额外支出 vue2 的写法

export default {
  install (Vue) {
    Vue.mixin({
      beforeCreate () {
        if (this.$options.myPlugin) {
          setupDevtools(this)
        }
      }
    })
  }
}

在开发者所在的应用中,创建 vue 实例时还要传myPlugin

import Vue from 'vue'
import App from './App.vue'
// 这个就是我们自定义的插件
import DevtoolsPlugin from './DevtoolsPlugin'

Vue.use(DevtoolsPlugin)

new Vue({
  render: h => h(App),
  myPlugin: true,
}).$mount('#app')

我这里把 Vue3 和 Vue2 的写法都揉在一起,这样可自动适配不同版本的 Vue

export default {
  install (Vue) {
    // 检查 Vue 版本
    const version = Vue.version.split('.')[0]

    // Vue 2 兼容代码
    if (version === '2') {
      Vue.mixin({
        beforeCreate () {
          if (this.$options.myPlugin) {
            setupDevtools(this)
          }
        },
      })
    // Vue 3 兼容代码
    } else if (version === '3') {
      setupDevtools(Vue)
    } else {
      console.warn('Unsupported Vue version')
    }
  },
}

至此,一个 Devtools 的插件框架搭建完毕了,剩下的处理就是为setupDevtools方法里补充第二个参数函数的逻辑,以实现定制化功能。

注意
vue2的项目要使用旧版本的@vue/devtools-api 才能起作用,用最新的setupDevtoolsPlugin不会生效。可使用版本是 6 的版本,如6.0.0-beta.11

自定义设置项

这节是教你为你的插件,添加一些全局的设置选项

image.png

截图这里以vue2的插件设置为例子,实际上我们要设置的是我们自己上面写的新的Devtools插件的设置内容,对应框选部分。

通过这些设置,用户可根据自己的需求进行设置,然后在其他功能中应用读取这些设置,开展进一步的定制化工作。

那这里就聊下怎么新建设置和应用设置等

新建

新建非常简单,在我们新建整个插件的配置中,添加 settings 字段,用以配置我们这个插件可支持的设置项

export function setupDevtools (app) {
  setupDevtoolsPlugin({
    id: 'my-awesome-devtools-plugin',
    label: 'My Awesome Plugin',
    app,
    // 这里就是对应截图的新增的自定义设置项
    // 这里有多个设置项,分别对应不同的 type 类型的设置表现,大家可对照着截图来看
    settings: {
      test1: {
        label: 'I like vue devtools',
        type: 'boolean',
        defaultValue: true
      },
      test2: {
        label: 'Quick choice',
        type: 'choice',
        defaultValue: 'a',
        options: [
          { value: 'a', label: 'A' },
          { value: 'b', label: 'B' },
          { value: 'c', label: 'C' }
        ],
        component: 'button-group'
      },
      test3: {
        label: 'Long choice',
        type: 'choice',
        defaultValue: 'a',
        options: [
          { value: 'a', label: 'A' },
          { value: 'b', label: 'B' },
          { value: 'c', label: 'C' },
          { value: 'd', label: 'D' },
          { value: 'e', label: 'E' }
        ]
      },
      test4: {
        label: 'What is your name?',
        type: 'text',
        defaultValue: ''
      }
    }
  }, api => {
  })
}

image.png

获取设置

既然我们可以设置,自然设置了之后我们要拿到这些设置项的值,才好开展进一步的工作。

我们在插件的第二个入参函数中写入

api.getSettings()

就能拿到当前所有设置项的情况。

以及当用户改变了某个设置项时,我们也可以监听到

// Plugin settings change
api.on.setPluginSettings(payload => {
  console.log('plugin settings changed', payload.settings,
    // Info about the change
    payload.key, payload.newValue, payload.oldValue)
})

Payload的属性:

  • key:设置项
  • newValue:修改后设置的新值
  • oldValue:旧值(深度克隆)
  • settings:整个当前设置状态对象

自定义组件树及状态数据

当我们点击 Component 这个检查器时,就会出现组件列表,我们再点击某个组件,就会出现对应这个组件此时界面上的各种数据状态情况,这个我称之为状态面板。

image.png

这个能力是 Devtools 内置的,我们最常用的地方就是这里了。

首先介绍下在这个 Component 检查器中,我们可以定制的地方有三个:

  • 组件树面板
  • 状态面板
  • 监听事件

组件树面板

在这个组件树面板区域,其实是由一个个 node 对象组成 tree 的,node 的数据格式是

  • id:一个唯一的节点id
  • label:节点的名称,展示到面板上的
  • children:子节点数组,里面的子节点格式也是一样的,嵌套的
  • tags:节点的标签(可选)。数组
    • label:标签名
    • textColor: 标签的字体颜色
    • backgroundColor: 标签的背景颜色
    • tooltip: 标签的 tooltip

而这个 tree 数据就是存储在payload.treeNode中。

因此当你能定制化改变这个组件树面板的东西就是上面这个 node 里的属性了。如这里给节点加个 tag

// 使用visitComponentTree这个事件,在遍历组件树时,给你想要的组件节点加 tag
api.on.visitComponentTree((payload, context) => {
  const node = payload.treeNode
  // 当前遍历到的组件实例
  if (payload.componentInstance.type.meow) {
    node.tags.push({
      label: 'meow',
      textColor: 0x000000,
      backgroundColor: 0xff984f
    })
  }
})

image.png

状态面板

第二个可以定制的地方就是状态面板,你可以在里面添加一些状态数据,甚至还能添加自定义点击事件。

那我们知道,出现组件状态面板,我们的行为是:先审查对应的组件,才会出现状态面板。因此我们只需要监听审查组件的行为,然后拦截到状态数据,给加入我们要自定义的状态数据。

具体解释看注释

export function setupDevtools (app) {
  setupDevtoolsPlugin({
    id: 'my-awesome-devtools-plugin',
    label: 'My Awesome Plugin',
    app,
    componentStateTypes: [ // 添加这个是给我们自定义的状态数据加入一个类别分组,在状态面板中可以看到这个数据分组
      '自定义状态类别',
    ],
  }, api => {
    // 使用inspectComponent拦截审查组件行为
    api.on.inspectComponent((payload) => {
      if (payload.instanceData) {
        // 拦截到状态数据,加入我们要自定义的状态数据。格式是按照官方的[格式](https://devtools-v6.vuejs.org/plugin/api-reference.html#on-inspectcomponent)来的
        payload.instanceData.state.push({
          type: '自定义状态类别',
          key: '$hello',
          value: 'hello'
        })
        // 除了基本的展示形式外,还能给数据添加一些自定义 icon 点击行为
        payload.instanceData.state.push({
          type: '自定义状态类别', // 跟上述的类别分组保持一致
          key: '$open',
          value: {
            _custom: {
              display: 'click ->',
              tooltip: '点击后面的图标打开',
              actions: [
                {
                  icon: 'input',
                  tooltip: '点击打开组件',
                  // 这个就是我们自定义的点击行为,可以做任何你想做的
                  action: () => {
                    console.log('click the input')
                  },
                },
              ],
            },
          },
        })
      }
    })
  })
}

效果:

image.png

但是使用_custom.actions属性进行设置,发现在最新版的 7.6.8 Devtools插件上是不生效的。已经在 GitHub 上提 issue 了。在旧版的兼容 vue2 和 vue3 的 6 版本中是可行的。

监听事件

我们能监听提供的一些事件,然后拦截执行我们自己的自定义脚本。事件有:

  • visitComponentTree: 遍历组件树
  • inspectComponent: 审查组件时
  • editComponentState: 编辑组件的状态数据时

自定义检查器

上述的 Component 检查器就是 Devtools 内置的检查器功能,那么你也可以自己添加新的检查器,完全自己写一个检查,实现里面的能力。

什么是检查器?就是这里

image.png

这里官方内置了ComponentTimeline两个检查器入口。如果你有使用piniarouter 这些插件,你也会在 Devtools 这里看到他们也实现了自己的自定义检查器。

那实现一个自定义检查器,有哪方面要自己定义的呢?

看截图,共分为这三部分:

  1. 检查器入口
  2. 打开检查器后出现的节点树面板
  3. 对应节点树中某节点的状态面板

就拿Component检查器为例子,

  1. Component 审查组件入口
  2. 组件树面板
  3. 某组件的状态面板

那接下来以这三部分简单给大家介绍下如何写个基础的自定义检查器。大家要触类旁通,举一反三,就好了,不行的话,还是乖乖仔细研读官方文档哈哈

添加检查器

addInspector

主要使用addInspector这个方法,它分为三部分组成

  • 检查器定义与入口展示
  • 节点树面板上方的操作区域定义
  • 节点状态面板上方的操作区域定义

看下下面的代码,对应这三部分

api.addInspector({
  // 1)检查器定义与入口展示
  id: 'custom-inspector', // 检查器的唯一 id
  label: '这是我新增的检查器', // 检查器的名称
  icon: 'tab_unselected', // 检查器入口的图标
  // 2)节点树面板上方的操作区域定义
  actions: [
    {
      icon: 'star', // 操作按钮图标
      tooltip: 'Test custom action', // 操作按钮的 tooltip
      action: () => console.log('Meow! 🐱') // 点击按钮执行的动作
    }
  ],
  // 3)节点状态面板上方的操作区域定义
  nodeActions: [
    {
      icon: 'star',
      tooltip: 'Test node custom action',
      action: (nodeId) => console.log('Node action:', nodeId)
    }
  ]
})

效果图:

image.png

看截图,这里暂时看不到第三部分节点状态面板的操作区设置,因为当前代码缺乏节点树面板的设置,自然进入不到节点状态面板。所以接下来就是进入设置节点树面板章节

自定义节点树面板

出现节点树面板的前置条件就是用户点击了检查器的入口,所以我们首先要拦截这个行为 —— getInspectorTree

我们可以监听getInspectorTree事件,通过检查器的 id 来判断是不是点击的我们新增的自定义检查器

api.on.getInspectorTree(payload => {
  if (payload.inspectorId === 'custom-inspector') {
    // Your logic here
  }
})

当拦截成功后,我们就可以开始往payload.rootNodes这个对象上塞节点数据了,这个对像就是用来渲染节点树的数据源。

节点树的数据格式跟我上面讲过的Component检查器里的组件树 node 的格式一样的(本质上就是一样的)

我们可以随便塞点数据玩玩:

api.on.getInspectorTree((payload) => {
  if (payload.inspectorId === 'custom-inspector') {
    payload.rootNodes = [
      {
        id: 'root',
        label: `Root (${+new Date()})`,
        children: [
          {
            id: 'child',
            label: 'Child',
            tags: [
              {
                label: 'active',
                textColor: 0x000000,
                backgroundColor: 0xFF984F,
              },
            ],
          },
        ],
      },
    ]
  }
})

image.png

自定义状态面板

要打开状态面板的前置行为就是用户要选择某个节点才会出现,因为我们要监听这个行为,然后往状态面板里塞状态数据。

使用getInspectorState事件监听,同样需要判断是否点击的是你自定义检查器里的节点

api.on.getInspectorState(payload => {
  if (payload.inspectorId === 'custom-inspector') {
    // Your logic here
  }
})

在这个监听处理中,我们需要往状态面板塞数据,本质上就是往payload.state这个对象上塞数据。它的格式要求是 key 为状态分组的名称,value为这个分组下状态数据集合(数组)。如:

payload.state = {
  'section 1': [
    // fields
  ],
  'section 2': [
    // fields
  ]
}

fields 的数据格式,跟前面说的实现【自定义状态数据的格式】要求一样。

其他

当然这里还有几个 api 可以供你做更加丰富的定制行为,但是这里就不展开描述了,基本上面三部分理解了,其他 api 都是锦上添花的能力。

具体查看 文档

自定义时间轴

其实有了上面三个部分的自定义内容,你已经对 Devtools 的定制化操作了解都差不多了,时间轴这里其实无非也是类似的东西。然后我看了下官方文档,其实本身解释也挺清晰的,建议可直接阅读官方文档这里

简单总结了下,就是插件会新建一个时间轴上的事件分组,然后插件提供一些事件触发函数,我们在业务代码中可使用这些事件函数在合适的地方调用,这样,在运行我们网站页面的时候,就能够捕获到这些事件的执行情况。

说白了,就有点埋点的意思。