Skip to content

@hy-bricks/devtools

诊断 SDK —— DEV 模式自动浮窗、prod 可选启用,用来看 hit-test / drag / render 出错时到底是什么原因。SDK 自己不会用,宿主显式启用才生效。

多 Tab 诊断浮窗:总览 / 命中 / 拖动 / 渲染(实例计数 + 渲染开销 + 运行时错误)/ Trace / Events(交互事件流),外加 Instance Inspect + DOM click inspect。运行态渲染场景(HyperCardPageRenderer)也会自动注册诊断 handle,无需 designer。

安装

bash
pnpm add @hy-bricks/devtools

peerDeps:vue / @hy-bricks/core / @hy-bricks/canvas。CSS 已通过构建内联进 JS bundle,宿主不需要额外 import 任何 style.css

零配置接入(推荐)

绝大多数情况下你只想"DEV 模式自动出浮窗",用带 side-effect 的子入口 @hy-bricks/devtools/auto 即可:

ts
// 任意入口 import 一下即可
if (import.meta.env.DEV) {
  void import('@hy-bricks/devtools/auto')
}

@hy-bricks/devtools/autopackage.json exports 里的子路径,带 side-effect:import 它就会无条件尝试启动诊断。它本身不判 DEV —— SDK 以 library 形式构建,import.meta.env.DEV 在 lib 产物里被固定求值为 false,SDK 自己判会永远是 prod,所以守门交给宿主,宿主在 import 之前用自己的 import.meta.env.DEV 包一层。

它从 window.__HYPERCARD__.canvases 自动拿画布注册表(createHyperCard() 会自动挂到 window),所以宿主要先 app.use(createHyperCard({...}))。考虑到启动顺序,auto 内部会短轮询(50ms × 60 次,3s 超时)等 registry 出现,超时只打一行 console.debug 不抛错。

如果你不想用 side-effect import,也可以显式调便利函数:

ts
import { autoEnable } from '@hy-bricks/devtools'

if (import.meta.env.DEV) {
  await autoEnable()
}
API说明
autoEnable()() => Promise<DiagnosticsHandle | null>。短轮询等 window.__HYPERCARD__.canvases,出现就调零参 enableHyBricksDiagnostics()(全默认值);超时返 null,不抛
autoEnableInDev()autoEnable 的别名(已 deprecated,内部转发到 autoEnable),保留兼容,后续移除

需要自定义 getHandle / 提高 level 时,不要用 auto,改用下面的 enableHyBricksDiagnostics

手动接入:enableHyBricksDiagnostics

主入口。调用一次就装好 collector + 浮窗,返回一个 DiagnosticsHandle

ts
import { enableHyBricksDiagnostics } from '@hy-bricks/devtools'

if (import.meta.env.DEV) {
  enableHyBricksDiagnostics({ level: 'debug' })
}

所有字段都可选,零参 enableHyBricksDiagnostics() 即可跑(全默认值 = DEV 友好)。

ts
interface DiagnosticsConfig {
  canvases?: CanvasesRegistryAPI
  getHandle?: (canvasId: string) => CanvasHandle | null
  ui?: boolean
  level?: DiagnosticsLevel
  probes?: ReadonlyArray<DiagnosticsProbeName>
  captureDomRects?: boolean
}
字段必填默认值说明
canvases内部 createCanvasesRegistry()多画布运行时注册表(从 @hy-bricks/core 拿)。不传 SDK 内部 fallback —— 它是同一份 @hy-bricks/core 全局 instanceRegistry只读视图,不是独立内存池,跟宿主自己 create 的看同一份数据。复杂多画布场景建议显式传同一个 registry
getHandle走 canvas 包内置 designer registry(canvasId) => CanvasHandle | null,把宿主持有的 designer handle 按 id 喂回来,三个 probe 才能干活。默认从 canvas 包的 designer registry 取(designer 会自动注册),宿主零接入也能拿到 handle;返 null 时该画布的 probe 返"no-handle"报告
uitrue是否挂浮窗。false 时只有 collector 数据流、不挂 UI
level'debug''silent' | 'error' | 'warn' | 'info' | 'debug''debug' 时守门失败事件会额外落 console.debug
probes['hit-test', 'drag', 'render', 'binding-trace']启用哪些 probe。Events(交互事件流)/ 运行时错误流不在 probes 里 —— 它们在 collector 内固定启动
captureDomRectstruehit-test probe 是否捕获 wrapper 的 DOM rect(cachedBox / liveBox 展示字段)

