feat: custom formatters

This commit is contained in:
Evan You 2020-10-13 11:50:09 -04:00
parent ffdb05e1f1
commit 6ba7ba47d5
5 changed files with 208 additions and 1 deletions

View File

@ -22,6 +22,7 @@ module.exports = {
'!packages/template-explorer/**',
'!packages/size-check/**',
'!packages/runtime-core/src/profiling.ts',
'!packages/runtome-core/src/customFormatter.ts',
// DOM transitions are tested via e2e so no coverage is collected
'!packages/runtime-dom/src/components/Transition*',
// only called in browsers

View File

@ -247,6 +247,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
return true
}
// for internal formatters to know that this is a Vue instance
if (__DEV__ && key === '__isVue') {
return true
}
// data / props / ctx
// This getter gets called for every property access on the render context
// during render and is a major hotspot. The most expensive part of this

View File

@ -0,0 +1,198 @@
import { isReactive, isReadonly, isRef, Ref, toRaw } from '@vue/reactivity'
import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared'
import { ComponentInternalInstance, ComponentOptions } from './component'
import { ComponentPublicInstance } from './componentPublicInstance'
export function initCustomFormatter() {
if (!__DEV__ || !__BROWSER__) {
return
}
const vueStyle = { style: 'color:#3ba776' }
const numberStyle = { style: 'color:#0b1bc9' }
const stringStyle = { style: 'color:#b62e24' }
const keywordStyle = { style: 'color:#9d288c' }
// custom formatter for Chrome
// https://www.mattzeunert.com/2016/02/19/custom-chrome-devtools-object-formatters.html
const formatter = {
header(obj: unknown) {
// TODO also format ComponentPublicInstance & ctx.slots/attrs in setup
if (!isObject(obj)) {
return null
}
if (obj.__isVue) {
return ['div', vueStyle, `VueInstance`]
} else if (isRef(obj)) {
return [
'div',
{},
['span', vueStyle, genRefFlag(obj)],
'<',
formatValue(obj.value),
`>`
]
} else if (isReactive(obj)) {
return [
'div',
{},
['span', vueStyle, 'Reactive'],
'<',
formatValue(obj),
`>${isReadonly(obj) ? ` (readonly)` : ``}`
]
} else if (isReadonly(obj)) {
return [
'div',
{},
['span', vueStyle, 'Readonly'],
'<',
formatValue(obj),
'>'
]
}
return null
},
hasBody(obj: unknown) {
return obj && (obj as any).__isVue
},
body(obj: unknown) {
if (obj && (obj as any).__isVue) {
return [
'div',
{},
...formatInstance((obj as ComponentPublicInstance).$)
]
}
}
}
function formatInstance(instance: ComponentInternalInstance) {
const blocks = []
if (instance.type.props && instance.props) {
blocks.push(createInstanceBlock('props', toRaw(instance.props)))
}
if (instance.setupState !== EMPTY_OBJ) {
blocks.push(createInstanceBlock('setup', instance.setupState))
}
if (instance.data !== EMPTY_OBJ) {
blocks.push(createInstanceBlock('data', toRaw(instance.data)))
}
const computed = extractKeys(instance, 'computed')
if (computed) {
blocks.push(createInstanceBlock('computed', computed))
}
const injected = extractKeys(instance, 'inject')
if (injected) {
blocks.push(createInstanceBlock('injected', injected))
}
blocks.push([
'div',
{},
[
'span',
{
style: keywordStyle.style + ';opacity:0.66'
},
'$ (internal): '
],
['object', { object: instance }]
])
return blocks
}
function createInstanceBlock(type: string, target: any) {
target = extend({}, target)
if (!Object.keys(target).length) {
return ['span', {}]
}
return [
'div',
{ style: 'line-height:1.25em;margin-bottom:0.6em' },
[
'div',
{
style: 'color:#476582'
},
type
],
[
'div',
{
style: 'padding-left:1.25em'
},
...Object.keys(target).map(key => {
return [
'div',
{},
['span', keywordStyle, key + ': '],
formatValue(target[key], false)
]
})
]
]
}
function formatValue(v: unknown, asRaw = true) {
if (typeof v === 'number') {
return ['span', numberStyle, v]
} else if (typeof v === 'string') {
return ['span', stringStyle, JSON.stringify(v)]
} else if (typeof v === 'boolean') {
return ['span', keywordStyle, v]
} else if (isObject(v)) {
return ['object', { object: asRaw ? toRaw(v) : v }]
} else {
return ['span', stringStyle, String(v)]
}
}
function extractKeys(instance: ComponentInternalInstance, type: string) {
const Comp = instance.type
if (isFunction(Comp)) {
return
}
const extracted: Record<string, any> = {}
for (const key in instance.ctx) {
if (isKeyOfType(Comp, key, type)) {
extracted[key] = instance.ctx[key]
}
}
return extracted
}
function isKeyOfType(Comp: ComponentOptions, key: string, type: string) {
const opts = Comp[type]
if (
(isArray(opts) && opts.includes(key)) ||
(isObject(opts) && key in opts)
) {
return true
}
if (Comp.extends && isKeyOfType(Comp.extends, key, type)) {
return true
}
if (Comp.mixins && Comp.mixins.some(m => isKeyOfType(m, key, type))) {
return true
}
}
function genRefFlag(v: Ref) {
if (v._shallow) {
return `ShallowRef`
}
if ((v as any).effect) {
return `ComputedRef`
}
return `Ref`
}
/* eslint-disable no-restricted-globals */
if ((window as any).devtoolsFormatters) {
;(window as any).devtoolsFormatters.push(formatter)
} else {
;(window as any).devtoolsFormatters = [formatter]
}
}

View File

@ -93,6 +93,7 @@ export {
setTransitionHooks,
getTransitionRawChildren
} from './components/BaseTransition'
export { initCustomFormatter } from './customFormatter'
// For devtools
export { devtools, setDevtoolsHook } from './devtools'

View File

@ -1,4 +1,4 @@
import { setDevtoolsHook } from '@vue/runtime-dom'
import { setDevtoolsHook, initCustomFormatter } from '@vue/runtime-dom'
import { getGlobalThis } from '@vue/shared'
export function initDev() {
@ -12,5 +12,7 @@ export function initDev() {
`You are running a development build of Vue.\n` +
`Make sure to use the production build (*.prod.js) when deploying for production.`
)
initCustomFormatter()
}
}