Skip to content

@hy-bricks/core

core 是运行时底座。

createHyperCard

createHyperCard 返回一个 Vue plugin,app.use() 之后把运行时实例挂到三处可达位置:provide('__HYPERCARD__')this.$hcwindow.__HYPERCARD__。组件源码渲染期统一通过 __HYPERCARD__.* 访问注入的库和运行时 API。

ts
import { createApp } from 'vue'
import { createHyperCard } from '@hy-bricks/core'
import ElementPlus from 'element-plus'
import axios from 'axios'
import * as echarts from 'echarts'

const app = createApp(App)

app.use(createHyperCard({
  libs: {
    ui: ElementPlus,                 // Vue plugin → SDK 自动 app.use + 挂 libs.ui
    http: axios,                     // 普通对象 → 只挂 libs.http
    echarts,                         // 第三方库 → 挂 libs.echarts
    formatPrice: (n) => `¥${n}`,     // 函数也行
    BRAND: 'HyperCard',              // 常量也行
  },
  strict: false,
  version: 'v1',
}))

HyperCardConfig

ts
interface HyperCardConfig {
  /** 注入到 __HYPERCARD__.libs 的库 / 函数 / 常量,key 任意命名 */
  libs?: LibsConfig
  /**
   * strict=true 时,启动检测发现"保留词命名 / 多 Vue plugin 共存"等问题会抛 Error;
   * strict=false(默认)仅 console.warn,不阻止启动
   */
  strict?: boolean
  /** SDK 实例版本号(组件作者可读,用于兼容判断)*/
  version?: string
}
字段默认说明
libs{}注入给组件源码使用的库、函数、常量。key 自取,组件源码用 __HYPERCARD__.libs.<name> 访问
strictfalse启动检测命中保留名 / 多 plugin 共存等问题时,true 直接抛错,false 仅 warn
version'v1'SDK 实例版本号,组件源码可读

libs 槽位允许的形态

ts
/** 配置时单个 lib 槽位允许的类型 */
type LibConfigValue<T> = T | readonly [T, ...unknown[]]

type LibsConfig = {
  [K in keyof LibsRegistry]?: LibConfigValue<LibsRegistry[K]>
} & Record<string, unknown>

每个槽位可以是:

  • 直接的库 / 对象 / 函数 / 常量 —— 原样挂到 libs.<name>
  • Vue plugin(带 install 方法的对象)—— SDK 自动 app.use(plugin),同时把 plugin 本身挂到 libs.<name>
  • 元组 [plugin, ...options] —— SDK 自动 app.use(plugin, ...options),然后 libs.<name> 仍指向 plugin 本身(不是元组)。
ts
app.use(createHyperCard({
  libs: {
    // 需要传 options 的 Vue plugin,用元组
    ui: [ElementPlus, { size: 'small', zIndex: 3000 }],
  },
}))
// 组件源码里:__HYPERCARD__.libs.ui 拿到的是 ElementPlus 本身

LibsRegistry 与类型补全

LibsRegistry 是 SDK 留给宿主做类型增强的空接口,SDK 自身不绑定任何具体库类型(包体积零负担)。宿主在自己的 *.d.tsdeclare module 增强它,组件源码写 __HYPERCARD__.libs.<name>. 就有 IDE 补全:

ts
// 宿主项目 src/types.d.ts
import 'element-plus'
import type { AxiosStatic } from 'axios'

declare module '@hy-bricks/core' {
  interface LibsRegistry {
    ui: typeof import('element-plus')
    http: AxiosStatic
    formatPrice: (n: number) => string
  }
}

HyperCardInstance

app.use(createHyperCard(...)) 后,运行时实例可通过三路拿到:

ts
import { inject } from 'vue'
import type { HyperCardInstance } from '@hy-bricks/core'

// 1. setup 内 inject
const hc = inject<HyperCardInstance>('__HYPERCARD__')!

// 2. Options API 内 this.$hc
this.$hc.libs.http.get('/api/x')

// 3. 组件源码字符串里直接用全局
__HYPERCARD__.libs.http.get('/api/x')
ts
interface HyperCardInstance {
  /** 注入的库 / 函数 / 常量集合 */
  libs: LibsRegistry & Record<string, unknown>
  /** 运行时跨实例 API(单画布场景);多画布请用 canvases */
  runtime: RuntimeAPI
  /** 资源短链工具 */
  assets: typeof assets
  /** 实例版本号(= config.version) */
  version: string
  /** 多画布 registry(见下) */
  canvases: CanvasesRegistryAPI
}
字段说明
libsconfig.libs 注入的内容(元组已展开为 plugin 本身)
runtime跨实例调用 / 订阅 / emit 的运行时 API。单画布可用,多画布撞名会 warn
assets资源短链工具
versionSDK 实例版本号
canvases多画布 registry,跨画布调用 / 广播 / 查实例

RuntimeAPI

