☰
把 json-render 接进生产,我踩过的坑:安全边界、流式渲染与组件白名单
2026/10/10 17:11:45 网站建设 项目流程

把 json-render 接进生产,我踩过的坑:安全边界、流式渲染与组件白名单

【免费下载链接】json-renderThe Generative UI framework项目地址: https://gitcode.com/GitHub_Trending/js/json-render

Vercel Labs 开源的 json-render 在过去一段时间里几乎刷屏了整个前端圈:上线几天 GitHub Star 数就从 7500 冲到 1 万+,核心卖点一句话就能讲完——"让 AI 在开发者定义的护栏里生成界面"。它不再让大模型直接吐 HTML/JSX 这种不可控的代码,而是输出一份 JSON spec,由你自己的渲染器把 spec 变成 UI。

这个思路确实漂亮,但"demo 很炫"和"能上线"之间隔着一整条生产链路。我把它接进真实业务后,先后踩过三个大坑:安全边界到底划在哪、流式渲染怎么做才不崩、组件白名单怎么设计 AI 才不会乱来。这篇文章不是产品吹捧,是基于仓库源码的工程复盘,希望能帮后来者少走弯路。

接入前的高频假设与现实的落差

接触 json-render 之前,大多数人(包括我)会有三组默认假设:

  • 假设一:安全是框架自动保证的。实际上框架只保证"约束 AI 的输出结构",真正的安全边界要靠你定义 catalog 时自己划出来;
  • 假设二:流式渲染就是把流接进来就行。实际上 LLM 输出是逐 token 到达的,spec 是增量 patch 拼接出来的,任何一个半行 JSON 处理不当,整棵树就会渲染出残次品;
  • 假设三:白名单组件越多越好。实际上 AI 的"自由发挥"和你的维护成本成正比,白名单设计得越克制,输出越稳定。

这三组落差,正好对应下面三个坑。

坑一:安全边界——真正的护栏是"组件 + 动作 + 数据绑定"三张白名单

json-render 的第一条设计原则写在 README.md 里:"AI can only use components in your catalog"(AI 只能用你 catalog 里的组件)。这句话很容易被误读为"框架替我挡住了所有风险"。真相是:框架只提供了一个机制,边界划在哪里完全取决于你。

在核心包 schema.ts 里,defineCatalog要求你同时声明两件事:AI 能用的组件(components)和 AI 能触发的动作(actions)。AI 生成的 spec 里出现的每一个type、每一个action,都被限制在这两个字典里。渲染器侧的 registry.tsx 则要求你为每个组件名提供你自己写的实现——这意味着 AI 永远不可能凭空执行任意函数,它只能组合你提供的积木。

这是整个安全模型的根基:与其让 LLM 输出可执行代码,不如让它输出"数据 + 受控引用",执行路径完全掌握在应用手里。我踩的第一个坑就是早期想让 AI 直接"操作"某些内部方法,最后被硬拉回白名单模型——这其实是对的,只是当时没想明白。

动作(actions)是第二张安全面,也是最容易漏的一块。核心包里 actions.ts 的ActionBinding结构给出了生产级动作该有的所有维度:

export interface ActionBinding { /** Action name (must be in catalog) */ action: string; /** Parameters to pass to the action handler */ params?: Record<string, DynamicValue>; /** Confirmation dialog before execution */ confirm?: ActionConfirm; /** Handler after successful execution */ onSuccess?: ActionOnSuccess; /** Handler after failed execution */ onError?: ActionOnError; /** Whether to prevent default browser behavior (e.g. navigation on links) */ preventDefault?: boolean; }

注意几个细节:每个动作的params可以用 Zod schema 做参数校验(ActionDefinition.params);confirm支持危险操作前的确认弹窗;onSuccess/onError可以链式触发navigate、set状态或另一个动作。生产环境里,"删除"这类动作必须配 confirm,重路由必须经navigate而不是任意跳转,参数必须过 schema。这些能力框架都给了,不用等于没设护栏。

