Skip to content

@hy-bricks/canvas

@hy-bricks/canvas 是页面渲染器和嵌入式设计器。它只消费宿主传入的数据,不直接请求后端。

公开组件

组件用途
HyperCardPageRenderer只读运行态渲染器,适合业务页面、预览页、外部 demo
HyperCardCanvasDesigner默认整机设计器,内含 stage、runtime layer、interaction layer 和 handle
CanvasStage低层舞台容器,高级宿主自组装时使用
RuntimeLayer低层运行时层,从 CanvasContext 读文档并嵌入 Renderer
InteractionLayer低层交互层,负责选中、拖拽、resize、reparent ghost

常规业务只需要前两个组件。后三个是高级拼装 API,用于宿主需要替换默认设计器结构时。

HyperCardPageRenderer

运行态入口:

vue
<HyperCardPageRenderer
  :payload="payload"
  canvas-id="dashboard-main"
  :scheduler-options="{ mountConcurrency: 4 }"
  :adapters="adapters"
/>

Props

Prop类型说明
payloadPageRenderPayload/render 聚合接口返回值;优先级高于 document
documentPageDocument设计器内嵌场景或宿主已拆开的页面结构
componentSourceMapComponentSourceMapcomponentVersionKey -> source asset 字典
schedulerOptionsRenderSchedulerOptions渐进挂载、视口缓冲、dispose 延迟等配置
canvasIdstring多画布运行时身份;业务 v-for 场景必须稳定且唯一
adaptersCanvasAdapters独立 Renderer 场景的资源解析 adapter
keepHiddenMountedboolean0.6.2 起,默认 true。隐藏(instance.hidden)实例保活:隐藏不卸载、组件状态保留,恢复显示无缝;false 退回老行为(隐藏可被 dispose,省内存但恢复时丢状态)
persistInstanceStateboolean0.6.3 起,默认 false(opt-in)。开启后:实例被真销毁(滚出视口虚拟化 dispose)前拍 $data 可序列化快照,重建时还原,使滚出 → 滚回保留用户选值 / 查询条件

payloaddocument + componentSourceMap 二选一。运行页推荐 payload;设计器内部会用 document + componentSourceMap

运行时规则

  • Renderer 不 fetch。
  • Renderer 不挂交互层。
  • canvasId 在组件生命周期内固定;想换画布身份应通过 Vue :key 重建。
  • 隐藏实例(hidden)默认保活(keepHiddenMounted)—— 隐藏 → 恢复 不丢组件内部状态(用户选值 / 查询条件 / 取数结果);只 root 实例受此影响(容器子本就一直保活)。
  • persistInstanceState(默认 false)与 keepHiddenMounted 互补:keepHiddenMounted 让隐藏实例 vm 不被拆,根本不丢状态、不需快照;persistInstanceState 针对实例真被拆掉的场景(滚出视口超 disposeDelayMs 被 dispose、滚回重建)——拍快照、重建时还原。一句话:hidden 走保活(vm 不拆),scroll-out 拆了才用快照。
  • canvas.height.mode = 'fill' 时,外层容器必须有明确高度,否则画布不可见。
  • componentVersions[key].status !== 'ok' 时渲染降级卡,不让整页崩溃。

HyperCardCanvasDesigner

设计器入口:

vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  HyperCardCanvasDesigner,
  type CanvasHandle,
  type ComponentSourceMap,
  type PageDocument,
} from '@hy-bricks/canvas'

const document = ref<PageDocument>(initialDocument)
const sourceMap = ref<ComponentSourceMap>(initialSourceMap)
const handle = ref<CanvasHandle | null>(null)
</script>

<template>
  <HyperCardCanvasDesigner
    v-model="document"
    :component-source-map="sourceMap"
    mode="design"
    @handle-ready="handle = $event"
  />
</template>

Props

Prop类型说明
modelValuePageDocumentv-model 页面结构;内部 mutation 会 emit snapshot
componentSourceMapComponentSourceMap宿主拼好的源码字典
modeCanvasModedesign / preview / runtime / inspect,默认 design
schedulerOptionsRenderSchedulerOptions透传给内部 Renderer
adaptersCanvasAdapters设计器统一资源 adapter;内嵌 Renderer 会优先使用它
persistInstanceStateboolean0.6.3 起,默认 false(opt-in)。透传给内嵌 Renderer:实例被真销毁前拍快照、重建时还原。语义见 Renderer 运行时规则

