我最近一直在纠结一件事:让 AI 生成界面,到底该让它直接吐 JSX,还是只吐一份被约束过的 JSON。前者灵活,但模型一次手滑就可能输出一段不存在的组件、把样式写成灾难;后者死板一点,但至少每次输出都在你的掌控里。Vercel Labs 的 json-render 押的是后者,而且把「约束」两个字做成了整个框架的卖点。
| 项目名片 | 信息 |
|---|---|
| 项目地址 | github.com/vercel-labs/json-render |
| 主要语言 / License | TypeScript / Apache-2.0 |
| Stars(2026-09 实查) | 17,388 |
| npm 月下载(@json-render/core,实查) | 约 512 万 |
| 适合谁 | 正在做 AI 产品、需要「模型动态生成界面」又怕它乱来的团队 |
它到底怎么把 AI 框住
json-render 的核心思路一句话就能说清:模型不写代码,只写一份 JSON,这份 JSON 只能引用你提前定义好的「目录」(catalog)。目录里装着三样东西——组件、动作、函数,每一项都带一个 zod schema 死死卡住它的入参。
这意味着坏也坏不到哪去。模型想输出一个「按钮」,它只能从你目录里的 Button 组件里选一个,props 也必须是 zod 校验过的字段,多一个属性、少一个必填值都会被拦下来。与其说它在「生成 UI」,不如说它在「你画好的盒子里拼积木」。
这跟早期「让模型直接返回 React 元素」的做法有本质区别。那套方案模型什么都能给你吐,看上去自由,但上线后你会发现自己得为每一种越界的输出写补丁;json-render 反过来,先框住边界,再谈自由。
一份 Spec 长什么样
它把界面描述成一份叫 Spec 的 JSON,最简形态长这样(下面这段是从仓库 examples/no-ai/lib/examples.ts 里摘的,没改动):
{
"root": "btn",
"elements": {
"btn": {
"type": "Button",
"props": { "label": "Click me", "variant": "primary" },
"on": { "press": { "action": "confetti" } }
}
}
}
root 指定根节点,elements 是一张「id → 节点」的映射表,每个节点声明 type(组件名)、props、children(子节点 id 列表),以及 on(事件 → 动作)。光看这个还不觉得厉害,重点是它的动态能力。我读源码时印象最深的是这几条内建指令:
- $bindState / $state:把某个输入框的值双向绑到 JSON Pointer 路径上,比如
/form/name; - $template:字符串插值,比如
"欢迎, ${/form/name}"; - $computed:调用目录里声明的函数算出一个值,比如拼地址、按国家拉城市列表;
- $cond / $then / $else:条件渲染,整段逻辑都塞在 JSON 里,不用写一行宿主代码。
换句话说,Spec 不只是一棵静态组件树,它自带一套足够写业务表单的状态、校验和条件逻辑。样例里的「注册表单」「级联下拉」都是纯 JSON 驱动,一个 React 组件都不用额外写。
「跨平台」是真有三个平台槽位,不是嘴炮
同一个 Spec,交给不同的渲染器就出不同的货。README 里列了一长串:Web 端的 React、Vue、Svelte、Solid,移动端的 React Native,还有 3D(react-three-fiber)、视频(Remotion)、PDF、邮件、终端界面(Ink)、图片(Satori)这些平时想都想不到的渲染目标。更狠的是 @json-render/next——它能把 JSON 直接变成带路由、布局、SSR 的整站应用。
这件事的意义不在「炫技」,而在复用。你为 Web 写死的那套「AI 能生成哪些 UI」的目录,理论上能原样搬到移动端、搬到终端、搬到邮件里。约束只定义一次,输出面却铺开了十几个。
跑起来花了我一点功夫,不过真跑通了
先踩了个坑:仓库根目录的 package.json 写死要求 Node ≥ 24、pnpm ≥ 11,而我机器默认是 Node 20。pnpm 11 依赖的 node:sqlite 内置模块在 Node 20 里根本不存在,第一次 install 直接抛 ERR_UNKNOWN_BUILTIN_MODULE 崩了。这其实是个挺常见的「版本门槛」,换个思路就过:nvm use 24 切过去,我本机 nvm 里正好装着 v24.19.0。
切过去之后一路顺:依赖 pnpm install 1 分 05 秒装完,core 包 tsup 构建只用了 34ms。为了确认它不是「装上了但跑不动」,我写了个几行的小脚本,用核心导出的 validateSpec 实跑两份 Spec——一份是 README 示例里的合法 confetti,一份故意把 children 指到一个不存在的节点。真实输出如下:
=== 合法 Spec(Confetti) ===
valid: true | issues: []
=== 非法 Spec(引用了不存在的子节点) ===
valid: false | issues: [{"severity":"error","message":"Element \"root\" references child \"nonexistent\" which does not exist in the elements map.","elementKey":"root","code":"missing_child"}]
看到这条 missing_child 我才算实打实懂了它说的「约束」是什么:不是口号,是每一份 Spec 都过一层校验,引用错了节点当场被打回。这比文档里写一百句「安全」都管用。至于流式渲染到底多顺滑,那得跑完整 demo,我这次只验到了校验这一层,先不明说。
谁该用,谁先等等
| 场景 | 结论 |
|---|---|
| 正在做 AI 产品的团队,需要可控的「模型出 UI」 | 值得认真看,尤其你已经踩在 Next.js + Vercel AI SDK 这条链上 |
| 想要一套 spec 同时伺候 Web / 移动端 / 特殊渲染目标 | 这是它最亮的地方,复用约束、铺开输出面 |
| 纯静态站点、没有 AI 生成界面的需求 | 别引入,纯属给自己找抽象层 |
| 生产环境还锁在 Node 20 的老项目 | 门槛是实打实的(Node 24 + pnpm 11),个人玩用 nvm 一行就过,但整条产线迁移要想清楚 |
| 不敢赌实验项目 | 它是 Vercel 的 LABS 项目,v0.x、API 还在 breaking(0.20 就改过动作绑定签名) |
一句话:json-render 不是又一个 UI 库,它是一套「让 AI 画 UI 但只许在框里画」的协议与运行时。值不值得花这个学习成本,取决于你到底有多想让模型去生成界面、而又多不敢让它乱来。