第三张白名单是数据绑定边界。spec 里可以出现$state、$cond、$template、$computed这类动态表达式(见 README.md 的 Dynamic Props 一节),它们会读写你的 state model。这意味着 AI 理论上能读到的状态范围,就是它能表达的状态范围。接入时我把它收窄成了"只暴露业务需要的状态子树",而不是把整个应用状态丢进去——$computed引用的是你注册的函数表,同理只注册受控函数。

坑二:流式渲染——真正的挑战在于"半行 JSON"和"部分 spec"

流式是 json-render 相对 A2UI 这类方案最明显的体验优势:模型还在生成,界面已经在一段一段地"长"出来。但把它跑稳,比想象中难。

SpecStream 的本质是 RFC 6902 patch 流

核心包把流式格式叫SpecStream,在 types.ts 里说得很直白:"每条 SpecStream 行都是一个 JSON patch 操作,逐步构建出最终的 spec。"解析一行就是严格判定它是不是合法的 patch:

export function parseSpecStreamLine(line: string): SpecStreamLine | null { const trimmed = line.trim(); if (!trimmed || !trimmed.startsWith("{")) return null; try { const patch = JSON.parse(trimmed) as SpecStreamLine; if (patch.op && patch.path !== undefined) return patch; return null; } catch { return null; } }

然后applySpecStreamPatch支持 RFC 6902 的全部六种操作:add、replace、remove、move、copy、test。我最初天真地以为 AI 会一次性吐出一棵完整的 UI 树,接入后才发现:AI 是按路径一点一点"补"出这棵树的。理解这一点,流式方案才算入门。

半行缓冲:第一个真正的生产级 bug

从fetch拿到的是流式 chunk,一个 JSON patch 很可能被切在任意位置。正确做法是先攒 buffer,按换行符切行,把最后一段不完整的行留回 buffer,等下一个 chunk 到来再续上。官方 playground 的实现 use-playground-stream.ts 里正是这个模式:

buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { const result = parseLine(line.trim()); if (!result) continue; currentSpec = applySpecPatch(currentSpec, result.patch); setSpec({ ...currentSpec }); }

我踩过的版本是"简单按行 split 直接解析",结果在流式响应较快时频繁出现 JSON 解析失败——因为整行还没到齐。把最后一段lines.pop()留回 buffer是这个坑的唯一解药。另外,逐条 patch 应用后要setSpec({ ...currentSpec })产生新引用,否则 React 不会触发重渲染——use-playground-stream.ts 里每轮都强制展开。

部分 spec 的渲染稳定性:半成品也要能"画出来"

流式的另一层挑战是:AI 还没画完,UI 就得先显示。一个只有root但elements还是空的对象,一个children引用了尚未生成的节点,都必须被安全地渲染成"加载中"而不是抛异常。

官方在 React 渲染器 renderer.tsx 里对root缺失、elements[spec.root]不存在都做了兜底,并提供loading/fallback参数;同时用useElementSignatures(spec)做元素签名稳定化,避免流式更新导致已渲染子树无谓重建。这个稳定性在 CHANGELOG.md 0.21.0 里被专门点名:"Stabilized streamed React renders and added coverage for incomplete streamed props and nested prop identities"。生产接入时,给Renderer的loading状态做骨架屏,比任何优化都重要——用户在等 AI 出界面,本身就是在等一个异步过程,视觉反馈不能缺。

编辑模式:patch 之外的 merge 与 diff

流式不只是"生成",还有"改稿"。playground 支持 JSONL 和 YAML 两种线格式,YAML 模式下有 spec / edit / patch / diff 四种围栏,edit 模式用deepMergeSpec合并、再用diffToPatches把差异转回 patch 流应用(见 use-playground-stream.ts)。diff 模式甚至允许 LLM 直接输出 unified diff,服务端先 apply 再重新序列化。这些高级编辑模式每个都有自己的坑(比如围栏闭合判定、合并结果的类型校验),如果业务只需要"生成 + 局部改稿",起步阶段只放开 patch 模式就够了,等链路稳定再逐步加 merge 和 diff。

坑三:组件白名单——不是限制,而是给 AI 的"受控词表"

第三个坑是关于 catalog 设计的。很多人(包括最初的我自己)把白名单理解成"能开多少组件就开多少",结果 AI 经常用错组件、传错 props,或者在同一件事上有七八种表达方式。

