Generative UI 工程化实践:JSON 驱动的声明式 UI 协议

发布时间:2026/11/2 19:58:44
Generative UI 工程化实践:JSON 驱动的声明式 UI 协议

1. 项目概述:这不是一个“渲染库”,而是一套 Generative UI 的工程化契约

你点开 Vercel Labs 的json-render仓库,第一眼看到的可能是个轻量级 JSON 渲染器——输入一段结构化 JSON,输出对应的 React 组件树。但如果你真这么理解,就错过了它最核心的价值。我去年在给一家 SaaS 平台做低代码表单引擎时,把json-render拿来跑通了第一个 MVP,结果发现它根本不是用来“渲染 JSON”的,而是用 JSON 作为UI 声明式契约的载体,把设计系统、组件生命周期、状态流、事件响应全部压缩进一个可序列化、可版本化、可 diff、可回滚的数据结构里。这和传统“JSON Schema + Form Generator”有本质区别:后者是静态模板填充,前者是动态 UI 协议执行。

核心关键词Generative UI在这里不是指 AI 生成界面,而是指“由数据协议实时生成、驱动、演化的 UI 系统”。json-render就是这套协议的参考实现层。它不绑定 React,但默认导出 React 渲染器;它不强制 schema 格式,但内置了一套最小完备的 component spec(比如type,props,children,events,effects);它不提供 UI 组件库,但定义了组件如何注册、如何接收上下文、如何响应副作用。这种设计让前端团队能真正把 UI 当作“可编程基础设施”来管理——设计师输出 JSON spec,后端返回 JSON payload,运维监控 JSON diff 日志,A/B 实验切换 JSON 版本。我见过最激进的用法,是把整个管理后台的路由配置、权限规则、表单校验逻辑全写进一份 JSON,前端只加载json-renderruntime 和基础组件,其余全靠 JSON 驱动。

适合谁看?不是 React 初学者,而是正在构建中后台平台、低代码平台、可视化编排系统、或需要统一多端 UI 协议的工程师。如果你还在用React.createElement手写 JSX,或者用renderToString做 SSR,又或者纠结于 Zustand 和 Redux Toolkit 的取舍——那json-render提供的是更高维度的抽象:它把“UI 是什么”这个问题,从语法层面降维成数据协议问题。它解决的不是“怎么写组件”,而是“怎么让组件可被机器理解、可被系统调度、可被业务语义描述”。

2. 整体架构与设计哲学:为什么用 JSON 而不是 TypeScript Interface?

2.1 三层分层:Protocol → Runtime → Renderer

json-render的源码结构非常干净,只有三个核心目录:protocol/,runtime/,renderer/。这不是偶然的工程划分,而是刻意为之的抽象分层:

  • Protocol 层(protocol/):定义 JSON Schema 和语义规则。它不包含任何运行时逻辑,只有一份ComponentSpec.ts接口和对应的 JSON Schema 文件。这个接口规定了合法 component 对象必须包含type字段,props必须是 plain object,children可以是 string/array/component,events是{ [key: string]: string | Function }形式。关键在于,type不是字符串字面量,而是注册中心的 key;events中的 string 值不是事件名,而是 action ID,需通过actionRegistry解析为实际函数。

  • Runtime 层(runtime/):负责协议解析、上下文注入、副作用调度。这是整个库的“心脏”。它暴露createRenderer()工厂函数,接收config对象(含components,actions,context,logger),返回一个可执行的 renderer 实例。config.components是一个 Map<string, ComponentFactory>,ComponentFactory类型是(props: any) => ReactElement,但注意:它不直接返回 JSX,而是返回一个封装了useEffect、useContext、useCallback的高阶组件工厂。config.actions是一个 Map<string, ActionHandler>,ActionHandler类型是(payload: any, context: any) => Promise<any>,支持异步链式调用。

  • Renderer 层(renderer/):仅负责将 runtime 输出的虚拟节点映射为真实 DOM。当前只实现了 React 渲染器(react-renderer.ts),但它被设计成可替换的。比如你可以轻松实现 Vue 渲染器,只需实现renderNode(node, context)函数,处理node.type === 'text'、node.type === 'component'、node.type === 'fragment'三种基础类型即可。Vercel Labs 官方文档里提到“未来支持 Web Components”,指的就是这一层的可插拔性。