canvases 何时该显式传(不是"否则看不到画布"):

  1. 语义清晰 —— collector 跟宿主共享同一对象引用,debug 时一眼对得上
  2. 初始化顺序可控 —— 宿主可提前建 registry,不依赖 @hy-bricks/core 全局已初始化
  3. 未来防分叉 —— 宿主自定义 registry(过滤 / 多版本 core / 微前端子应用各自持副本)时,显式传杜绝 SDK 跟宿主看到不同视图
ts
import { createCanvasesRegistry } from '@hy-bricks/core'

const canvases = createCanvasesRegistry() // 宿主 runtime 自己也用这份
enableHyBricksDiagnostics({ canvases, level: 'debug' })

返回 DiagnosticsHandle

ts
const handle = enableHyBricksDiagnostics({ level: 'debug' })
handle.dispose()              // 关掉诊断器(多次调 idempotent)
const json = handle.dumpJson() // 一键导出全状态 + 三 inspect 快照,复制进 bug report

重复调用 enableHyBricksDiagnostics单例语义:旧 handle 先自动 dispose(),再生成新的,宿主一般不用主动 dispose。

DiagnosticsHandle 关键成员:

成员说明
inspectHitTest(canvasId, canvasPt?)给个 canvas 坐标,返该点命中诊断(候选 / winner / 分叉点 / reason)
inspectDrag(canvasId, instanceId)该实例的 layoutMode / lockState / canReparent dry-run 采样
inspectRender(canvasId)各实例 status / layoutIssues / compile cache / 渲染开销汇总
getPageDocument(canvasId)该画布 PageDocument 快照(designer 优先,运行态 fallback,都没有返 null)
recentReparentFailuresRef<readonly CannotReparentEvent[]>,reparent 守门失败事件流
recentDragFailuresRef<readonly CannotDragLayoutManagedChildEvent[]>,layout-managed child 拖动失败事件流
recentRuntimeErrorsRef<readonly RuntimeErrorRecord[]>,运行时错误流(ErrorBoundary 抓到的 mount/render 异常)
recentInteractionsRef<readonly InteractionEventRecord[]>,交互事件流(call / emit / setProp / setDataInput)
recentBindingTracesRef<readonly BindingTraceEvent[]>,数据绑定 trace 流
selectionVersionRef<number>,选中态版本号(可 watch 后自动刷新诊断)
canvasIdsRef<readonly string[]>,当前注册画布 id 快照
refreshCanvasIds()手动刷新 canvasIds(面板早于画布 mount 时用)
levelRef<DiagnosticsLevel>,运行时可改
uiEnabledRef<boolean>,浮窗显隐开关
dumpJson() / dispose()导出 bug report / 销毁

不需要浮窗、只要数据流的宿主可以关 UI 后自己订阅:

ts
import { watch } from 'vue'

const handle = enableHyBricksDiagnostics({ canvases, ui: false })
watch(handle.recentReparentFailures, (events) => {
  // 自己上报 Sentry / Datadog
})

getActiveDiagnosticsHandle

ts
import { getActiveDiagnosticsHandle } from '@hy-bricks/devtools'

const handle = getActiveDiagnosticsHandle() // 当前活动 handle 或 null

为外部插件 / 扩展预留,SDK 主流程不依赖它。

prod 可选启用

prod 不该默认开浮窗。常见做法是用 URL 参数或全局开关守门:

ts
if (window.location.search.includes('_hcdebug=1')) {
  enableHyBricksDiagnostics({ level: 'debug' })
}