白名单条目要带"使用说明书"

AI 对组件的理解,取决于你在 catalog 里给了它多少上下文。看 playground 的实现 catalog.ts,每个组件都配了description和example,甚至会用否定句约束使用场景:

Card: { props: z.object({ title: z.string().nullable(), description: z.string().nullable(), maxWidth: z.enum(["sm", "md", "lg", "full"]).nullable(), centered: z.boolean().nullable(), }), slots: ["default"], description: "Container card for content sections. Use for forms/content boxes, NOT for page headers.", example: { title: "Overview", description: "Your account summary" }, },

description会进系统提示词,example则是少样本示例。这一行 "NOT for page headers" 看起来是给 AI 的,实际上是在帮你减少一半的返工。写白名单本质上是在写 prompt engineering,只是载体是 catalog。

少即是多:schema 越简单,AI 越不出错

catalog.ts 顶部有句非常务实的设计注释:"Keep schemas simple — one format per prop, no unions. Fewer components = less confusion for the AI."我实际体会到的正是如此:props 用z.union或过于自由的z.string(),AI 会频繁生成边缘值;而z.enum把可能值列死,输出立刻稳定下来。白名单和 schema 的自由度,要反着 AI 的随机性来设计——越随意,越失控。

结构校验与 autofix 兜底

即使白名单设计得再好,LLM 偶尔还是会产出结构错误的 spec。核心包的 spec-validator.ts 专门为"AI 常见错误"做校验,检查项包括:缺失 root、root 指向不存在的元素、children引用不存在的节点、把visible误放进props而不是元素顶层、孤立元素等等:

const result = validateSpec(spec); if (!result.valid) { console.log("Spec errors:", result.issues); }

更贴心的是autoFixSpec——比如children里的悬空引用会被直接修剪掉。生产接入建议在应用 patch 之后、交给渲染器之前做一次轻量校验,把error级问题拦在渲染前,warning级问题留作日志。另外,如果走 LLM 结构化输出接口(OpenAI/Gemini/Anthropic),schema.ts 提供了catalog.jsonSchema()导出 strict JSON Schema(additionalProperties: false、全部属性进required),这个模式值得单独踩点——它直接决定了模型端"结构幻觉"的概率。

给后来者的避坑清单

最后,把上面所有经验压缩成一份可以直接对照的清单:

  1. 安全边界三件套:组件白名单、动作白名单、状态绑定边界,三者都要显式收紧,不要依赖框架"默认安全";
  2. 动作必须有 schema + confirm:涉及副作用(写库、跳转、删除)的动作,参数过z校验,危险操作配确认弹窗,onError一定要设计——AI 生成的 UI 触发失败动作时,用户不该看到白屏;
  3. 流式解析必须做半行缓冲:按\n切行,最后一段留回 buffer;每行解析失败要静默跳过而不是中断整个流;
  4. 每次 patch 应用后产生新 spec 引用,用展开运算符触发 React 重渲染;
  5. 给Renderer配loading/fallback:半成品 spec 会频繁出现,视觉反馈不能缺;
  6. 白名单克制:每个组件带description+example,props 优先z.enum和z.number()这类窄类型,避免 union 和自由字符串;
  7. 校验 + autofix 兜底:patch 应用后过一遍validateSpec,必要时autoFixSpec,把 AI 的结构错误消化在渲染之前;
  8. 结构化输出优先:能用catalog.jsonSchema()的 strict 模式就别让模型自由发挥纯文本输出;
  9. 起步只开 patch 编辑模式:merge、diff、多围栏 YAML 这些高级模式,等链路稳定再逐步放开。

json-render 解决的是"AI 生成 UI"里最核心的工程问题:把不可控的生成,收敛成可组合、可校验、可流式的数据。它确实配得上"Generative UI 框架"的定位,但框架给的是机制,不是默认安全、默认稳定——这两样东西,得靠接入方一坑一坑踩出来。希望这份清单能让你踩得少一点。

【免费下载链接】json-renderThe Generative UI framework项目地址: https://gitcode.com/GitHub_Trending/js/json-render

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询