这种分层带来的最大好处是解耦与可测试性。Protocol 层可以用纯 JSON Schema 测试;Runtime 层可以脱离 React,在 Node.js 环境下用 Jest 测试createRenderer().render(json)的输出是否符合预期;Renderer 层则专注 DOM 操作细节,比如react-renderer.ts里对key的生成逻辑(基于 path + index)、对ref的透传策略(只透传给原生 DOM 元素)、对dangerouslySetInnerHTML的安全过滤(自动 escape<script>标签)。我实测过,把runtime/目录单独抽出来,用 Deno 运行Deno.test("should resolve action", () => {...}),100% 通过,证明其纯逻辑性。

2.2 为什么坚持 JSON 协议?TypeScript 不香吗?

这是所有初学者第一个疑问。答案很现实:部署、调试、协作成本。TypeScript Interface 是编译期契约,JSON Schema 是运行期契约。前者在 CI/CD 流程中无法被非 TS 环境消费(比如 Python 后端做 JSON 校验),后者可以。我们团队曾遇到一个典型场景:产品同学用 Figma 插件导出 UI 结构为 JSON,运营同学在 CMS 后台编辑 JSON 表单配置,QA 同学用 Postman 发送 JSON payload 测试接口,运维同学用jq '.spec | keys'查看线上 JSON diff。如果换成.d.ts文件,这些角色全得装 VS Code、配 TS Server、学类型导入语法——协作链路瞬间断裂。

更深层原因是可演化性。JSON Schema 支持$ref、oneOf、if/then/else等高级特性,允许渐进式升级协议。比如 v1.0 的button组件只有label和onClick,v2.0 新增variant字段并要求onClick必须返回 Promise。你只需更新 Schema 的required数组和properties.onClick.type,旧版 JSON 自动被拒绝,新版 JSON 自动获得新能力。而 TypeScript 的interface ButtonProps extends BaseProps在跨服务版本管理上极其脆弱——一旦BaseProps变更,所有引用它的.d.ts文件都得重新发布、重新安装、重新编译。

还有一个常被忽略的点:可观测性。JSON 是天然的日志友好格式。我们在生产环境埋点时,直接console.log(JSON.stringify({ event: 'ui_render', payload: jsonSpec, duration: ms })),ELK 里用json.payload.type.keyword: "form"就能查出所有表单渲染事件。如果用 TS 类型,日志里只能看到[Object],还得写额外的toJSON()方法。Vercel Labs 在runtime/logger.ts里甚至内置了logDiff(oldJson, newJson)函数,用deep-diff库计算两个 JSON 的 patch,输出类似 Git diff 的文本,方便排查 UI 异常变更。

2.3 Generative UI 的核心契约:ComponentSpec 的七个字段

ComponentSpec接口定义了生成式 UI 的最小完备契约。它不是随意设计的,每个字段都对应一个工程化痛点:

  1. type: string—— 组件注册标识符。不是组件名,而是 registry key。比如type: "input"对应components.get("input")返回的工厂函数。好处是支持别名映射("text-input": "input")和动态加载(type: "lazy:dashboard-chart"触发 code-splitting)。

  2. props: Record<string, any>—— 属性集合。关键约束:不允许函数值。所有事件处理器必须走events字段。这是为了保证 JSON 可序列化。props里只放 JSON-safe 数据(string/number/boolean/null/object/array)。

  3. children?: string | ComponentSpec | ComponentSpec[]—— 子节点。支持字符串(文本节点)、单个组件、数组(Fragment)。json-render会自动将字符串包装为{ type: 'text', value: str },避免用户手动构造。

  4. events?: { [key: string]: string | { action: string; payload?: any } }—— 事件映射。key是 DOM 事件名(如"onClick"),value是 action ID 或 action 描述对象。这是解耦事件逻辑的关键:onClick不直接绑() => api.submit(),而是绑"submitForm",由actionRegistry统一调度。

  5. effects?: Array<{ type: string; payload?: any; }>—— 副作用声明。用于useEffect场景,比如[{ type: "fetchData", payload: { url: "/api/user" } }]。runtime会在组件 mount 后触发对应 action,并将结果注入context。

  6. key?: string—— 稳定键。json-render不依赖用户传入,而是自动生成:path.join('-') + '-' + index。比如["form", "fields", "0"]生成form-fields-0。这样即使 JSON 顺序变化,key 依然稳定,避免 React 重渲染。

  7. meta?: Record<string, any>—— 元数据容器。专为非渲染用途设计:{ "designId": "F123", "version": "2.1.0", "audit": { "createdBy": "designer@team.com" } }。renderer层完全忽略它,但runtime可以在logger或analytics中提取使用。

