保存、加载与只读预览
Designer 不自动持久化页面。宿主用 exportSchema() 获取可传输快照,用 importSchema() 恢复外部输入。
仓储边界
贯穿示例使用 revision 保护草稿:
ts
import type { DocumentSchema } from '@dragcraft/designer'
export interface SavedPage {
id: string
revision: number
schema: DocumentSchema
}
export interface SavePageInput {
id: string
revision: number
schema: DocumentSchema
}
export interface PageRepository {
load: (id: string) => Promise<SavedPage | null>
save: (input: SavePageInput) => Promise<SavedPage>
}
export class PageRevisionConflictError extends Error {
constructor(id: string) {
super(`页面 ${id} 已被其他编辑会话更新`)
this.name = 'PageRevisionConflictError'
}
}
function clonePersistedValue<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T
}
export function createMemoryPageRepository(seed: SavedPage[] = []): PageRepository {
const pages = new Map(seed.map(page => [page.id, clonePersistedValue(page)]))
return {
async load(id) {
const page = pages.get(id)
return page ? clonePersistedValue(page) : null
},
async save(input) {
const current = pages.get(input.id)
if (current && current.revision !== input.revision)
throw new PageRevisionConflictError(input.id)
const page: SavedPage = {
id: input.id,
revision: (current?.revision ?? 0) + 1,
schema: clonePersistedValue(input.schema),
}
pages.set(page.id, page)
return clonePersistedValue(page)
},
}
}真实服务校验页面归属、revision、type 白名单、业务 props、资源 URL 与 region 约束。旧 revision 保存必须拒绝,不能覆盖新草稿。
恢复结果
ts
const result = designer.importSchema(draft.schema)
if (result.status === 'rejected')
showDiagnostics(result.diagnostics)degraded 和 conflicted 保留文档,分别提示未知 type 或结构冲突;rejected 保留导入前的有效文档。
独立 Runtime
Runtime 按 type 解释 Schema,不使用 DcDesigner。Guide Project 的参考 Runtime 说明了将导出的纯数据交给宿主组件树的边界:
ts
import type { DocumentSchema, NodeDefinition } from '@dragcraft/designer'
import type { Component, PropType, VNodeChild } from 'vue'
import type { RuntimeLayoutEdge, RuntimeLayoutEntry } from './layout'
import type { RuntimeRegions, RuntimeRegistry } from './registry'
import { defineComponent, h } from 'vue'
import { createFrameworkLayerStyle, createRuntimeLayoutPlan } from './layout'
export const DefaultRuntimeFallback = defineComponent({
name: 'GuideRuntimeFallback',
props: {
node: { type: Object as PropType<NodeDefinition>, required: true },
},
setup(props) {
return () => h('p', {
class: 'guide-runtime-fallback',
role: 'status',
}, `无法渲染物料 ${props.node.type}(${props.node.id})`)
},
})
export function createRuntimeNodeRenderer(
registry: RuntimeRegistry,
schema: DocumentSchema,
fallback: Component = DefaultRuntimeFallback,
): (node: NodeDefinition) => VNodeChild {
const nodesById = new Map(schema.nodes.map(node => [node.id, node]))
const renderNode = (node: NodeDefinition): VNodeChild => {
const definition = registry[node.type]
const container = schema.structure.containers[node.id]
let content: VNodeChild
if (!definition || (container && definition.kind !== 'container')) {
content = h(fallback, { node })
}
else if (definition.kind === 'container') {
if (!container) {
content = h(fallback, { node })
}
else {
const regions = Object.fromEntries(
Object.entries(container.regions).map(([regionId, childIds]) => [
regionId,
childIds.flatMap((childId) => {
const child = nodesById.get(childId)
return child ? [renderNode(child)] : []
}),
]),
) as RuntimeRegions
content = h(definition.component, {
node,
regions,
})
}
}
else {
content = h(definition.component, {
...node.props,
style: node.style?.content,
})
}
return h('div', {
'class': 'guide-runtime-node',
'data-runtime-node-id': node.id,
'data-runtime-node-type': node.type,
'style': node.style?.container,
}, [content])
}
return renderNode
}
function renderEntries(
entries: RuntimeLayoutEntry[],
renderNode: (node: NodeDefinition) => VNodeChild,
): VNodeChild[] {
return entries.map(entry => renderNode(entry.node))
}
function chromeEntries(
entries: RuntimeLayoutEntry[],
edge: RuntimeLayoutEdge,
fixed: boolean,
): RuntimeLayoutEntry[] {
return entries.filter(entry => entry.placement.kind === 'chrome'
&& entry.placement.edge === edge
&& (entry.placement.position === 'fixed') === fixed)
}
export const RuntimePage = defineComponent({
name: 'GuideRuntimePage',
props: {
schema: { type: Object as PropType<DocumentSchema>, required: true },
registry: { type: Object as PropType<RuntimeRegistry>, required: true },
fallback: { type: Object as PropType<Component>, default: () => DefaultRuntimeFallback },
},
setup(props) {
return () => {
const plan = createRuntimeLayoutPlan(props.schema, props.registry)
const renderNode = createRuntimeNodeRenderer(props.registry, props.schema, props.fallback)
const flowRegions = [...plan.flow.entries()].map(([regionId, entries]) => h('section', {
'class': 'guide-runtime-region',
'data-runtime-region': regionId,
}, renderEntries(entries, renderNode)))
const renderChrome = (edge: RuntimeLayoutEdge, fixed: boolean) => {
const entries = chromeEntries(plan.chrome, edge, fixed)
return entries.length === 0
? null
: h('div', {
'class': {
'guide-runtime-edge': true,
'guide-runtime-edge--fixed': fixed,
},
'data-runtime-edge': edge,
}, renderEntries(entries, renderNode))
}
const layerVNodes = [...plan.layers.entries()].flatMap(([layer, entries]) => entries.map((entry) => {
if (entry.placement.kind !== 'layer')
return null
return h('div', {
'class': 'guide-runtime-overlay-entry',
'data-runtime-overlay': layer,
'data-runtime-overlay-mode': entry.placement.mode,
'style': createFrameworkLayerStyle(entry.placement),
}, [renderNode(entry.node)])
}))
return h('main', {
class: 'guide-runtime-page',
style: {
'--guide-runtime-inset-block-start': plan.insets['block-start'],
'--guide-runtime-inset-block-end': plan.insets['block-end'],
'--guide-runtime-inset-inline-start': plan.insets['inline-start'],
'--guide-runtime-inset-inline-end': plan.insets['inline-end'],
},
}, [
h('div', { class: 'guide-runtime-scrollport' }, [
h('div', {
class: 'guide-runtime-surface',
style: props.schema.page.style?.surface,
}, [
renderChrome('block-start', false),
h('div', { class: 'guide-runtime-inline-layout' }, [
renderChrome('inline-start', false),
h('div', { class: 'guide-runtime-content' }, flowRegions),
renderChrome('inline-end', false),
]),
renderChrome('block-end', false),
]),
]),
renderChrome('block-start', true),
renderChrome('block-end', true),
renderChrome('inline-start', true),
renderChrome('inline-end', true),
h('div', { class: 'guide-runtime-overlays' }, layerVNodes),
])
}
},
})小程序、原生应用或其他运行时可消费同一 Schema,但自行实现目标平台组件、布局和未知 type 策略。