Emits

事件Payload说明
update:modelValuePageDocument文档快照变更;drag/resize batch 结束后只 emit 一次
handle-readyCanvasHandle宿主集成属性面板、工具栏、快捷键的主入口
context-readyCanvasContext兼容旧事件;新接入不要依赖内部 context
cannot-drag-slot-child{ instanceId, parentId }旧 slot child 禁拖位事件
cannot-drag-layout-managed-child{ instanceId, parentId, parentLayoutMode }flex/grid/split 受管实例被尝试拖位

Slots

SlotProps说明
canvas-overlay{ viewport, handle }渲染在 stage world 内,自动跟随 pan/scale
default保留给高级宿主塞同层扩展

canvas-overlay 适合画 palette 拖入 ghost、业务辅助线、协作光标。它已经在 canvas 坐标系里,不要再自己乘 viewport scale。

Expose

字段说明
handle对外稳定的 CanvasHandle
context内部 CanvasContext,兼容旧接入;不建议新代码依赖

模式

Mode行为
design完整编辑;选中、拖拽、resize、键盘移动、undo 可用
preview组件内部交互可用,不修改文档
runtime纯运行态;不挂 InteractionLayer
inspect可选中查看,但不拖不改

mode 是会话级。工具级状态走 handle.toolMode / setToolMode()

自组装低层组件

默认整机已经覆盖大多数场景。只有当宿主要替换舞台结构或交互层时,才需要低层组件:

vue
<CanvasStage>
  <RuntimeLayer :component-source-map="sourceMap" mode="design" />
  <InteractionLayer mode="design" />
  <MyOverlay />
</CanvasStage>

自组装必须自己创建并 provide CanvasContext,并负责 reparent bridge、handle 生命周期等细节。业务项目优先使用 HyperCardCanvasDesigner

自组装路径(高级)

下面这一组 API 是给"想自己拼装设计器结构"的宿主用的。大多数业务只需要 <HyperCardCanvasDesigner>(整机设计器)和 <HyperCardPageRenderer>(只读运行态),不需要碰这一层。整机已经把状态层、context、handle、生命周期都装好了。

只有当你要替换默认舞台/交互结构,或在非 Vue 组件树外消费画布状态时,才需要这些工厂。

createCanvasContext

CanvasContext 是设计器内部的"广播站":状态层(document / selection / viewport store)、渲染调度、adapter 槽位都挂在它上面,各 UI 模块通过 Vue provide / inject 共享它,而不互相 import。

ts
function createCanvasContext(opts?: CanvasContextOptions): CanvasContext

function provideCanvasContext(ctx: CanvasContext): void
function useCanvasContext(): CanvasContext

const CANVAS_CONTEXT_KEY: InjectionKey<CanvasContext>

CanvasContextOptions:

字段类型说明
canvasIdstring本 ctx 绑定的画布身份;不传走默认 scope,用于过滤 per-canvas 的 instance:* 生命周期事件
initialDocumentPageDocument初始文档,不传时空文档
schedulerRenderSchedulerOptions透传给内部 createRenderScheduler
viewportViewportStoreOptions缩放范围、初始缩放
adaptersCanvasAdapters业务 adapter 槽位
hooksCanvasActionHooksaction 生命周期 hooks(command stack / undo / dirty / 埋点)
getMode() => CanvasModelazy 拿当前 mode,派生默认 behavior 用;不传走 design
freeSplitEnabledboolean | (() => boolean | undefined)per-canvas free-split 编辑门控;不传回落构建默认

CanvasAdapters 是宿主接入业务能力的槽位:

字段类型说明
dataDataAdapter数据接入(Pull 模式);不传时数据绑定 emit no-data-adapter 并跳过
assetsAssetsAdapter静态资源解析;不传则直接走 AssetRef.url

provideCanvasContext / useCanvasContext 是子组件树的 provide/inject 对;useCanvasContext 必须在 setup 同步上下文调用,没有 provider 时抛错。CANVAS_CONTEXT_KEY 是底层 injection key,一般不直接用。

ts
// 在自组装的顶层组件 setup 里
const ctx = createCanvasContext({ initialDocument: props.modelValue })
provideCanvasContext(ctx)