__HYPERCARD__.runtime 是单画布场景的跨实例运行时 API。多画布业务请改用 canvases,它要求显式 canvasId,语义更清晰。

ts
interface RuntimeAPI {
  getInstance(id: string): ComponentInstanceHandle | null
  listInstances(filter?: { componentId?: string }): ComponentInstanceHandle[]
  call<T = unknown>(id: string, method: string, ...args: unknown[]): T | undefined
  on(id: string, event: string, handler: (payload: unknown) => void): (() => void) | undefined
  emit(id: string, event: string, payload?: unknown): void
}
js
// 组件源码里调另一个实例的方法
__HYPERCARD__.runtime.call('table1', 'refresh')

// 订阅另一个实例 emit 的事件
const off = __HYPERCARD__.runtime.on('filter1', 'change', (payload) => { /* ... */ })

跨画布存在同名 instanceId 时,runtime 的查询会 console.warn 并返回第一个匹配项——此时应改用 canvases.callComponent(canvasId, ...) 显式指定画布。

isVuePlugin

createHyperCard 用它判断某个 lib 槽位是不是 Vue plugin,从而决定要不要自动 app.use。宿主一般无需直接调用,了解判定规则即可:

ts
function isVuePlugin(v: unknown): boolean
  • 只认 { install: fn } 对象形式,以及 [{ install }, ...options] 元组形式。
  • 不认裸函数。因为 axios / lodash / dayjs 等也是函数,若被当 plugin 调用会引发破坏性错误。极少数纯函数签名的老 plugin,请显式包成 { install: fn } 再传。

HC_INJECT_KEY

provide / inject 用的字符串 key,等于 '__HYPERCARD__'createHyperCard 安装时 app.provide(HC_INJECT_KEY, instance),组件用同 key inject:

ts
import { inject } from 'vue'
import { HC_INJECT_KEY, type HyperCardInstance } from '@hy-bricks/core'

const hc = inject<HyperCardInstance>(HC_INJECT_KEY)!

它是字符串字面量而非 Symbol,方便组件源码字符串和跨包复用时无需 import。

运行时对象

ts
window.__HYPERCARD__

即上面的 HyperCardInstance,包含 libs / runtime / assets / canvases / version

多画布 registry

__HYPERCARD__.canvases(CanvasesRegistryAPI)用于宿主以 v-for 渲染多个 <HyperCardPageRenderer> 时,拿到所有活跃画布并跨画布调用 / 广播 / 查实例。

ts
const canvases = window.__HYPERCARD__?.canvases

canvases?.list()                                       // 所有活跃画布 handle
canvases?.get('card-a')                                // 单个画布 handle,不存在返 null
canvases?.callComponent('card-a', 'table1', 'refresh') // 显式跨画布调方法
canvases?.emitToCanvas('card-a', 'filter1', 'change')  // 给某画布某实例 emit
canvases?.broadcast('reload')                          // 全局广播,fire-and-forget
API说明
list()所有活跃画布 handle 快照
get(canvasId)单画布 handle;不存在返 null
getAll()全表快照(普通对象,不暴露内部 Map)
callComponent(canvasId, instanceId, method, ...args)显式跨画布调用实例方法;找不到 console.warn + 返 undefined,不抛
emitToCanvas(canvasId, instanceId, event, payload?)给某画布某实例 emit;找不到 console.warn 后 return
broadcast(event, payload?)跨所有画布广播;两层 try/catch,单实例炸不影响其它实例 / 画布

单画布 handle(CanvasRuntimeHandle)还提供 listInstances / getInstance / call / emit / broadcastInCanvas / dispose

多画布业务必须优先使用显式 canvasId。完整示例见 多画布 recipe

进阶:scoped registry 底层 API

大多数宿主用 <HyperCardPageRenderer> 渲染页面,实例的注册 / 销毁由 SDK 自动完成,不需要碰下面这些 API。它们只在你自己组装渲染管线(不走 <HyperCardPageRenderer>)时才用到。

ts
import {
  registerInCanvas,
  unregister,
  disposeCanvas,
  listCanvasIds,
  registryVersion,
  DEFAULT_CANVAS_ID,
  HC_CANVAS_ID_KEY,
} from '@hy-bricks/core'
API说明
registerInCanvas(canvasId, instanceId, componentId, vm)在指定画布内注册组件实例,返回 ComponentInstanceHandle。同画布内 instanceId 重复会覆盖 + warn;跨画布同名不冲突
unregister(instanceId, canvasId?)注销实例;不传 canvasId 默认 __default__;找不到 silent
disposeCanvas(canvasId)销毁整个画布的注册表(dispose 所有 handle + 删内部 Map);多次调 idempotent。<HyperCardPageRenderer> 卸载时自动调
listCanvasIds()当前所有活跃画布 id 列表(快照)
registryVersionRef<number>,注册 / 注销 / 销毁画布时 +1。UI 可 watch 它来触发重读 listInstances() 等派生视图
DEFAULT_CANVAS_ID兜底画布字面量 '__default__'。不传 canvasId 的旧用法 / 编辑器预览落到此 scope。正式多画布业务请显式传 canvasId
HC_CANVAS_ID_KEYprovide / inject key(字符串 '__hcCanvasId')。容器组件 provide(HC_CANVAS_ID_KEY, canvasId) 向子树传播画布上下文,子组件 inject(HC_CANVAS_ID_KEY) 读取

