@hy-bricks/core
core 是运行时底座。
createHyperCard
createHyperCard 返回一个 Vue plugin,app.use() 之后把运行时实例挂到三处可达位置:provide('__HYPERCARD__')、this.$hc、window.__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> 访问 |
strict | false | 启动检测命中保留名 / 多 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.ts 里 declare 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
}| 字段 | 说明 |
|---|---|
libs | config.libs 注入的内容(元组已展开为 plugin 本身) |
runtime | 跨实例调用 / 订阅 / emit 的运行时 API。单画布可用,多画布撞名会 warn |
assets | 资源短链工具 |
version | SDK 实例版本号 |
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 列表(快照) |
registryVersion | Ref<number>,注册 / 注销 / 销毁画布时 +1。UI 可 watch 它来触发重读 listInstances() 等派生视图 |
DEFAULT_CANVAS_ID | 兜底画布字面量 '__default__'。不传 canvasId 的旧用法 / 编辑器预览落到此 scope。正式多画布业务请显式传 canvasId |
HC_CANVAS_ID_KEY | provide / 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 | 类型 | 说明 |
|---|---|---|
label | string? | 错误日志定位标签,显示在降级卡标题 |
instanceId | string? | 错误流定位维度(可选) |
canvasId | string? | 错误流定位维度(可选) |
componentId | string? | 错误流定位维度(可选) |
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.$el 是 null。
- 不访问 DOM 的回调(push 数据 /
setDataInput)→ 透明升级 - 访问 DOM → 改
nextTick(() => vm.$el ...)
升级前 audit:grep -rn "instance:ready\|onInstanceLifecycle\|getInstanceReady" src/