@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/devtoolspeerDeps: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/auto 是 package.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"报告 | |
ui | true | 是否挂浮窗。false 时只有 collector 数据流、不挂 UI | |
level | 'debug' | 'silent' | 'error' | 'warn' | 'info' | 'debug'。'debug' 时守门失败事件会额外落 console.debug | |
probes | ['hit-test', 'drag', 'render', 'binding-trace'] | 启用哪些 probe。Events(交互事件流)/ 运行时错误流不在 probes 里 —— 它们在 collector 内固定启动 | |
captureDomRects | true | hit-test probe 是否捕获 wrapper 的 DOM rect(cachedBox / liveBox 展示字段) |
canvases 何时该显式传(不是"否则看不到画布"):
- 语义清晰 —— collector 跟宿主共享同一对象引用,debug 时一眼对得上
- 初始化顺序可控 —— 宿主可提前建 registry,不依赖
@hy-bricks/core全局已初始化 - 未来防分叉 —— 宿主自定义 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) |
recentReparentFailures | Ref<readonly CannotReparentEvent[]>,reparent 守门失败事件流 |
recentDragFailures | Ref<readonly CannotDragLayoutManagedChildEvent[]>,layout-managed child 拖动失败事件流 |
recentRuntimeErrors | Ref<readonly RuntimeErrorRecord[]>,运行时错误流(ErrorBoundary 抓到的 mount/render 异常) |
recentInteractions | Ref<readonly InteractionEventRecord[]>,交互事件流(call / emit / setProp / setDataInput) |
recentBindingTraces | Ref<readonly BindingTraceEvent[]>,数据绑定 trace 流 |
selectionVersion | Ref<number>,选中态版本号(可 watch 后自动刷新诊断) |
canvasIds | Ref<readonly string[]>,当前注册画布 id 快照 |
refreshCanvasIds() | 手动刷新 canvasIds(面板早于画布 mount 时用) |
level | Ref<DiagnosticsLevel>,运行时可改 |
uiEnabled | Ref<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,必填
})DiagnosticsCollector 是 DiagnosticsHandle 的核心子集 —— 同样有 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) => HitTestReport | canvasPt 为 canvas 坐标系的点(宿主用 clientToCanvasPoint 转好再传) |
inspectDrag | (handle, instanceId) => DragReport | —— |
inspectRender | (canvasId, designerHandle?) => RenderReport | designerHandle 省略 / 为 null 时 fallback 到运行态 RendererDiagnosticsHandle |
HitTestProbeOptions:
| 字段 | 类型 | 说明 |
|---|---|---|
captureDomRects | boolean | 是否捕获 wrapper DOM rect 到 cachedBox / liveBox |
domBoxLookup? | (instanceId) => Rect | null | DOM box 查询 callback,仅用于展示字段,不参与命中判定 |
domScope? | HTMLElement | null | wrapper 查询 scope,默认 document.body |
selectedIds? | readonly string[] | 不传 canvasPt 时优先用选中实例中心点 |
report 类型速查
所有 report 都是值快照(可直接 JSON 序列化),不带 Vue ref。
HitTestReport —— 命中诊断
| 字段 | 类型 | 说明 |
|---|---|---|
winnerId | string | null | SDK 真实命中实例;null = 完全没命中 |
candidates | HitTestCandidate[] | 所有落在 box 内的实例,含 hidden / locked(看全貌) |
forkPoint | HitTestForkPoint | null | winner 跟"第二名"的分叉点;只有 1 个候选时 null |
reason | string | 人话:为什么 winner 赢 / 为什么没命中 |
queryPoint | Point | 查询的点(canvas 坐标系) |
HitTestCandidate 含 id / componentId / placement / hidden / locked / path(root→self id 链)/ instRect / cachedBox / liveBox / wrapperInStage / wrapperInCanvasDesigner。
DragReport —— 拖动 / reparent 诊断
| 字段 | 类型 | 说明 |
|---|---|---|
instanceId | string | —— |
exists | boolean | 实例存在性;false 时其余字段为默认值 |
placement | DiagnosticsPlacement | 归一后的 placement |
layoutMode | ContainerLayoutMode | 'none' | 实例自己作为容器的内部布局模式 |
parentLayoutMode | ContainerLayoutMode | 'none' | 父容器布局模式 —— 决定子能不能自由拖 |
mode | string | 当前画布全局 mode(design / preview / readonly,或 no-handle / disabled / disposed) |
toolMode | string | 当前工具模式 |
locked | DragLockState | { position, size, effective } |
canReparentSamples | DragReparentSample[] | 对 root / self / 父 / 选中 等目标的 canReparent dry-run 结果 |
RenderReport —— 渲染诊断
| 字段 | 类型 | 说明 |
|---|---|---|
instances | RenderInstanceRecord[] | 各实例 status / layoutIssue? / timings? |
globalIssues | LayoutIssue[] | handle.getLayoutIssues() 透传 |
compileCacheSize | number | 全局 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 结果 + 渲染开销 + 运行时错误 |
BindingTraceInspector | Trace Tab:数据绑定事件流 + filter / clear |
ChipBadge | 共享:小号状态标签 |
JsonViewer | 共享:可折叠 JSON 树查看器 |
DataTable | 共享:轻量表格 |
createMockCollector() | 返回一个填了假数据的 DiagnosticsHandle,给 UI 组件做 storybook / 单测 / 离线预览用 |
UI 子树独立于 collector 实现,只认 DiagnosticsHandle 接口形状,因此自建 collector 也能直接喂给这些组件。