registerInCanvas 返回的 ComponentInstanceHandle:

ts
interface ComponentInstanceHandle {
  readonly instanceId: string
  readonly componentId: string
  readonly vm: ComponentPublicInstance

  call<T = unknown>(method: string, ...args: unknown[]): T
  on(event: string, handler: (payload: unknown) => void): () => void
  emit(event: string, payload?: unknown): void
  setProp(key: string, value: unknown): void
  setDataInput(key: string, value: unknown): void
}

setProp(key, value)vm[key](主动改 prop);setDataInput(key, value)vm.custom[key].value,要求组件源码已用 custom.<key>.dataInput = true 显式声明该字段接收灌入,否则会 warn 且 no-op。

自组装路径下的画布上下文传播

不走 <HyperCardPageRenderer> 时,自定义容器需要自己用 HC_CANVAS_ID_KEY 向子树 provide 画布 id,并在卸载时 disposeCanvas:

ts
import { provide, onBeforeUnmount } from 'vue'
import { HC_CANVAS_ID_KEY, disposeCanvas } from '@hy-bricks/core'

const canvasId = 'card-a'
provide(HC_CANVAS_ID_KEY, canvasId)

onBeforeUnmount(() => disposeCanvas(canvasId))

ErrorBoundary

ts
import { ErrorBoundary } from '@hy-bricks/core'

Vue 3 错误边界组件,运行时错误兜底:捕获 slot 内组件 mount / update / render 阶段抛出的异常,渲染降级卡(错误信息 + 可展开堆栈 + 重试按钮),不让单个组件出错拖垮整页。SDK 渲染管线给每个组件实例都包了一层 ErrorBoundary,宿主自组装渲染时可手动套用。

Prop类型说明
labelstring?错误日志定位标签,显示在降级卡标题
instanceIdstring?错误流定位维度(可选)
canvasIdstring?错误流定位维度(可选)
componentIdstring?错误流定位维度(可选)
vue
<ErrorBoundary label="价格卡片">
  <UserComponent />
</ErrorBoundary>

注意:它拦截 setTimeout / Promise.reject 等异步回调内的错误(Vue onErrorCaptured 的固有边界),这类需要宿主用全局 window.onerror / unhandledrejection 兜底。捕获到错误后默认吞掉(不再向上传播),保证整页继续渲染。

组件保留 prop(0.4.0)

SDK 给每个组件实例注入 4 个保留 prop,组件作者可读自己身份(别用这些名字声明自己的 prop):

Prop含义
this.hcCustomValues属性面板 override 值
this.hcInstanceId实例 ID(★ 0.4.0)
this.hcCanvasId所属画布 ID(★ 0.4.0)
this.hcComponentId组件源 ID(★ 0.4.0)
hcPersistInstanceState快照持久化开关(★ 0.6.3,渲染器从 persistInstanceState 一路透下来)。组件作者别声明同名 prop,也别在 host 直接设——由渲染器注入
js
export default {
  mounted() {
    // ✅ 0.4.0 起 register 已在 created 完成,mounted 里立即可订阅
    __HYPERCARD__.runtime.on(this.hcInstanceId, 'refresh', () => { /* ... */ })
  },
}

hcCustomValues watcher diff(0.6.3)

hcCustomValues 的内部 watch 在 0.6.3 起改为 diff 同步:只在某 key 的 override 值【相对上次 prop】真变化时(Object.is 比较)才同步进组件 custom 值。无 gate、所有宿主常开。

  • 修的问题:渲染器每次重渲染都传"内容相同、引用不同的新对象",老逻辑无条件写回文档默认,会冲掉用户运行时改的 custom 值(如下拉选值)。
  • host 经属性面板真改值(old ≠ new)仍正常同步;初始值走 data() merge(该 watch 非 immediate)。

⚠️ 0.4.0 BREAKING — instance:ready 时机前移

0.4.0 把实例 register 从 mounted 后挪到组件 created 钩子(mounted 前)。onInstanceLifecycle('instance:ready') / handle.on('instance:ready') 回调被调时子组件 DOM 还没渲染,vm.$elnull

  • 不访问 DOM 的回调(push 数据 / setDataInput)→ 透明升级
  • 访问 DOM → 改 nextTick(() => vm.$el ...)

升级前 audit:grep -rn "instance:ready\|onInstanceLifecycle\|getInstanceReady" src/