@hy-bricks/editor
editor 是组件源码编辑器:嵌入式 Monaco + 可拖拽分屏 + 实时预览。它是受控组件,不是业务后台。
设计前提:编辑器只负责"写代码 + 看预览",拉历史、判权限、存组件库这些全交给宿主。SDK 不主动碰任何后端,版本信息、保存、发布都通过 props/emits 让宿主接管。
ts
import { HyperCardEditor } from '@hy-bricks/editor'HyperCardEditor
主入口组件,v-model 双向绑定一份三段式源码(html / js / css)。
最小用法
vue
<script setup lang="ts">
import { ref } from 'vue'
import { HyperCardEditor, type Source } from '@hy-bricks/editor'
const source = ref<Source>({ html: '', js: '', css: '' })
</script>
<template>
<HyperCardEditor v-model="source" />
</template>v-model
ts
interface Source {
html: string
js: string
css: string
}v-model 绑定的是整份 Source。编辑器内部用内容比对终止 v-model 循环:宿主回灌同一份内容不会重置光标 / dirty 状态。
宿主要换组件源码(切换正在编辑的组件)时,不要只改 v-model 值,用 :key 强制重建:
vue
<HyperCardEditor :key="componentId" v-model="source" />这样 scope、Monaco model、草稿检测都会干净重置。
Props
| Prop | 类型 | 默认 | 说明 |
|---|---|---|---|
modelValue | Source | — | v-model 绑定的源码,必填 |
draftKey | string | — | 启用 localStorage 草稿(key = hc:draft:<draftKey>);留空 = 不启用草稿 + 独立预览窗 |
componentId | string | draftKey 或内部 uid | 给运行时预览实例用的 id |
features | EditorFeatures | — | 入口按钮显隐 / 启用开关 |
versionStatus | VersionStatus | — | 宿主传入的版本摘要,只读、只展示 |
extraLibs | ExtraLib[] | — | 额外 Monaco 类型注入(见下文) |
autoPreview | boolean | true | 是否自动重渲染预览 |
previewRatio | number | 0.62 | 编辑区与预览区初始宽度比 |
showBackButton | boolean | false | 顶栏显示返回按钮 |
title | string | — | 顶栏标题 |
statusLabel | string | — | 顶栏右侧状态文本(遗留,优先用 versionStatus.currentVersionLabel) |
features 控制的是入口是否展示,不控制权限。真正权限由宿主自己判断后再决定 enablePublish 等传不传 true。
ts
interface EditorFeatures {
/** 显示"历史版本"入口 */
showHistory?: boolean
/** 显示"查看改动"入口 */
showDiff?: boolean
/**
* "推版本"入口(三态):
* undefined → 显示(向后兼容)
* true → 显示
* false → 隐藏
*/
enablePublish?: boolean
}versionStatus 是宿主告诉编辑器的版本摘要,编辑器自己不加载:
ts
interface VersionStatus {
/** 当前版本展示名,如 "v1" / "v12" / "draft";undefined = 宿主还没传 */
currentVersionLabel?: string
/**
* 是否有历史版本(三态):
* undefined → 还没加载完,历史 / diff 入口不渲染(避免闪烁)
* false → 确认无历史
* true → 有历史
*/
hasHistory?: boolean
/** 最近发布时间,ISO 8601 字符串;编辑器自己格式化为相对时间 */
lastPublishedAt?: string
}Emits
宿主拿到事件后自己处理(toast / 弹窗 / 调 API / 路由跳转),编辑器不替宿主做副作用。
| 事件 | 载荷 | 说明 |
|---|---|---|
update:modelValue | Source | v-model 回写 |
dirty | boolean | 是否有未保存修改 |
save-draft | — | 点"保存草稿"(有 draftKey 时 SDK 已写 localStorage,宿主再接 toast) |
push-version | — | 点"推版本",宿主弹 dialog 调发布 API |
open-history | — | 点"历史版本" |
open-diff | — | 点"查看改动" |
open-settings | — | 点组件设置,宿主弹元数据面板 |
open-preview-window | — | 点独立预览窗,宿主自己开新窗口 |
go-back | — | 点返回按钮(showBackButton 时) |
error | { message: string; stack?: string } | 预览编译 / 运行时错误 |
vue
<HyperCardEditor
v-model="source"
:features="{ showHistory: true, showDiff: true, enablePublish: true }"
:version-status="versionStatus"
@open-history="openHistory"
@open-diff="openDiff"
@push-version="pushVersion"
@error="reportError"
/>暴露的 ref
通过组件 ref 拿到:
| 成员 | 类型 | 说明 |
|---|---|---|
isDirty | Ref<boolean> | 用户修改未保存 |
confirmLeave() | () => boolean | 同步 confirm 弹窗,dirty 才提示;返回 true 表示放行 |
applyDraft(draft) | (draft: ComponentDraft) => void | 把外部 draft 灌进来(恢复历史版本) |
manualPreview() | () => void | 手动触发预览(autoPreview=false 时) |
applyPreview() | () => void | v-model 外部灌入后想立即刷预览时调 |
路由守卫示例:
ts
const editor = ref<InstanceType<typeof HyperCardEditor> | null>(null)
onBeforeRouteLeave(() => editor.value?.confirmLeave() ?? true)Monaco 类型注入
编辑器会给组件作者提供 __HYPERCARD__ / runtime / assets 的自动补全,这套类型 SDK 自带、不用宿主管。宿主只需要补充自己注入的 libs(如 http 客户端、UI 库)的具体类型。
setupHyperCardMonacoTypes
ts
function setupHyperCardMonacoTypes(): void注册 HyperCard 全局类型。SDK 内部首次创建 Monaco 编辑器时会自动调一次,多次调用自动去重。一般宿主不用手动调;只有自己单独起 Monaco 实例时才需要。
retainMonacoExtraLib / releaseMonacoExtraLib
ts
interface ExtraLib {
/** d.ts 内容字符串 */
content: string
/** 唯一文件路径,影响 Monaco 内部模块识别 */
filePath: string
}
function retainMonacoExtraLib(lib: ExtraLib): void
function releaseMonacoExtraLib(filePath: string): void带引用计数的 extra lib 注册/释放。多个 HyperCardEditor 实例可能传同一个 filePath,ref-count 保证 A 卸载不会把 B 还在用的 lib 删掉 —— 每次 retain 计数 +1,release 计数 -1,归零才真删。
推荐做法:直接用 :extra-libs prop 交给 SDK,SDK 在挂载时 retain、卸载时 release,无需手动管理:
vue
<HyperCardEditor
v-model="source"
:extra-libs="[
{
filePath: 'file:///hypercard-libs.http.d.ts',
content: `interface HyperCardLibs {
http: { get<T = unknown>(url: string): Promise<T> }
}`,
},
]"
/>HyperCardLibs 是开放命名空间(默认 { [key: string]: any }),宿主在自己的 d.ts 里声明合并补具体类型即可。
如果同一个 filePath 用不同 content 重复注册,SDK 会 warn 并保留先注册的版本;需要独立内容请用不同 filePath。
手动注入(自起 Monaco 时):
ts
import { setupHyperCardMonacoTypes, retainMonacoExtraLib, releaseMonacoExtraLib } from '@hy-bricks/editor'
setupHyperCardMonacoTypes()
retainMonacoExtraLib({ filePath: 'file:///my-libs.d.ts', content: '...' })
// 清理时
releaseMonacoExtraLib('file:///my-libs.d.ts')addMonacoExtraLib / removeMonacoExtraLib(已废弃)
ts
/** @deprecated 用 retainMonacoExtraLib 代替 */
function addMonacoExtraLib(lib: ExtraLib): void
/** @deprecated 用 releaseMonacoExtraLib 代替 */
function removeMonacoExtraLib(filePath: string): void旧 API,内部已转发到 retain/release。新代码一律用 retainMonacoExtraLib / releaseMonacoExtraLib,避免多实例互删。
草稿
源码草稿存取(localStorage,按 componentId 隔离,key 前缀 hc:draft:)。纯数据进出,不依赖 Monaco 单例。HyperCardEditor 传了 draftKey 时会自动用这套;宿主想自己做草稿逻辑也可以直接 import。
ts
interface ComponentDraft {
html: string
javascript: string
css: string
savedAt: number
}
function loadDraft(componentId: string): ComponentDraft | null
function saveDraft(
componentId: string,
sources: { html: string; javascript: string; css: string },
): ComponentDraft | null
function clearDraft(componentId: string): void
function formatRelative(ts: number): string| API | 说明 |
|---|---|
loadDraft(id) | 读草稿;无 / 损坏返 null |
saveDraft(id, sources) | 写当前源码,返回写入的 ComponentDraft;失败返 null |
clearDraft(id) | 清掉草稿(如发布成功后) |
formatRelative(ts) | 时间戳 → 中文"多久前"(刚刚 / N 分钟前 / N 小时前 / N 天前) |
注意 ComponentDraft 用 javascript 字段(而非 Source 的 js),与 Monaco 语言 id 对齐。
实时预览总线
主窗口 ↔ 独立预览窗口之间的 BroadcastChannel 总线。HyperCardEditor 传了 draftKey 时内部自动用;宿主自己做独立预览窗才需要直接调。
ts
type PreviewMessage =
| { type: 'sources'; html: string; javascript: string; css: string }
| { type: 'hello'; windowId: string }
| { type: 'goodbye'; windowId: string }
| { type: 'request-sources' }
interface PreviewBus {
channel: BroadcastChannel
send(msg: PreviewMessage): void
close(): void
}
function createPreviewBus(componentId: string): PreviewBuschannel 名为 hc-preview-${componentId},按组件 id 隔离不同组件的预览。协议:
- 主 → 子:
sources(源码变化时节流发送) - 子 → 主:
hello(子窗挂载)/goodbye(子窗关闭)/request-sources(子窗挂载后主动拉一次完整源码)
ts
const bus = createPreviewBus(componentId)
bus.channel.addEventListener('message', (e) => {
const msg = e.data as PreviewMessage
if (msg.type === 'sources') render(msg.html, msg.javascript, msg.css)
})
bus.send({ type: 'request-sources' })
// 关闭时
bus.close()版本钩子显示判定
一组纯函数,把"入口按钮要不要显示""时间相对展示"抽出来。HyperCardEditor 内部用它们决定顶栏入口;宿主想在别处复用同款判定 / 同款相对时间格式时可直接调。
ts
function shouldShowHistoryEntry(features?: EditorFeatures, status?: VersionStatus): boolean
function shouldShowDiffEntry(features?: EditorFeatures, status?: VersionStatus): boolean
function shouldShowPublishEntry(features?: EditorFeatures): boolean
function formatLastPublished(iso?: string, now?: Date): string | null| API | 显示条件 |
|---|---|
shouldShowHistoryEntry | features.showHistory 且 status.hasHistory === true(严格三态,undefined/false 不显示) |
shouldShowDiffEntry | 在历史条件基础上,还要求 currentVersionLabel 存在(diff 必须有 baseline) |
shouldShowPublishEntry | features.enablePublish !== false(undefined 默认显示) |
formatLastPublished | ISO 8601 → 中文相对时间;无效 / 未提供返 null,调用方按 null 决定不渲染 |
shouldShow* 用严格三态是为了避免宿主版本信息未加载时按钮闪现。
高级:自己拼编辑器
如果宿主想绕过 HyperCardEditor 顶层壳、自己组合编辑布局,SDK 把内部子组件 / scope / 布局 helper 也导出了。多数宿主用不到,直接用 HyperCardEditor 即可。
子组件
| 导出 | 说明 |
|---|---|
EditorLayout | 按布局树递归渲染分屏 |
EditorGroup | 单个 tab 组(tab bar + Monaco) |
MonacoEditor | 单个 Monaco 编辑器 |
Splitter | 左右 / 上下可拖拽分隔器 |
TabDropPayload | EditorGroup 拖拽落点载荷类型 |
这些子组件必须挂在 HyperCardEditor / createEditorScope 提供的 scope 内,否则 useEditorScope 会 throw。
Editor scope
每个 HyperCardEditor 实例自己一份 scope,多实例互不污染 Monaco model。
ts
function createEditorScope(): EditorScope
function useEditorScope(): EditorScope
const EDITOR_SCOPE_KEY: InjectionKey<EditorScope>
type SourceKey = 'html' | 'javascript' | 'css'
interface ModelMap {
html: monaco.editor.ITextModel
javascript: monaco.editor.ITextModel
css: monaco.editor.ITextModel
}
const SOURCE_KEY_LABEL: Record<SourceKey, string> // { html: 'index.html', javascript: 'component.js', css: 'styles.css' }EditorScope 提供 useModels() / getSource(key) / setSource(key, value) / snapshotSources() / onSourceChange(handler) / dispose()。createEditorScope() 创建后要 provide(EDITOR_SCOPE_KEY, scope),子组件 useEditorScope() inject 取用;不在 scope 内调用会 throw(明确开发期错误,不静默回退)。
布局 helper
不可变操作:每次返回新的布局树,Vue ref 替换触发 reactivity。
ts
type DropEdge = 'center' | 'left' | 'right' | 'top' | 'bottom'
interface GroupNode { type: 'group'; id: string; tabs: SourceKey[]; activeTab: SourceKey }
interface SplitNode { type: 'split'; id: string; direction: 'horizontal' | 'vertical'; ratio: number; a: LayoutNode; b: LayoutNode }
type LayoutNode = GroupNode | SplitNode
function defaultLayout(): LayoutNode
function activateTab(root: LayoutNode, groupId: string, tab: SourceKey): LayoutNode
function closeTab(root: LayoutNode, groupId: string, tab: SourceKey): LayoutNode
function splitWithTab(root: LayoutNode, sourceGroupId: string, tab: SourceKey, targetGroupId: string, edge: 'left' | 'right' | 'top' | 'bottom', copy?: boolean): LayoutNode
function moveTab(root: LayoutNode, sourceGroupId: string, tab: SourceKey, targetGroupId: string, insertIndex: number | null, copy?: boolean): LayoutNode
function setSplitRatio(root: LayoutNode, splitId: string, ratio: number): LayoutNodedefaultLayout() 返回单个 group(tabs html / javascript / css,默认激活 javascript)。setSplitRatio 的 ratio 会 clamp 到 [0.05, 0.95]。
Button
SDK 内置 vendor 的 shadcn-vue 风按钮,宿主装 SDK 后无需 npx shadcn-vue add,可直接 import 复用。
ts
import { Button, buttonVariants, type ButtonVariants } from '@hy-bricks/editor'variant:default / destructive / outline / secondary / ghost / link。 size:default / xs / sm / lg / icon / icon-sm。 buttonVariants 是 cva 工厂,可在别处生成同款 class;ButtonVariants 是其 variant props 类型。
边界
编辑器不做:拉历史版本、判断发布权限、知道后端表结构、自动保存到组件库。这些宿主拿到事件后自己处理。
导出速查
| 名称 | 类别 | 说明 |
|---|---|---|
HyperCardEditor | 组件 | 主入口受控组件 |
Source / VersionStatus / EditorFeatures | 类型 | 主组件 props 类型 |
setupHyperCardMonacoTypes | 函数 | 注册 HyperCard 全局 Monaco 类型 |
retainMonacoExtraLib / releaseMonacoExtraLib | 函数 | ref-count 安全的 extra lib 增持 / 释放 |
addMonacoExtraLib / removeMonacoExtraLib | 函数 | 已废弃,转发到 retain/release |
ExtraLib | 类型 | extra lib 形状 |
loadDraft / saveDraft / clearDraft | 函数 | 草稿存取 |
formatRelative | 函数 | 时间戳 → 相对时间 |
ComponentDraft | 类型 | 草稿数据形状 |
createPreviewBus | 函数 | 创建预览 BroadcastChannel 总线 |
PreviewMessage | 类型 | 预览总线消息 union |
shouldShowHistoryEntry / shouldShowDiffEntry / shouldShowPublishEntry | 函数 | 入口显示判定纯函数 |
formatLastPublished | 函数 | ISO 8601 → 相对时间(可空) |
EditorLayout / EditorGroup / MonacoEditor / Splitter | 组件 | 自拼编辑器用的子组件 |
TabDropPayload | 类型 | tab 拖拽落点载荷 |
createEditorScope / useEditorScope / EDITOR_SCOPE_KEY | scope | editor scope 工厂 / inject / key |
ModelMap / SourceKey / SOURCE_KEY_LABEL | scope | model 相关类型 / 常量 |
defaultLayout / activateTab / closeTab / splitWithTab / moveTab / setSplitRatio | 函数 | 布局树操作 helper |
LayoutNode / GroupNode / SplitNode / DropEdge | 类型 | 布局树类型 |
Button / buttonVariants / ButtonVariants | UI | 内置 shadcn-vue 风按钮 |