组合多个组件并封装为新组件
先分清两种组合
Section titled “先分清两种组合”不能把几个已经运行的 FlamoInstance 对象序列化后直接变成新组件。Instance 包含 Runtime、DOM、ECS、事件和资源引用,只能存在于当前运行生命周期。
要封装成可保存、可导入和可发布的新组件,应在 定义层 组合它们对应的精确 Component Ref 或 Artifact:
Component Ref / Artifact → Composition IR → canonical Artifact + dependency lock + compiler receipt → verify / publish → 新的 Component Ref → Runtime 创建父子 Instance 图运行时的 mountChild()、swapChild()、reconcileChildren() 只用于临时修改当前 Instance 图,不会自动生成新的 Artifact 或 Component Release。
层级由 key 与 parentKey 构造
Section titled “层级由 key 与 parentKey 构造”下面的组合会生成这棵组件树:
SearchPanel├─ query Input├─ submit Button│ └─ icon SearchIcon└─ results ResultList规则如下:
key是组合内稳定且全局唯一的 child identity,用于复用、查询、路由和热更新。parentKey: null表示该 child 直接属于新组件边界。parentKey: 'submit'表示该 child 嵌套在submit之下。mount描述逻辑挂载位置,可以是root、某个node、slot或portal。props、slots、layout、statePolicy和serviceAllowlist属于每个 child 的创建合同。- 重复
key、未知parentKey或父子循环会在编译或 Runtime commit 前失败。
parentKey 表示组件所有权层级,mount 表示内容挂到父级的哪个位置;两者不能互相替代。
定义组合组件
Section titled “定义组合组件”inputRef、buttonRef、iconRef 和 resultListRef 应是从 Component Registry / lockfile 获得的完整 FlamoComponentRefV1,包含精确版本、Artifact hash 和 ABI,不能只写 alias。
import { compileResolvedFlamoComposition, defineFlamoComponent,} from '@flamo/tools';
const definition = defineFlamoComponent({ id: 'SearchPanel',
children: [ { key: 'query', ref: inputRef, parentKey: null, mount: { kind: 'root' }, props: { placeholder: '搜索内容' }, slots: {}, statePolicy: 'preserve', serviceAllowlist: [], }, { key: 'submit', ref: buttonRef, parentKey: null, mount: { kind: 'root' }, props: { label: '搜索' }, slots: {}, statePolicy: 'preserve', serviceAllowlist: [], }, { key: 'icon', ref: iconRef, parentKey: 'submit', mount: { kind: 'slot', slotId: 'icon', outletId: 'main' }, props: {}, slots: {}, statePolicy: 'preserve', serviceAllowlist: [], }, { key: 'results', ref: resultListRef, parentKey: null, mount: { kind: 'root' }, props: {}, slots: {}, statePolicy: 'preserve', serviceAllowlist: [], }, ],
surface: { props: [ { name: 'query', type: 'string', required: false, attribute: 'query' }, ], events: [ { name: 'submit', detailType: '{ query: string }' }, ], methods: [ { name: 'focus', signature: '(): void' }, ], slots: [], publicChildRefs: ['query', 'submit'], },
routes: [ { kind: 'prop', from: { childKey: null, name: 'query' }, to: { childKey: 'query', name: 'value' }, }, { kind: 'event', from: { childKey: 'submit', name: 'click' }, to: { childKey: null, name: 'submit' }, }, { kind: 'method', from: { childKey: null, name: 'focus' }, to: { childKey: 'query', name: 'focus' }, }, ],});
const compiled = await compileResolvedFlamoComposition(definition, resolver);compileResolvedFlamoComposition() 会先通过 resolver.resolveExact() 校验每个依赖,再生成:
compiled.ir:规范化 Composition IR;compiled.artifact:带新 boundary、exports.Default和 composition 的 canonical Artifact;compiled.dependencyLock:全部 child 的精确依赖;compiled.receipt:composition、Artifact 与 lock 的 hash 证据。
只有把这些产物通过验证与发布流程晋升为新的 Component Release,组合结果才获得新的稳定 Component Ref。单纯在 JavaScript 中 export 这个对象,不等于完成 Flamo 组件发布。
在 Runtime 中实例化组合组件
Section titled “在 Runtime 中实例化组合组件”组合 Artifact 中保存的是精确 child ref,因此 Runtime 必须配置能解析这些依赖的 componentResolver:
import { createFlamoRuntime, defineFlamoArtifact,} from '@flamo/runtime';
const runtime = createFlamoRuntime({ componentResolver: resolver });const artifact = defineFlamoArtifact(compiled.artifact);const panel = await runtime.create({ artifact });
// 通过父组件的 prop route 更新 query childpanel.set({ query: 'Flamo' });
panel.children; // query、submit、resultspanel.child('submit'); // 任意直接 childpanel.publicChild('query'); // 只允许 surface.publicChildRefs 中声明的 childRuntime 读取 Artifact 中的 composition 后,会按父级优先顺序准备 children,再以一次 composition transaction 提交整棵树。父级销毁时,Runtime-owned children 会按层级递归释放。
在 Node Script 中动态创建组件
Section titled “在 Node Script 中动态创建组件”Node Script 可以通过 ctx.components 只修改当前 Runtime 的 Instance 图。字符串只解析当前 owning Artifact 的一个 export 及其完整 definition closure;跨 Artifact 时必须传完整的 FlamoComponentRefV1,Runtime 会校验 Artifact revision、hash、Runtime ABI 与 component ABI:
import { defineNodeScript } from '@flamo/runtime';
export default defineNodeScript({ setup(_props, ctx) { let badge;
ctx.onActivate('add-badge', async () => { badge = await ctx.components.create('Badge', { parent: ctx.scene.root, props: { label: 'New' }, placement: { kind: 'append' }, key: 'dynamic-badge', });
const off = badge.on('dismiss', () => badge.destroy()); ctx.dispose(off); }); },});create() 返回生命周期受 Runtime 管理的 ComponentInstanceHandle<TEvents>:
id与root提供当前实例身份和 live 根节点;setProps()原子提交公共属性;on()订阅业务事件并返回幂等 disposer;inspect()返回公共状态与资源生命周期计数;destroy()幂等销毁当前 child 及其 parent-owned 后代。
placement 支持 append、prepend、before 和 after;后两者接收同一 physical parent 下的 RuntimeNode 或组件句柄作为 sibling,并可带局部 gap。一次 owner 最多保留 1000 个动态 child,一次 createBatch() 最多接收 100 个请求。
批量创建、取消与回滚
Section titled “批量创建、取消与回滚”批量创建共享一个可选 AbortSignal;单个 request 的 signal 优先。任何一个请求失败时,已经创建的实例会按逆序销毁,调用方不会看到半棵成功、半棵失败的组件树:
const controller = new AbortController();
const handles = await ctx.components.createBatch([ { component: 'Avatar', props: { userId: 'a' } }, { component: 'Avatar', props: { userId: 'b' } },], { signal: controller.signal });父实例销毁会递归释放这些 parent-owned children。脚本也可以显式调用 await ctx.self.destroy() 销毁自己的 owning instance;重复调用不会重复释放资源。
原子更新组件属性
Section titled “原子更新组件属性”宿主 Instance 与动态组件句柄使用同一套序列化事务:
const receipt = await panel.setProps({ disabled: false, variant: { Size: 'Large', State: 'Active' },});
if (receipt.status !== 'committed') { console.warn(receipt.error);}Runtime 先验证完整 patch,再完成 Variant 变更,最后才让普通绑定和公共更新事件可见。失败或取消会恢复之前的 Variant selection,不会发布部分 props。FlamoPropsCommitReceipt 可用于把这次提交与 interaction trace 对齐。
Component Set 根脚本继承到页面 INSTANCE 后,ctx.instance.setProps() 自动按当前实例的有效 surface 解析,不会把 state 等属性写到页面级 props:
const receipt = await ctx.instance.setProps({ state: 'hover' });if (receipt.status !== 'committed' || receipt.variantReceipts[0]?.status !== 'applied') { throw new Error(receipt.error?.message ?? 'Variant state was not applied');}未知、无绑定或跨作用域属性会得到 rejected 回执。Variant 轴成功提交时,variantReceipts 会包含当前具体 INSTANCE 的 applied 回执,可作为视觉重物化完成的门禁。
只做运行时组合
Section titled “只做运行时组合”如果不需要保存或发布,可以直接修改某个父 Instance 的运行树:
await panel.reconcileChildren([ { key: 'query', artifact: Input({ placeholder: '搜索内容' }), parentKey: null, mount: { kind: 'root' }, props: {}, }, { key: 'submit', artifact: Button({ label: '搜索' }), parentKey: null, mount: { kind: 'root' }, props: {}, },]);这种调用只改变当前 Runtime:
- 相同
key、相同 ref / Artifact identity 且statePolicy不是reset时可以复用已有 child; swapChild()用于替换一个 child 及其后代;- 传入现成
instance时默认是 caller-owned,父级销毁只解除挂接,不替调用方销毁它; - 如果最终要封装成新组件,应把相同结构写回
defineFlamoComponent()/ Composition IR,再编译和发布。