// 在子模块(自定义属性面板)setup 里
const ctx = useCanvasContext()

createCanvasHandle

createCanvasHandle 把内部 CanvasContext 精简成对外稳定的 CanvasHandle。整机设计器内部就是这么造 handle 再 @handle-ready 抛出来的;自组装时你需要自己造一个。

ts
function createCanvasHandle(
  ctx: CanvasContext,
  options: CanvasHandleOptions,
): CanvasHandle & { dispose(): void }

CanvasHandleOptions:

字段类型说明
getStageEl() => HTMLElement | nulllazy 拿 stage 外层 DOM;clientToCanvasPoint 用它的 rect 做坐标换算
recorder{ maxSize?: number }命令栈选项(undo 栈上限)
resolveContractContractResolvercomponentVersionKey → ComponentContract 的 lookup;不传则跳过 contract 维度的 reparent 校验。强烈建议传

返回值额外带一个 dispose():handle 不再使用时调用,释放 binding 订阅、pending 信号、命令栈。方法全集见 CanvasHandle

ts
const handle = createCanvasHandle(ctx, {
  getStageEl: () => stageRef.value?.$el ?? null,
  resolveContract: (key) => sourceMap.value[key]?.contract,
})
onBeforeUnmount(() => handle.dispose())

Headless 状态层

三个状态 store 是纯逻辑(只用 Vue reactive,不引 Pinia / Zustand)。createCanvasContext 内部就是组合它们;宿主也可以单独创建消费,例如在非画布组件里读 / 改文档。

ts
function createDocumentStore(initial?: PageDocumentInput): DocumentStore
function createSelectionStore(): SelectionStore
function createViewportStore(opts?: ViewportStoreOptions): ViewportStore

function emptyPageDocument(): PageDocument
  • DocumentStore:PageDocument 的 reactive 包装 + mutation helper(addInstance / removeInstance / updateInstance / updateLayout 等)。store.document 是只读 reactive,Vue 模板直接订阅。
  • SelectionStore:选中实例集合,暴露 selectedIds / primaryId 计算属性,以及 select / toggleSelect / addToSelection 等写口。
  • ViewportStore:缩放 + 平移 + 坐标换算(toCanvasPoint / toViewportPoint);ViewportStoreOptions 控制 minScale(默认 0.25)/ maxScale(默认 4)/ initialScale

emptyPageDocument() 造一份最小空 v1 文档(canvas 默认 fill 模式),createDocumentStore() 不传初值时就用它。需要直接喂 Renderer / Designer 的合法文档工厂请用 createMinimalPageDocument / createFreePageDocument(见 快速上手)。

ts
const docStore = createDocumentStore()
docStore.addInstance({ instanceId: 'a', componentId: 'btn' /* ... */ })
docStore.document.instances.length // 1(reactive)

normalizePageDocument

ts
function normalizePageDocument(raw: PageDocumentInput): PageDocument

把宿主灌入的文档归一化到 v1 形态:v0 → v1 layout 升级、补齐 parentId / slot / layoutBox / rootLayout 等派生字段、bindings 投影成规范形态。幂等:normalize(normalize(x)) === normalize(x)

整机路径不用手动调——HyperCardPageRenderer / HyperCardCanvasDesigner / createDocumentStore / createCanvasContext 在入口都已各调一次。需要手动调的场景:

  • 自组装时直接操作 store / context 之外的原始 PageDocument,想先归一再用。
  • 宿主自己持久化文档,想在写库前统一成 v1 形态。

后端 /render 返回的 payload normalize 之后已是 v1,业务页面一般无需手动调用。

isFreeSplitEnabled

ts
function isFreeSplitEnabled(): boolean

读取 free-split 自由分割布局的编辑门控构建默认值。free-split 协议默认关闭:关闭态下类型 / normalize / validate 认得 mode: 'free-split',但不产出、也不修整这类持久数据,渲染降级为平铺。per-canvas 门控可通过 CanvasContextOptions.freeSplitEnabled 覆盖;不传时回落到本函数。布局语义见 Free-split 布局

createRenderScheduler

ts
function createRenderScheduler(opts?: RenderSchedulerOptions): RenderScheduler

渐进 mount 调度器:用 IntersectionObserver 按需渲染、requestIdleCallback 每帧只挂少量实例、出视口延迟 dispose,让单页渲染大量实例时主线程不堵、滚动不抖。HyperCardPageRenderer / CanvasContext 内部自带一个;只有自组装渲染管线时才需要直接创建。