浮窗自身锁了 isolation: isolate,不会被宿主 dialog / popover 反向覆盖。

浮窗能力一览

ui: true(默认)时,SDK 在 <body> 挂一个独立 Vue app 渲染浮窗,跟宿主 app 隔离,dispose() 时一并 unmount + 清 DOM。浮窗包含:

  • 多 Tab:总览 / 命中 / 拖动 / 渲染(实例计数 + 渲染开销 p50/p95/max + 运行时错误)/ Trace(数据绑定)/ Events(交互事件流)
  • Instance Inspect:选中单实例看其协议字段 / timings / 错误 / 事件
  • DOM click inspect:在页面上点元素,反查并定位到对应实例

头部可拖拽、双击折成图标,位置 / 尺寸 / 折叠态走 localStorage 持久化。

低层 API:自组装诊断

想跳过浮窗、自己接第三方面板,或把诊断数据塞进自有监控的宿主,可以直接用 collector + probe 纯函数。

createDiagnosticsCollector

ts
import { createDiagnosticsCollector } from '@hy-bricks/devtools'

const collector = createDiagnosticsCollector({
  canvases,                                  // CanvasesRegistryAPI,必填
  getHandle: (id) => myHandles[id] ?? null,  // 可选
  level: 'debug',                            // DiagnosticsLevel,必填
  probes: ['hit-test', 'drag', 'render'],    // ReadonlyArray<DiagnosticsProbeName>,必填
  captureDomRects: true,                     // boolean,必填
})

DiagnosticsCollectorDiagnosticsHandle 的核心子集 —— 同样有 inspectHitTest / inspectDrag / inspectRender / getPageDocument、各 recent* reactive ring buffer、dumpJson() / dispose()enableHyBricksDiagnostics 本质就是 "collector + 浮窗 + 一组 registry 订阅" 的封装。

注意 DiagnosticsCollectorOptions 的字段无默认值(canvases / level / probes / captureDomRects 都必填),便利默认值是 enableHyBricksDiagnostics 那一层补的。

RingBuffer

collector 内部用的固定容量 FIFO,溢出 evict 最早的。也单独导出供自建数据流复用:

ts
import { RingBuffer } from '@hy-bricks/devtools'

const ring = new RingBuffer<MyEvent>(20) // capacity 默认 20,必须 > 0
ring.push(ev)
ring.toArray() // readonly 浅拷快照
ring.size
ring.clear()

probe 纯函数

三个 probe 是无副作用纯函数,直接喂 CanvasHandle 就能拿到一次性诊断报告(自组装面板、单测、临时排查都能用):

ts
import { inspectHitTest, inspectDrag, inspectRender } from '@hy-bricks/devtools'

const hit = inspectHitTest(handle, canvasPt, {
  captureDomRects: true,
  selectedIds: handle.selectedIds.value, // 不传 canvasPt 时优先用选中实例中心点
})
const drag = inspectDrag(handle, instanceId)
const render = inspectRender(canvasId, handle) // designer handle 可选;不传走运行态 fallback
函数签名说明
inspectHitTest(handle, canvasPt, opts: HitTestProbeOptions) => HitTestReportcanvasPt 为 canvas 坐标系的点(宿主用 clientToCanvasPoint 转好再传)
inspectDrag(handle, instanceId) => DragReport——
inspectRender(canvasId, designerHandle?) => RenderReportdesignerHandle 省略 / 为 null 时 fallback 到运行态 RendererDiagnosticsHandle

HitTestProbeOptions:

字段类型说明
captureDomRectsboolean是否捕获 wrapper DOM rect 到 cachedBox / liveBox
domBoxLookup?(instanceId) => Rect | nullDOM box 查询 callback,仅用于展示字段,不参与命中判定
domScope?HTMLElement | nullwrapper 查询 scope,默认 document.body
selectedIds?readonly string[]不传 canvasPt 时优先用选中实例中心点

report 类型速查

所有 report 都是值快照(可直接 JSON 序列化),不带 Vue ref。

