插件开发指南
注意
该 API 仅在 Vue Devtools 6+ 中可用
架构
Vue Devtools 插件从用户应用程序中的 Vue 包代码注册。它通过 公共 API 与 Vue Devtools 后端交互。后端是当用户打开 Vue Devtools 时注入网页的脚本 - 它负责注册 Vue 应用程序并与 Vue Devtools 前端通信。前端是浏览器开发者工具窗格中显示的 Vue 应用程序。钩子是添加到页面中的全局变量,以便 Vue 应用程序和你的插件可以向后端发送消息。
有 3 个主要的 API 类别
- 组件检查器:你的插件可以向组件树和状态添加更多信息。
- 自定义检查器:你可以添加新的检查器来显示任何类型的状态。例如:路由、存储当前状态...
- 时间线:你可以添加自定义图层并发送事件。
使用 API,你可以向用户显示信息并改善应用程序调试体验。像 vue-router
和 vuex
这样的官方库已经使用了这个 API!
架构图
示例
设置
在你的包中,安装 @vue/devtools-api
作为依赖项
yarn add @vue/devtools-api
这个包将允许你从你的代码中注册一个新的 Vue Devtools 插件,并附带完整的 TypeScript 类型定义。
你的 package.json
文件应该类似于这个
{
"name": "my-awesome-plugin",
"version": "0.0.0",
"main": "dist/index.js",
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.14"
},
"peerDependencies": {
"vue": "^3.1.0"
},
"devDependencies": {
"vue": "^3.1.0"
}
}
最好也指定 vue
作为对等依赖项,以告知用户你的包与哪个版本的 Vue 兼容。
TypeScript
如果你使用 TS,你的 package.json
文件应该类似于这个
{
"name": "my-awesome-plugin",
"version": "0.0.0",
"main": "dist/index.js",
"scripts": {
"dev": "tsc --watch -d",
"build": "tsc -d"
},
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.14"
},
"peerDependencies": {
"vue": "^3.1.0"
},
"devDependencies": {
"@types/node": "^14.14.22",
"typescript": "^4.1.3",
"vue": "^3.1.0"
}
}
以下是一个 tsconfig.json
文件示例,可以放在 package.json
文件旁边
{
"include": [
"src/global.d.ts",
"src/**/*.ts",
"__tests__/**/*.ts"
],
"compilerOptions": {
"outDir": "dist",
"sourceMap": false,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"allowJs": false,
"skipLibCheck": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": false,
"strict": true,
"isolatedModules": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"removeComments": false,
"jsx": "preserve",
"lib": [
"esnext",
"dom"
],
"types": [
"node"
]
}
}
Rollup
Rollup 是一个通用捆绑器。你可以用它来编译你的包,以便更容易地使用。如果你有 .vue
文件要编译,它也非常方便!
yarn add -D rollup rollup-plugin-vue @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace rollup-plugin-terser pascalcase rimraf
关于包的说明
rollup-plugin-vue
编译.vue
文件。@rollup/plugin-commonjs
将 CommonJS 模块转换为 ES2015 模块。@rollup/plugin-node-resolve
定位并捆绑node_modules
中的第三方依赖项。@rollup/plugin-replace
允许我们在构建时用我们想要的值替换一些源代码文本,比如process.
。env.NODE_ENV rollup-plugin-terser
缩小生产环境的输出。pascalcase
用于将你的包名称(来自package.json
)转换为 Pascal 大小写,例如my-plugin
转换为MyPlugin
。rimraf
用于在构建之前清除dist
文件夹。
Rollup 配置
在 package.json
文件旁边创建一个 rollup.config.js
文件
Rollup 包
将主字段、exports
和 scripts
添加到你的 package.json
中
{
"name": "my-plugin",
"version": "0.0.0",
"description": "A demo Vue 3 plugin with devtools integration",
"author": {
"name": "Guillaume Chau",
"email": "[email protected]"
},
"main": "dist/my-plugin.cjs.js",
"module": "dist/my-plugin.esm-bundler.js",
"unpkg": "dist/my-plugin.global.js",
"jsdelivr": "dist/my-plugin.global.js",
"exports": {
".": {
"require": "./dist/my-plugin.cjs.js",
"browser": "./dist/my-plugin.esm-browser.js",
"import": "./dist/my-plugin.esm-bundler.js",
"module": "./dist/my-plugin.esm-bundler.js"
},
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"build": "rimraf dist && rollup -c rollup.config.js"
}
...
}
不要忘记将 my-plugin
替换为你的包名称。
你现在可以使用 build
脚本编译包
yarn build
带有 TypeScript 的 Rollup
安装 Rollup TS 插件
yarn add -D rollup-plugin-typescript2
修改 Rollup 配置以编译 TS 文件
并将 types
字段添加到你的 package.json
文件中
{
"name": "my-plugin",
"version": "0.0.0",
"description": "A demo Vue 3 plugin with devtools integration",
"author": {
"name": "Guillaume Chau",
"email": "[email protected]"
},
"main": "dist/my-plugin.cjs.js",
"module": "dist/my-plugin.esm-bundler.js",
"unpkg": "dist/my-plugin.global.js",
"jsdelivr": "dist/my-plugin.global.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"require": "./dist/my-plugin.cjs.js",
"browser": "./dist/my-plugin.esm-browser.js",
"import": "./dist/my-plugin.esm-bundler.js",
"module": "./dist/my-plugin.esm-bundler.js"
},
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"build": "rimraf dist && rollup -c rollup.config.js"
}
...
}
查看 TypeScript 获取 tsconfig.json
示例。
注册你的插件
在你的源文件夹中创建一个新的 devtools.js
文件。
插件设置
我们将从 @vue/devtools-api
包中导入 setupDevtoolsPlugin
import { setupDevtoolsPlugin } from '@vue/devtools-api'
然后我们导出一个函数来设置我们的 Vue Devtools 插件
export function setupDevtools () {
setupDevtoolsPlugin({ /* Options... */}, api => {
// Logic...
})
}
在第一个参数中添加插件选项
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
label: 'My Awesome Plugin',
packageName: 'my-awesome-plugin',
homepage: 'https://vuejs.ac.cn'
}, api => {
// Logic...
})
每个插件都绑定到一个 Vue 应用程序。你需要将用户应用程序传递给 setupDevtoolsPlugin
- 与你的插件 install
方法作为第一个参数获取的相同。
export function setupDevtools (app) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
label: 'My Awesome Plugin',
packageName: 'my-awesome-plugin',
homepage: 'https://vuejs.ac.cn',
app
}, api => {
// Logic...
})
}
setupDevtoolsPlugin
的第二个参数是一个回调函数,它将以 Vue Devtools API 作为第一个参数。
export function setupDevtools (app) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
// Use the API here...
})
}
我们现在可以在我们的 Vue 插件中导入并使用我们的 setupDevtools
函数
import { setupDevtools } from './devtools'
export default {
install (app, options = {}) {
// Our Vue plugin logic
setupDevtools(app)
}
}
Vue 2
在 Vue 2 应用程序中,你需要将根组件实例作为 app
参数传递
import { setupDevtools } from './devtools'
export default {
install (Vue) {
Vue.mixin({
beforeCreate () {
if (this.$options.myPlugin) {
setupDevtools(this)
}
}
})
}
}
在用户的应用程序中
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')
插件设置
使用 settings
选项,你的插件可以向用户公开一些设置。这对于允许一些自定义非常有用!
所有设置项都必须具有以下属性
type
(见下文)label
:用于描述设置项的字符串defaultValue
可用的设置类型
boolean
text
choice
options
:类型为{ value: any, label: string }
的对象的列表component
:(可选)可以是'select'
(默认)或'button-group'
示例
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
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 => {
// Use `api.getSettings()` to get the current settings for the plugin
console.log(api.getSettings())
})
你可以使用 api.on.setPluginSettings
钩子监听用户对设置的更改
api.on.setPluginSettings(payload => {
// Do something...
})
生产环境的 Tree-shaking
由于我们将只编写用于集成 Vue Devtools 的代码,因此最好为我们包的生产版本删除它 - 从而提高大小和性能。
默认情况下,Vue 3 不会在生产环境中包含与 devtools 相关的代码。 它使用 _
环境变量作为编译标志来强制启用此代码。我们可以使用相同的标志来实现相同目的,并且我们还可以检查 NODE_ENV
以自动在开发环境中包含 devtools 插件。
if (process.env.NODE_ENV === 'development' || __VUE_PROD_DEVTOOLS__) {
setupDevtools(app)
}
在你的构建设置中,你应该替换 process.
和 _
,用于不针对捆绑器的库构建。查看 Rollup 设置,其中包含 @rollup/plugin-replace
的示例用法。
组件
组件 API 允许你
- 向组件树添加标签。
- 在组件状态检查器中显示其他数据。
钩子
Vue Devtools API 包含通过 api.on
可用的钩子。钩子对于向现有元素(例如组件树节点、组件状态、时间线事件)添加新的调试信息很有用。可以使用 api.on.hookName(callback)
注册特定钩子的新回调函数。
每个钩子都期望回调函数具有相同的参数
payload
,它包含与钩子相关的状态。它可以被修改以传递回其他信息。context
公开有关 devtools 的数据
示例
export function setupDevtools (app) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
api.on.hookNameHere((payload, context) => {
// Do something...
})
})
}
每个钩子都处理异步回调
export function setupDevtools (app) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
api.on.hookNameHere(async (payload, context) => {
await something()
})
})
}
所有注册的回调函数将按顺序调用,包括异步回调函数,按照它们在页面上注册的顺序。
组件树
要向组件树添加标签,我们可以使用 visitComponentTree
钩子
export function setupDevtools (app) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
api.on.visitComponentTree((payload, context) => {
const node = payload.treeNode
if (payload.componentInstance.type.meow) {
node.tags.push({
label: 'meow',
textColor: 0x000000,
backgroundColor: 0xff984f
})
}
})
})
}
这个示例将向任何将 meow: true
选项添加到其定义中的组件添加一个 meow
标签
export default {
meow: true
}
以下是一个示例结果
颜色
Vue Devtools API 中的颜色以数字而不是字符串的形式编码。你可以在 JavaScript 中使用 0x
来编写十六进制值
0x000000 // black
0xffffff // white
0xff984f // orange
0x41b86a // green
示例
api.on.visitComponentTree((payload, context) => {
const node = payload.treeNode
if (payload.componentInstance.type.meow) {
node.tags.push({
label: 'meow',
textColor: 0x000000,
backgroundColor: 0xff984f
})
}
})
通知变更
默认情况下,Vue Devtools 将确定何时更新组件树(从而再次调用钩子)。我们可以使用 api.notifyComponentUpdate
函数强制刷新
api.notifyComponentUpdate(componentInstance)
这样,如果我们知道特定组件的标签应该更改,我们可以通知 Vue Devtools,然后我们的钩子回调函数将再次被调用。
组件状态
可以使用 inspectComponent
钩子向组件状态检查器添加新数据
api.on.inspectComponent((payload, context) => {
// ...
})
每个新字段都应该添加到 payload.instanceData.state
数组中
api.on.inspectComponent((payload, context) => {
payload.instanceData.state.push({
type: 'My Awesome Plugin state',
key: '$hello',
value: data.message
})
payload.instanceData.state.push({
type: 'My Awesome Plugin state',
key: 'time counter',
value: data.counter
})
})
请注意,我们如何使用 setupDevtools
的 data
参数(见下文)将一些数据从我们的库传递到 Vue Devtools 插件中。
type
将用于在状态检查器中创建一个新部分。建议使用一个变量
const stateType = 'My Awesome Plugin state'
export function setupDevtools (app, data) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
api.on.inspectComponent((payload, context) => {
payload.instanceData.state.push({
type: stateType,
key: '$hello',
value: data.message
})
payload.instanceData.state.push({
type: stateType,
key: 'time counter',
value: data.counter
})
})
})
}
然后,使用此变量在 Vue Devtools 插件选项中使用 componentStateTypes
声明您的数据类型
export function setupDevtools (app) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
label: 'My Awesome Plugin',
packageName: 'my-awesome-plugin',
homepage: 'https://vuejs.ac.cn',
componentStateTypes: [
stateType
],
app
}, api => {
// ...
})
}
以下是一个示例结果
如果您不传递任何参数,您可以再次使用 notifyComponentUpdate
强制当前组件的状态刷新
api.notifyComponentUpdate()
编辑组件状态
您可以将一些状态字段标记为 editable
以允许用户编辑
payload.instanceData.state.push({
type: stateType,
key: '$hello',
value: data.message,
editable: true
})
然后,您可以使用 editComponentState
钩子处理编辑提交
api.on.editComponentState(payload => {
// ...
})
然后,您需要检查正在编辑的字段是否来自您的插件,使用 payload.type
api.on.editComponentState(payload => {
if (payload.type === stateType) {
// ...
}
})
这样,如果用户正在编辑其他内容(例如数据属性),我们就不会做任何事情。
可以以下列方式修改已编辑的字段
- 分配一个新值,
- 添加一个新属性(对象)或项目(数组),
- 删除属性或项目。
然后,您可以使用 payload.set
辅助函数来应用编辑。payload.set
的参数应该是包含我们可编辑状态的对象
api.on.editComponentState(payload => {
if (payload.type === stateType) {
payload.set(data)
}
})
在本例中,我们执行 payload.set(data)
,因为我们正在发送 data.message
可编辑状态
payload.instanceData.state.push({
type: stateType,
key: '$hello',
value: data.message,
editable: true
})
您也可以使用单独的对象来保存您的可编辑状态(示例)。
自定义检查器
Vue Devtools 默认情况下只有一个检查器:组件检查器。Vue Devtools 插件可以在它旁边引入新的检查器以显示更多调试信息。
要设置自定义检查器,您的插件必须
- 使用
addInspector
注册自定义检查器。 - 处理
getInspectorTree
钩子以填充检查器树(在 devtools 的左侧或顶部)。 - 处理
getInspectorState
钩子以发送状态(在 devtools 的右侧或底部)。
注册自定义检查器
让我们从使用 addInspector
注册我们的自定义检查器开始
api.addInspector({
id: 'my-awesome-plugin',
label: 'Awesome!',
icon: 'pets',
})
建议使用一个变量来存储检查器 ID,因为它将在以后的钩子中需要
const inspectorId = 'my-awesome-plugin'
export function setupDevtools (app, data) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
api.addInspector({
id: inspectorId,
label: 'Awesome!',
icon: 'pets',
})
})
}
您可以使用任何 Material Icon 代码作为图标,例如 note_add
或 drag_indicator
。
以下是一个示例结果
发送检查器树
使用 getInspectorTree
,我们可以在检查器中显示一个节点树
api.on.getInspectorTree((payload, context) => {
// ...
})
但在此之前,我们需要检查钩子是否针对正确的检查器(我们的)调用
api.on.getInspectorTree((payload, context) => {
if (payload.inspectorId === inspectorId) {
// ...
}
})
然后,我们可以将节点树放入 payload.rootNodes
中
api.on.getInspectorTree((payload, context) => {
if (payload.inspectorId === inspectorId) {
payload.rootNodes = [
{
id: 'root',
label: 'Awesome root',
children: [
{
id: 'child-1',
label: 'Child 1',
tags: [
{
label: 'awesome',
textColor: 0xffffff,
backgroundColor: 0x000000
}
]
},
{
id: 'child-2',
label: 'Child 2'
}
]
},
{
id: 'root2',
label: 'Amazing root'
}
]
}
})
以下是一个示例结果
发送检查器状态
使用 getInspectorState
钩子根据选定的节点发送状态(检查 payload.nodeId
)
api.on.getInspectorState((payload, context) => {
if (payload.inspectorId === inspectorId) {
if (payload.nodeId === 'child-1') {
payload.state = {
'my section': [
{
key: 'cat',
value: 'meow',
}
]
}
} else if (payload.nodeId === 'child-2') {
payload.state = {
'my section': [
{
key: 'dog',
value: 'waf',
}
]
}
}
}
})
不要忘记使用 payload.inspectorId
检查检查器是否正确!
以下是在选择“子节点 1”时的示例结果
刷新检查器
Vue Devtools 不会自动刷新您的自定义检查器。要发送更新,您有两个不同的可用函数
sendInspectorTree
用于刷新节点树,sendInspectorState
用于刷新状态检查器。
示例
// Update tree
api.sendInspectorTree(inspectorId)
// Update state
api.sendInspectorState(inspectorId)
调用这些函数将分别调用 getInspectorTree
和 getInspectorState
钩子。
时间线
Vue Devtools 在时间线上附带了一些内置层,例如鼠标和键盘事件、组件事件和性能火焰图。您可以按照以下步骤添加自定义时间线层
- 使用
addTimelineLayer
注册层。 - 使用
addTimelineEvent
发送事件。
注册一个层
使用 addTimelineLayer
函数注册您的自定义时间线层
const timelineLayerId = 'my-awesome-plugin'
export function setupDevtools (app, data) {
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
api.addTimelineLayer({
id: timelineLayerId,
color: 0xff984f,
label: 'Awesome!'
})
})
}
以下是一个示例结果
发送一个事件
使用 addTimelineEvent
函数添加新事件
window.addEventListener('click', event => {
api.addTimelineEvent({
layerId: timelineLayerId,
event: {
time: api.now(),
data: {
mouseX: event.clientX,
mouseY: event.clientY
}
}
})
})
您可以将 enableEarlyProxy
插件选项设置为 true
,以便能够在 Vue devtools 打开并连接之前发送时间线事件
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
app,
enableEarlyProxy: true
}, api => {
// `api` will be a proxy waiting for the real API to be available
})
事件组
您可以将事件分组在一起。它将以药丸形矩形显示它们,并在事件检查器中显示组的总持续时间。
使用相同的值在 event
上设置 groupId
选项以创建一个组。以下是一个包含 3 个事件的组的示例
const groupId = 'group-1'
devtoolsApi.addTimelineEvent({
layerId: timelineLayerId,
event: {
time: api.now(),
data: {
label: 'group test'
},
title: 'group test',
groupId
}
})
devtoolsApi.addTimelineEvent({
layerId: timelineLayerId,
event: {
time: api.now() + 10,
data: {
label: 'group test (event 2)',
},
title: 'group test',
groupId
}
})
devtoolsApi.addTimelineEvent({
layerId: timelineLayerId,
event: {
time: api.now() + 20,
data: {
label: 'group test (event 3)',
},
title: 'group test',
groupId
}
})
以下是时间线上的结果,其中选择了组的第一个事件
以及事件检查器视图中的“组”选项卡
注意“组信息”部分,其中包含有关该组的更多信息。
来自包的事件
让我们使用 addTimelineEvent
API 来跟踪我们的 Vue 插件公开的异步方法。我们将称之为 $doSomething
import { setupDevtools } from './devtools'
export default {
install (app, options = {}) {
app.mixin({
methods: {
$doSomething () {
return new Promise(resolve => {
setTimeout(() => {
resolve('done')
}, 1000)
})
}
}
})
if (process.env.NODE_ENV === 'development' || __VUE_PROD_DEVTOOLS__) {
setupDevtools(app, data)
}
}
}
我们现在可以从用户应用程序中的任何组件调用此方法
this.$doSomething()
我们希望从我们的库代码中跟踪此方法,因此我们需要从我们的 setupDevtools
函数返回一组辅助函数。devtoolsApi
变量将保存 Vue Devtools API,devtools
变量将是一个包含辅助函数的对象,我们将返回它
export function setupDevtools (app, data) {
let devtoolsApi
const devtools = {
// Our helpers here
}
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
devtoolsApi = api
// ...
})
return devtools
}
回到我们的库代码中,我们可以创建一个 devtools
变量来检索 setupDevtools
的结果,即包含我们的辅助函数的对象
import { setupDevtools } from './devtools'
export default {
install (app, options = {}) {
let devtools
app.mixin({
// ...
})
if (process.env.NODE_ENV === 'development' || __VUE_PROD_DEVTOOLS__) {
devtools = setupDevtools(app)
}
}
}
让我们实现一个 trackStart
辅助函数,当我们想要开始跟踪调用时创建一个事件,并在调用完成时创建另一个事件
export function setupDevtools (app, data) {
let devtoolsApi
let trackId = 0
const devtools = {
trackStart: (label: string) => {
const groupId = 'track' + trackId++
// Start
devtoolsApi.addTimelineEvent({
layerId: timelineLayerId,
event: {
time: api.now(),
data: {
label
},
title: label,
groupId
}
})
return () => {
// End
devtoolsApi.addTimelineEvent({
layerId: timelineLayerId,
event: {
time: api.now(),
data: {
label,
done: true
},
title: label,
groupId
}
})
}
}
}
setupDevtoolsPlugin({
id: 'my-awesome-devtools-plugin',
// ...
}, api => {
devtoolsApi = api
// ...
})
return devtools
}
trackId
用于为每次调用 startTrack
生成一个唯一的组 ID。通过使用相同的 ID,我们将创建一个包含两个事件的组,即使它们并非同时创建。
我们可以像这样使用 trackStart
辅助函数
const trackEnd = devtools.trackStart('some-label')
// Later
trackEnd()
我们现在可以在我们的库代码中使用 trackStart
函数
let devtools
app.mixin({
methods: {
$doSomething () {
const trackEnd = devtools ? devtools.trackStart('$doSomething') : null
return new Promise(resolve => {
setTimeout(() => {
if (trackEnd) trackEnd()
resolve('done')
}, 1000)
})
}
}
})
if (process.env.NODE_ENV === 'development' || __VUE_PROD_DEVTOOLS__) {
devtools = setupDevtools(app, data)
}
警告
我们必须注意检查我们的 devtools
变量是否已定义,因为我们在生产中剥离了 devtools 代码!请参阅 生产中的树摇。
以下是一个示例结果
以及事件检查器
感谢您遵循 Vue Devtools 插件开发指南!您可以在 API 参考 中找到有关 API 的更详细说明。祝您构建愉快!😸