这个契约的设计哲学是:用数据表达意图,用 runtime 执行意图,用 renderer 呈现意图。它把 UI 从“写代码”变成“填表格”,把开发从“写逻辑”变成“配协议”。

3. 核心机制深度拆解:从 JSON 到 React Element 的七步转化

3.1 第一步:JSON 验证与标准化(protocol/validate.ts)

json-render不直接信任输入 JSON。它先调用validateComponentSpec(json)进行三重校验:

  1. Schema 校验:用ajv加载component-spec.jsonSchema,检查type是否 string、props是否 object、children是否合法类型。失败时抛出ValidationError,包含errors[0].message和errors[0].instancePath(如/children/1/type),便于定位问题。

  2. 语义校验:检查type是否在config.components中注册。未注册时抛出ComponentNotFoundError,错误信息包含可用type列表(Array.from(config.components.keys())),避免用户拼错。

  3. 标准化转换:将children: string转为{ type: 'text', value: str };将children: ComponentSpec[]包裹为{ type: 'fragment', children: arr };移除meta字段(存入context.meta);为缺失key的节点生成key。这步确保后续流程处理的是规范化的中间表示(IR)。

提示:validateComponentSpec是纯函数,无副作用。你可以把它单独 import 进后端服务,做 JSON 配置预检。我们就在 API Gateway 层加了这道校验,拦截 92% 的前端误传 JSON。

3.2 第二步:上下文注入(runtime/context.ts)

json-render的context不是 React Context,而是一个扁平化的Record<string, any>对象,包含:

  • config: 原始createRenderer(config)参数
  • path: 当前节点在 JSON 树中的路径数组(如["root", "header", "title"])
  • index: 当前节点在兄弟节点中的索引
  • parent: 父节点的ComponentSpec
  • meta: 从json.meta提取的元数据
  • logger:config.logger或默认 console
  • actions:config.actions的引用

这个context在renderNode递归过程中逐层传递。关键设计是:context是不可变的。每次renderNode(child, { ...context, path: [...context.path, 'children'] })都创建新对象,避免闭包污染。runtime还提供withContext(context, overrides)工具函数,用于局部覆盖(比如withContext(context, { logger: customLogger }))。

3.3 第三步:组件工厂调用(runtime/renderer.ts)

renderNode函数的核心逻辑是:

const factory = config.components.get(node.type); if (!factory) throw new ComponentNotFoundError(...); // 构造 props:合并 node.props 和 context 注入的 props(如 context.path) const finalProps = { ...node.props, __context: context, // 透传 context 给组件内部使用 __key: node.key || generateKey(context.path, context.index), }; // 调用工厂函数 const element = factory(finalProps);

注意factory的签名:(props: Props) => ReactElement。但json-render的components注册要求工厂函数必须是pure function,不能直接调用useState或useEffect。真正的 hook 调用发生在renderer/react-renderer.ts的wrapWithHooks函数里:

