@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 | 类型 | 说明 |
|---|---|---|
payload | PageRenderPayload | /render 聚合接口返回值;优先级高于 document |
document | PageDocument | 设计器内嵌场景或宿主已拆开的页面结构 |
componentSourceMap | ComponentSourceMap | componentVersionKey -> source asset 字典 |
schedulerOptions | RenderSchedulerOptions | 渐进挂载、视口缓冲、dispose 延迟等配置 |
canvasId | string | 多画布运行时身份;业务 v-for 场景必须稳定且唯一 |
adapters | CanvasAdapters | 独立 Renderer 场景的资源解析 adapter |
keepHiddenMounted | boolean | 0.6.2 起,默认 true。隐藏(instance.hidden)实例保活:隐藏不卸载、组件状态保留,恢复显示无缝;false 退回老行为(隐藏可被 dispose,省内存但恢复时丢状态) |
persistInstanceState | boolean | 0.6.3 起,默认 false(opt-in)。开启后:实例被真销毁(滚出视口虚拟化 dispose)前拍 $data 可序列化快照,重建时还原,使滚出 → 滚回保留用户选值 / 查询条件 |
payload 和 document + 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 | 类型 | 说明 |
|---|---|---|
modelValue | PageDocument | v-model 页面结构;内部 mutation 会 emit snapshot |
componentSourceMap | ComponentSourceMap | 宿主拼好的源码字典 |
mode | CanvasMode | design / preview / runtime / inspect,默认 design |
schedulerOptions | RenderSchedulerOptions | 透传给内部 Renderer |
adapters | CanvasAdapters | 设计器统一资源 adapter;内嵌 Renderer 会优先使用它 |
persistInstanceState | boolean | 0.6.3 起,默认 false(opt-in)。透传给内嵌 Renderer:实例被真销毁前拍快照、重建时还原。语义见 Renderer 运行时规则 |
Emits
| 事件 | Payload | 说明 |
|---|---|---|
update:modelValue | PageDocument | 文档快照变更;drag/resize batch 结束后只 emit 一次 |
handle-ready | CanvasHandle | 宿主集成属性面板、工具栏、快捷键的主入口 |
context-ready | CanvasContext | 兼容旧事件;新接入不要依赖内部 context |
cannot-drag-slot-child | { instanceId, parentId } | 旧 slot child 禁拖位事件 |
cannot-drag-layout-managed-child | { instanceId, parentId, parentLayoutMode } | flex/grid/split 受管实例被尝试拖位 |
Slots
| Slot | Props | 说明 |
|---|---|---|
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:
| 字段 | 类型 | 说明 |
|---|---|---|
canvasId | string | 本 ctx 绑定的画布身份;不传走默认 scope,用于过滤 per-canvas 的 instance:* 生命周期事件 |
initialDocument | PageDocument | 初始文档,不传时空文档 |
scheduler | RenderSchedulerOptions | 透传给内部 createRenderScheduler |
viewport | ViewportStoreOptions | 缩放范围、初始缩放 |
adapters | CanvasAdapters | 业务 adapter 槽位 |
hooks | CanvasActionHooks | action 生命周期 hooks(command stack / undo / dirty / 埋点) |
getMode | () => CanvasMode | lazy 拿当前 mode,派生默认 behavior 用;不传走 design |
freeSplitEnabled | boolean | (() => boolean | undefined) | per-canvas free-split 编辑门控;不传回落构建默认 |
CanvasAdapters 是宿主接入业务能力的槽位:
| 字段 | 类型 | 说明 |
|---|---|---|
data | DataAdapter | 数据接入(Pull 模式);不传时数据绑定 emit no-data-adapter 并跳过 |
assets | AssetsAdapter | 静态资源解析;不传则直接走 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 | null | lazy 拿 stage 外层 DOM;clientToCanvasPoint 用它的 rect 做坐标换算 |
recorder | { maxSize?: number } | 命令栈选项(undo 栈上限) |
resolveContract | ContractResolver | componentVersionKey → 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(): PageDocumentDocumentStore: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:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
rootMargin | string | '500px' | 视口缓冲距离(IntersectionObserver rootMargin) |
disposeDelayMs | number | 1500 | 出视口多少 ms 才真 dispose;之内回到视口直接 cancel |
disposeOnExit | boolean | true | 0.6.6 起。false = 渐进挂载但不卸载:仍进视口才分帧挂,但已挂的滚出视口不再 dispose、永久常驻(永不重挂 = 不重渲染、不触发宿主重取)。代价:已挂常驻内存,大画布别用 |
mountConcurrency | number | 2 | 每个 idle frame 最多挂几个实例 |
root | Element | null | (() => Element | null) | null(视口) | IntersectionObserver 的 root(判断"实例可见"的参照系)。0.6.5 起接受 getter:渲染器嵌进宿主自有滚动容器时传 () => ref.value(裸 ref.value 会在调度器创建期退化为视口) |
disabled | boolean | false | 0.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。