RenderSchedulerOptions:

字段类型默认说明
rootMarginstring'500px'视口缓冲距离(IntersectionObserver rootMargin)
disposeDelayMsnumber1500出视口多少 ms 才真 dispose;之内回到视口直接 cancel
disposeOnExitbooleantrue0.6.6 起false = 渐进挂载但不卸载:仍进视口才分帧挂,但已挂的滚出视口不再 dispose、永久常驻(永不重挂 = 不重渲染、不触发宿主重取)。代价:已挂常驻内存,大画布别用
mountConcurrencynumber2每个 idle frame 最多挂几个实例
rootElement | null | (() => Element | null)null(视口)IntersectionObserver 的 root(判断"实例可见"的参照系)。0.6.5 起接受 getter:渲染器嵌进宿主自有滚动容器时传 () => ref.value(裸 ref.value 会在调度器创建期退化为视口)
disabledbooleanfalse0.6.5 起true = 关掉视口虚拟化:所有实例立即且永久 mounted,不建 IO、不渐进、永不 dispose(setHeld/refreshRoot 为 no-op)。少量实例 / 嵌宿主滚动容器用,不适用大画布

RenderScheduler 接口:register(id, el) / unregister(id) 管理监听,isMounted(id) 决定渲染真 RuntimeBox 还是占位,getState(id) 返回 MountState(idle / pending / mounting / mounted / disposing),getStats() 返回 RenderSchedulerStats(各状态 O(1) 计数),refreshRoot() 重新解析 root getter 并按需重建 IO(0.6.5 起;渲染器 onMounted 自动调一次,消解 root 时序坑),dispose() 清监听 + 队列 + timer。

怎么选(卸载策略 × "可见"参照系)

你想要配置
大画布省内存:滚出卸载、滚回重挂(会重渲染 + 触发宿主重取)默认(disposeOnExit: true)
往下滚到哪块才加载哪块(跨页懒加载)+ 绝不重取,代价是已挂常驻内存{ disposeOnExit: false },且不传 root(按浏览器窗口算可见)
嵌进宿主自有滚动容器、要"整页滚动不卸载",可接受容器内部滚动时卸载/重取{ root: () => 容器ref.value }(默认卸载策略)
少量实例图省事:立即全挂、永不卸{ disabled: true }

root 是"判断可见"的唯一参照系,没法既当"浏览器窗口"(给你跨页懒加载)又当"某容器"(给你整页滚不卸载)—— 二选一。传了 root: () => 容器 后:可见以该容器为准、整页滚动不再卸载,但页面外还没滚到的实例也会提前排队挂(跨页懒加载变弱),且容器内部滚动仍会卸载 → 滚回重取。要"跨页懒加载 + 零重取"就别传 root、配 disposeOnExit: false

ts
const scheduler = createRenderScheduler({ mountConcurrency: 4 })
onMounted(() => scheduler.register(instanceId, wrapperEl))
onBeforeUnmount(() => scheduler.dispose())
// 模板里 v-if="scheduler.isMounted(instanceId)" 决定渲染 RuntimeBox / skeleton

推荐导入

常规业务:

ts
import {
  HyperCardCanvasDesigner,
  HyperCardPageRenderer,
  type CanvasHandle,
  type PageDocument,
  type PageRenderPayload,
  type ComponentSourceMap,
} from '@hy-bricks/canvas'

自组装路径:

ts
import {
  createCanvasContext,
  provideCanvasContext,
  useCanvasContext,
  CANVAS_CONTEXT_KEY,
  createCanvasHandle,
  createDocumentStore,
  createSelectionStore,
  createViewportStore,
  emptyPageDocument,
  createRenderScheduler,
  normalizePageDocument,
  isFreeSplitEnabled,
  type CanvasContext,
  type CanvasContextOptions,
  type CanvasAdapters,
  type CanvasHandleOptions,
  type DocumentStore,
  type SelectionStore,
  type ViewportStore,
  type ViewportStoreOptions,
  type RenderScheduler,
  type RenderSchedulerOptions,
  type RenderSchedulerStats,
  type MountState,
} from '@hy-bricks/canvas'

协议类型见 Canvas Types,宿主操作入口见 CanvasHandle