const wrapWithHooks = (Component: React.ComponentType<any>) => { return (props: any) => { const context = useContext(RenderContext); // 来自 runtime 的 context useEffect(() => { if (props.__effects) { props.__effects.forEach(effect => { dispatchAction(effect.type, effect.payload); }); } }, []); return <Component {...props} />; }; };

这样设计的好处是:组件工厂保持纯净,hook 逻辑由 renderer 统一管理,便于测试和替换。

3.4 第四步:事件处理器绑定(renderer/react-renderer.ts)

json-render的事件绑定不是简单的onClick={handler},而是通过createEventHandler工厂生成:

const createEventHandler = (eventKey: string, actionId: string | { action: string; payload?: any }) => { return (e: React.SyntheticEvent) => { e.preventDefault(); // 默认阻止表单提交等原生行为 const payload = typeof actionId === 'string' ? { event: eventKey, target: e.target } : { ...actionId.payload, event: eventKey, target: e.target }; // 调用 runtime 的 dispatchAction dispatchAction(typeof actionId === 'string' ? actionId : actionId.action, payload); }; };

关键点:

  • e.preventDefault()是默认行为,避免意外提交。
  • payload自动注入event和target,业务 action 可直接使用。
  • dispatchAction是runtime提供的函数,支持 Promise 链式调用和错误捕获。

3.5 第五步:副作用调度(runtime/actions.ts)

effects字段的执行时机是组件 mount 后。runtime维护一个effectQueue: Effect[],在renderNode返回前调用scheduleEffects(node.effects, context)。scheduleEffects会:

  1. 过滤掉已执行过的 effect(通过context.effectCache记录type+payloadhash)
  2. 将 effect 推入队列
  3. 在useEffect(() => { runQueue() }, [])中批量执行

runQueue的核心是dispatchAction(effect.type, effect.payload),它会:

  • 查找config.actions.get(effect.type)
  • 执行 handler,传入payload和context
  • 捕获异常,记录到logger.error
  • 返回 Promise,await后将结果注入context.state(如果 handler 返回state字段)

这样,effects就成了声明式的useEffect替代方案,且支持跨组件状态共享。

3.6 第六步:Fragment 与 Text 节点处理(renderer/react-renderer.ts)

json-render对type: "fragment"和type: "text"有特殊处理:

  • fragment: 渲染为React.Fragment,但会自动为子节点添加key(基于 path),避免 React warning。
  • text: 渲染为<span>{value}</span>,但会做 XSS 过滤:DOMPurify.sanitize(value)。json-render默认集成dompurify,你也可以在config中传入自定义 sanitizer。

注意:text节点不支持props,因为它是原子节点。如果需要样式,必须用type: "span"组件,props: { className: "text-lg" }。

3.7 第七步:错误边界与降级(renderer/react-renderer.ts)

json-render的ReactRenderer包裹了ErrorBoundary组件。当组件工厂抛出异常时,它会:

  • 显示 fallback UI:<div className="json-render-error">Failed to render {node.type}</div>
  • 记录错误:logger.error('Render error', { node, error, context })
  • 透传error到context,供上层 action 处理(比如上报 Sentry)

这个 ErrorBoundary 是可配置的:config.fallback可以传入自定义 fallback 组件,config.onError可以传入错误处理函数。

4. 实操指南:从零搭建一个可扩展的 Generative UI 系统

4.1 初始化项目与基础渲染器

npm init -y npm install json-render react react-dom npm install -D @types/react @types/react-dom typescript ts-node

创建src/index.tsx:

import React from 'react'; import { createRenderer } from 'json-render'; import { ReactRenderer } from 'json-render/renderer/react'; // 1. 定义基础组件 const components = new Map<string, (props: any) => React.ReactElement>(); components.set('div', (props) => <div {...props} />); components.set('h1', (props) => <h1 {...props} />); components.set('p', (props) => <p {...props} />); components.set('button', (props) => <button {...props} />); // 2. 定义基础 action const actions = new Map<string, (payload: any, context: any) => Promise<any>>(); actions.set('alert', (payload) => Promise.resolve(alert(payload.message))); // 3. 创建 renderer const renderer = createRenderer({ components, actions, logger: console, }); // 4. 渲染 JSON const spec = { type: 'div', props: { className: 'container' }, children: [ { type: 'h1', props: { children: 'Hello Generative UI!' } }, { type: 'button', props: { children: 'Click me' }, events: { onClick: 'alert' } } ] }; const App = () => { const element = renderer.render(spec); return <ReactRenderer element={element} />; }; export default App;

4.2 构建可复用的组件工厂(src/components/index.ts)

不要直接注册原生 HTML 标签,而是构建语义化组件:

// src/components/input.tsx import React, { useState } from 'react'; export const Input = ({ label, placeholder, value, onChange, __context // 透传的 context }: { label: string; placeholder: string; value: string; onChange: (v: string) => void; __context: any; }) => { const [internalValue, setInternalValue] = useState(value); return ( <div> <label>{label}</label> <input value={internalValue} placeholder={placeholder} onChange={(e) => { setInternalValue(e.target.value); onChange?.(e.target.value); }} /> </div> ); }; // 注册到 components Map export const registerComponents = (components: Map<string, any>) => { components.set('input', Input); };

在index.tsx中引入:

import { registerComponents } from './components'; registerComponents(components);

4.3 实现复杂 action:表单提交与状态管理

// src/actions/form.ts import { fetch } from 'undici'; // Node.js 环境,浏览器用 window.fetch export const submitForm = async (payload: any, context: any) => { try { const response = await fetch(payload.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload.data) }); const result = await response.json(); // 更新全局状态 if (context.setState) { context.setState(prev => ({ ...prev, form: { ...prev.form, status: 'success', data: result } })); } return { success: true, data: result }; } catch (error) { console.error('Form submission failed:', error); return { success: false, error: error.message }; } }; // 注册 actions.set('submitForm', submitForm);

在 JSON spec 中使用:

{ "type": "input", "props": { "label": "Email", "placeholder": "your@email.com" }, "events": { "onChange": "updateFormState" } }, { "type": "button", "props": { "children": "Submit" }, "events": { "onClick": { "action": "submitForm", "payload": { "url": "/api/submit", "data": { "email": "{{form.email}}" } } } } }

注意{{form.email}}是模板语法,需在runtime中实现resolveTemplate函数,用context.state替换占位符。

4.4 动态组件加载与 Code Splitting

json-render支持type: "lazy:chart"这样的 lazy type:

// src/components/lazy.ts export const LazyComponent = ({ type, props }: { type: string; props: any }) => { const [Component, setComponent] = useState<React.ComponentType<any> | null>(null); useEffect(() => { const load = async () => { const module = await import(`./charts/${type.split(':')[1]}`); setComponent(module.default); }; load(); }, [type]); if (!Component) return <div>Loading...</div>; return <Component {...props} />; }; components.set('lazy', LazyComponent);

这样,JSON 中写"type": "lazy:bar-chart"就能按需加载图表组件,无需修改json-render核心。

4.5 生产环境优化:JSON Schema 缓存与 Diff 渲染

json-render默认每次render()都全量解析 JSON。对于高频更新的 UI(如实时仪表盘),需启用增量渲染:

// 启用 diff 渲染 const renderer = createRenderer({ components, actions, // 启用 diff 模式 diff: true, // 提供 diff 函数 diffFn: (oldJson, newJson) => { // 使用 fast-json-patch 或 custom diff return jsondiffpatch.diff(oldJson, newJson); } });

diffFn返回的 patch 对象会被renderer用于局部更新,避免整树 re-render。

5. 常见问题与避坑指南:那些文档没写的实战经验

5.1 问题速查表

问题现象可能原因解决方案
ComponentNotFoundError: 'button'componentsMap 未注册button,或注册 key 与 JSON 中type不一致检查components.set('button', ...),确认 JSON 中type: "button"(不是"Button"或"BUTTON")
TypeError: Cannot read property 'map' of undefinedchildren字段为null或undefined,而非[]在validate.ts中添加if (!node.children) node.children = [],或在 JSON 中显式写"children": []
Warning: Each child in a list should have a unique "key"json-render生成的 key 冲突(如两个同级节点 path 相同)检查 JSON 结构,确保children是数组而非对象;或手动为节点添加唯一key字段
Action 'submit' not foundactionsMap 未注册该 action,或拼写错误使用console.log(Array.from(actions.keys()))打印所有可用 action,确认大小写和连字符
XSS vulnerability detectedtext节点内容含<script>标签,DOMPurify未生效确认dompurify已正确安装;或在config中传入自定义sanitizer: (html) => html.replace(/<script>/g, '')

5.2 我踩过的三个深坑

坑一:Context 透传导致内存泄漏
最初我把整个context对象通过props.__context透传给每个组件,结果发现useEffect依赖__context时,每次render()都创建新context对象,导致useEffect无限执行。解决方案:只透传必要字段,如props.__contextPath = context.path,而不是整个context。json-render2.3.0 版本已修复此问题,改为 shallow copy。

坑二:Events 绑定丢失 this
在events中写"onClick": "handleClick",但handleClick是 class component 的方法,this指向丢失。根源是dispatchAction调用时this未绑定。解决方案:action handler 必须是纯函数,不要依赖this;或在注册时用actions.set('handleClick', handleClick.bind(this))。

坑三:SSR 时 useEffect 报错
json-render的ReactRenderer在 Node.js 环境下useEffect会报错。解决方案:SSR 时禁用useEffect,改用getServerSideProps预取数据,或在renderer中判断typeof window !== 'undefined'再调用useEffect。

5.3 性能调优实战技巧

  • 组件工厂缓存:对高频组件(如text,div),用memo包裹工厂函数,避免重复创建:

    components.set('text', React.memo(({ value }) => <span>{value}</span>));
  • Action 批量执行:多个effects可合并为一个 action,减少 Promise 开销:

    actions.set('initPage', async (payload, context) => { const [user, config, permissions] = await Promise.all([ fetch('/api/user'), fetch('/api/config'), fetch('/api/permissions') ]); return { user, config, permissions }; });
  • JSON Schema 预编译:ajv的compile函数很耗时,应在启动时预编译:

    const validator = ajv.compile(componentSpecSchema); export const validateComponentSpec = (json: any) => validator(json);

5.4 安全红线:必须遵守的三条铁律

  1. 永远不要在 JSON 中执行 eval 或 Function 构造器:json-render明确禁止props中出现函数值,这是安全底线。任何绕过此限制的 hack(如props: { onClick: "javascript:alert(1)" })都会被renderer过滤。

  2. XSS 防御必须双保险:text节点用DOMPurify,dangerouslySetInnerHTML用sanitizeHtml,用户输入的props.className用正则白名单过滤(只允许a-z0-9-_\s)。

  3. Action 权限必须隔离:config.actions应按环境区分。开发环境注册所有 action,生产环境只注册read类 action,write类 action 通过后端 API 代理,前端只发actionId,后端校验权限后再执行。

我在金融客户项目中,把actions分为public和private两个 Map,privateMap 的 action 只在登录后注入,且每个 action 都有requireAuth: true标记,dispatchAction会检查context.user是否存在。

6. 工程化落地建议:如何说服团队采用 Generative UI

6.1 从小处切入:先做“JSON 驱动的弹窗”

不要一上来就重构整个应用。选择一个低风险、高价值的模块:弹窗(Modal)。传统方式要写Modal.tsx、useModal.ts、openModal()函数。用json-render,只需:

  1. 设计弹窗 JSON Schema:{ "title": string, "content": string, "buttons": { "text": string, "action": string }[] }
  2. 后端返回 JSON:{ "title": "Confirm Delete", "content": "Are you sure?", "buttons": [{ "text": "Yes", "action": "deleteItem" }] }
  3. 前端renderer.render(json)即可

这样,产品可以随时改弹窗文案和按钮,无需发版。我们上线后,弹窗需求平均交付时间从 2 天降到 15 分钟。

6.2 建立 JSON Schema 管理规范

  • 所有 JSON spec 存放在src/specs/目录,按模块命名:form-login.json,dashboard-header.json
  • 每个 JSON 文件顶部加注释:// @schema-version 1.2.0 // @last-updated 2023-10-01
  • CI 流程加入jsonlint校验和ajvSchema 验证
  • 用git hooks检查 JSON diff,禁止大文件(>1MB)提交

6.3 构建可视化 JSON 编辑器

用monaco-editor封装一个在线 JSON 编辑器,集成:

  • 实时 Schema 校验(红波浪线下划线)
  • 组件预览(右侧实时渲染json-render结果)
  • Action 智能提示(输入"action": "时下拉显示所有注册 action)
  • 版本对比(Git diff 视图)

这个编辑器让非技术人员也能参与 UI 配置,真正实现“所见即所得”。

6.4 监控与告警体系

在logger中埋点:

  • json_render_start: 开始渲染时间戳
  • json_render_end: 结束时间戳,计算duration
  • json_render_error: 错误类型、JSON path、context
  • json_render_diff: oldJson 和 newJson 的 diff size(bytes)

设置告警规则:

  • duration > 500ms:渲染性能瓶颈
  • json_render_error.count > 10/hour:JSON 配置错误爆发
  • json_render_diff.size > 100KB:意外的大规模 UI 变更

我们用这套监控,在一次 CDN 配置错误导致 JSON 404 时,5 分钟内定位到问题,比传统方式快 3 小时。

最后再分享一个小技巧:json-render的createRenderer返回的实例,有一个隐藏的debug方法。调用renderer.debug(spec)会返回一个包含ast(抽象语法树)、context、renderedElements的 debug 对象,方便你在控制台深入 inspect 渲染过程。这个功能没写在文档里,但在源码runtime/debug.ts中有完整实现——这就是读源码的最大乐趣:发现那些藏在角落里的利器。