Skip to content

@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类型默认说明
modelValueSourcev-model 绑定的源码,必填
draftKeystring启用 localStorage 草稿(key = hc:draft:<draftKey>);留空 = 不启用草稿 + 独立预览窗
componentIdstringdraftKey 或内部 uid给运行时预览实例用的 id
featuresEditorFeatures入口按钮显隐 / 启用开关
versionStatusVersionStatus宿主传入的版本摘要,只读、只展示
extraLibsExtraLib[]额外 Monaco 类型注入(见下文)
autoPreviewbooleantrue是否自动重渲染预览
previewRationumber0.62编辑区与预览区初始宽度比
showBackButtonbooleanfalse顶栏显示返回按钮
titlestring顶栏标题
statusLabelstring顶栏右侧状态文本(遗留,优先用 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:modelValueSourcev-model 回写
dirtyboolean是否有未保存修改
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 拿到:

成员类型说明
isDirtyRef<boolean>用户修改未保存
confirmLeave()() => boolean同步 confirm 弹窗,dirty 才提示;返回 true 表示放行
applyDraft(draft)(draft: ComponentDraft) => void把外部 draft 灌进来(恢复历史版本)
manualPreview()() => void手动触发预览(autoPreview=false 时)
applyPreview()() => voidv-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 天前)

注意 ComponentDraftjavascript 字段(而非 Sourcejs),与 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): PreviewBus

channel 名为 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显示条件
shouldShowHistoryEntryfeatures.showHistorystatus.hasHistory === true(严格三态,undefined/false 不显示)
shouldShowDiffEntry在历史条件基础上,还要求 currentVersionLabel 存在(diff 必须有 baseline)
shouldShowPublishEntryfeatures.enablePublish !== false(undefined 默认显示)
formatLastPublishedISO 8601 → 中文相对时间;无效 / 未提供返 null,调用方按 null 决定不渲染

shouldShow* 用严格三态是为了避免宿主版本信息未加载时按钮闪现。

高级:自己拼编辑器

如果宿主想绕过 HyperCardEditor 顶层壳、自己组合编辑布局,SDK 把内部子组件 / scope / 布局 helper 也导出了。多数宿主用不到,直接用 HyperCardEditor 即可。

子组件

导出说明
EditorLayout按布局树递归渲染分屏
EditorGroup单个 tab 组(tab bar + Monaco)
MonacoEditor单个 Monaco 编辑器
Splitter左右 / 上下可拖拽分隔器
TabDropPayloadEditorGroup 拖拽落点载荷类型

这些子组件必须挂在 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): LayoutNode

defaultLayout() 返回单个 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 / linksize:default / xs / sm / lg / icon / icon-smbuttonVariantscva 工厂,可在别处生成同款 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_KEYscopeeditor scope 工厂 / inject / key
ModelMap / SourceKey / SOURCE_KEY_LABELscopemodel 相关类型 / 常量
defaultLayout / activateTab / closeTab / splitWithTab / moveTab / setSplitRatio函数布局树操作 helper
LayoutNode / GroupNode / SplitNode / DropEdge类型布局树类型
Button / buttonVariants / ButtonVariantsUI内置 shadcn-vue 风按钮

进一步阅读