HitTestReport —— 命中诊断

字段类型说明
winnerIdstring | nullSDK 真实命中实例;null = 完全没命中
candidatesHitTestCandidate[]所有落在 box 内的实例,含 hidden / locked(看全貌)
forkPointHitTestForkPoint | nullwinner 跟"第二名"的分叉点;只有 1 个候选时 null
reasonstring人话:为什么 winner 赢 / 为什么没命中
queryPointPoint查询的点(canvas 坐标系)

HitTestCandidateid / componentId / placement / hidden / locked / path(root→self id 链)/ instRect / cachedBox / liveBox / wrapperInStage / wrapperInCanvasDesigner

DragReport —— 拖动 / reparent 诊断

字段类型说明
instanceIdstring——
existsboolean实例存在性;false 时其余字段为默认值
placementDiagnosticsPlacement归一后的 placement
layoutModeContainerLayoutMode | 'none'实例自己作为容器的内部布局模式
parentLayoutModeContainerLayoutMode | 'none'父容器布局模式 —— 决定子能不能自由拖
modestring当前画布全局 mode(design / preview / readonly,或 no-handle / disabled / disposed)
toolModestring当前工具模式
lockedDragLockState{ position, size, effective }
canReparentSamplesDragReparentSample[]对 root / self / 父 / 选中 等目标的 canReparent dry-run 结果

RenderReport —— 渲染诊断

字段类型说明
instancesRenderInstanceRecord[]各实例 status / layoutIssue? / timings?
globalIssuesLayoutIssue[]handle.getLayoutIssues() 透传
compileCacheSizenumber全局 compile cache 大小
schedulerStats?RenderSchedulerStatsSnapshot{ pending, mounting, mounted, disposing }(运行态路径才有)
timingsSummary?RenderTimingsSummary本画布 mountMs 汇总:sampleCount / mountMsP50 / mountMsP95 / mountMsMax / slowCount(mountMs > 16ms 计入)

RenderInstanceStatus:'ok' | 'missing-source' | 'compile-pending' | 'layout-error'RenderInstanceTimings:compileStartedAt / compileEndedAt / mountStartedAt / mountEndedAt / compileMs / mountMs / totalMs(单位 ms,均 optional)。

其它导出类型

DiagnosticsLevel / DiagnosticsProbeName / DiagnosticsPlacement / HitTestCandidate / HitTestForkPoint / DragLockState / DragReparentSample / RenderInstanceRecord / RenderInstanceStatus / RenderInstanceTimings / RenderSchedulerStatsSnapshot / RenderTimingsSummary,以及 RuntimeErrorRecord(message / stack? / name / count 去重计数等)、InteractionEventRecord(kind / key / args / timestamp)均从包根导出。

UI 组件清单

想自渲面板(不用内置浮窗、但复用 SDK 的 inspector 视图)的话,所有 UI 组件都从包根导出。它们统一通过 collector prop 接 DiagnosticsHandle(命中 / 拖动 / 渲染 / Instance 这几个还需要 canvasId prop):

组件一句话
DevtoolsPanel整个浮窗外壳(多 Tab + 拖拽 + 持久化),enableHyBricksDiagnostics 内部挂的就是它
OverviewInspector总览 Tab:画布列表 + 实例计数 + 全局 issues / 错误概览
HitTestInspector命中 Tab:inspectHitTest 结果可视化
DragInspector拖动 Tab:inspectDrag 结果 + 守门失败事件流
RenderInspector渲染 Tab:inspectRender 结果 + 渲染开销 + 运行时错误
BindingTraceInspectorTrace Tab:数据绑定事件流 + filter / clear
ChipBadge共享:小号状态标签
JsonViewer共享:可折叠 JSON 树查看器
DataTable共享:轻量表格
createMockCollector()返回一个填了假数据的 DiagnosticsHandle,给 UI 组件做 storybook / 单测 / 离线预览用

UI 子树独立于 collector 实现,只认 DiagnosticsHandle 接口形状,因此自建 collector 也能直接喂给这些组件。