json-render:Generative UI 的终极框架 —— 让 AI 安全地生成界面
引言:当 AI 想要"画"界面
如果你用过 ChatGPT 或 Claude,你会发现它们回复的都是文字——无论多复杂的数据,最终呈现给用户的要么是 Markdown,要么是代码块。这就像请了一个天才设计师,却只允许他用打字机工作。
如果 AI 能直接生成界面本身呢? 不是生成描述界面的代码,而是生成一个可以立即渲染的 UI 结构?
这就是 Generative UI 的愿景,也正是 Vercel 开源的 json-render 要解决的核心问题。
一、传统方式的困境
为什么不直接让 AI 生成 React 代码?
最直觉的做法是让 LLM 直接输出 JSX 或 HTML,然后 eval 执行。但这条路有三个致命缺陷:
❌ 安全性 → AI 生成的代码可能包含任意 JavaScript 执行
❌ 可预测性 → LLM 可能"幻觉"出不存在的组件、无效的属性
❌ 跨平台 → React 代码无法直接跑在 React Native / Vue / Svelte 上
json-render 的核心洞察是:不要让 AI 生成代码,让它生成数据(JSON)。这份 JSON 严格约束在你预定义的组件范围内,然后由各平台的渲染器将其转化为原生 UI。
一句话概括:你设置围栏,AI 在围栏里自由发挥。
二、全局架构总览
在深入细节之前,先用两张图建立全局认知。
2.1 核心工作流(三步走)
┌─────────────┐ ┌─────────────────┐ ┌───────────────┐ ┌──────────────┐
│ 用户 Prompt │───▶│ AI + Catalog │───▶│ JSON Spec │───▶│ Renderer │
│ "创建仪表盘" │ │ (受限生成) │ │ (结构化数据) │ │ (原生UI) │
└─────────────┘ └─────────────────┘ └───────────────┘ └──────────────┘
│ │ │
✅ 有围栏的 ✅ 可预测的 ✅ 可流式的
第一步:你定义 Catalog("AI 能用什么组件和动作") 第二步:AI 根据 Catalog 的约束生成 JSON Spec("用这些组件搭出什么界面") 第三步:Renderer 把 JSON Spec 渲染成原生 UI("在屏幕上画出来")
2.2 包架构全景图
┌─────────────────────────────────┐
│ @json-render/core │
│ (Schema, Catalog, Prompt, │
│ Props, Visibility, State, │
│ SpecStream, Validation) │
└───────────────┬─────────────────┘
│
┌────────────┬────────────┼────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌──────────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│@json-render/ │ │ /vue │ │ /svelte │ │ /solid │ │ /react-native│
│ react │ │ │ │ │ │ │ │ │
└──────┬───────┘ └────────┘ └──────────┘ └──────────┘ └──────────────┘
│
┌────────┼──────────┬───────────┬──────────┬────────────┐
▼ ▼ ▼ ▼ ▼ ▼
┌───────┐┌────────┐┌─────────┐┌────────┐┌──────────┐┌────────────┐
│/shadcn││/remotion││/react- ││/react- ││ /image ││/react-three│
│(36个 ││(视频) ││ pdf ││ email ││(SVG/PNG) ││ -fiber │
│组件) ││ ││(PDF) ││(邮件) ││ ││ (3D) │
└───────┘└────────┘└─────────┘└────────┘└──────────┘└────────────┘
状态管理适配器: /redux /zustand /jotai /xstate
其他工具: /codegen /mcp /yaml
@json-render/core 是与框架无关的核心层,包含所有共享逻辑。各渲染器只负责将 JSON Spec 映射为各自平台的原生组件。
三、Schema / Catalog / Spec —— 先搞清这三兄弟
这三个概念经常被混淆,用一个类比就能记住:
┌──────────────────────────────────────────────────────┐
│ 类比:写作文 │
│ │
│ Schema = 语法规则(主谓宾怎么排列) │
│ Catalog = 词汇表 (你能用哪些词) │
│ Spec = 作文本身(AI 按语法用词汇写出的文章) │
└──────────────────────────────────────────────────────┘
Schema 定义 JSON 的骨架结构。内置的 React schema 使用扁平元素树:一个 root 键 + 一个 elements map。这种扁平结构是刻意设计的——比深层嵌套更适合 AI 生成和流式传输。
Catalog 定义"词汇"——有哪些组件、各自接受什么属性、有哪些可用动作。用 Zod 做类型约束。
Spec 就是 AI 最终产出的 JSON 文档,遵守 Schema 的结构,使用 Catalog 中的组件。
四、从零开始:Hello World(🌱 入门级)
4.1 安装
npm install @json-render/core @json-render/react
4.2 最简三步
// ① 定义 Catalog —— "AI 能用什么"
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
slots: ["default"],
description: "容器卡片",
},
Text: {
props: z.object({ content: z.string() }),
description: "文本段落",
},
},
actions: {},
});
// ② 定义 Registry —— "组件长什么样"
import { defineRegistry, Renderer } from '@json-render/react';
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<div style={{ border: '1px solid #ddd', padding: 16, borderRadius: 8 }}>
<h2>{props.title}</h2>
{children}
</div>
),
Text: ({ props }) => <p>{props.content}</p>,
},
});
// ③ 渲染一份手写的 Spec
const spec = {
root: "card-1",
elements: {
"card-1": {
type: "Card",
props: { title: "Hello json-render!" },
children: ["text-1"],
},
"text-1": {
type: "Text",
props: { content: "这是我的第一个 json-render 界面" },
children: [],
},
},
};
function App() {
return <Renderer spec={spec} registry={registry} />;
}
这就是全部!即使不接入 AI,json-render 也能作为一个 JSON 驱动的 UI 渲染引擎使用。
4.3 渲染流程图解
spec (JSON)
│
├── root: "card-1"
│
└── elements:
│
├── "card-1" ──▶ type: "Card" ──▶ registry 查到 Card 组件 ──▶ <div>
│ props.title: "Hello" <h2>Hello</h2>
│ children: ["text-1"] ↓ 递归渲染子节点
│
└── "text-1" ──▶ type: "Text" ──▶ registry 查到 Text 组件 ──▶ <p>第一个界面</p>
Renderer 读取 root,在 elements map 中查找该元素,匹配 registry 中的组件实现,递归渲染 children 引用的子元素。
五、接入 AI 生成(🌿 进阶级)
5.1 数据流全景
┌──────────┐ prompt ┌───────────┐ system prompt ┌──────────┐
│ 浏览器 │─────────▶│ API Route │───────────────▶│ LLM │
│ (React) │ │ (Next.js) │ │(Claude等)│
│ │◀─────────│ │◀────────────────│ │
│ useUI │ JSONL │ stream │ JSONL patches │ │
│ Stream │ patches │ Text │ │ │
└──────────┘ └───────────┘ └──────────┘
│
▼
Renderer ──▶ 原生 UI(边生成边渲染)
5.2 服务端:API Route
// app/api/generate/route.ts
import { streamText } from 'ai';
import { catalog } from '@/lib/catalog';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: 'anthropic/claude-haiku-4.5',
system: catalog.prompt(), // ← 自动从 Catalog 生成 system prompt
prompt,
});
return result.toTextStreamResponse();
}
catalog.prompt() 是关键——它把你的组件定义、属性约束、可用动作全部转化为 LLM 能理解的 system prompt,告诉 AI "你只能用这些积木"。
5.3 客户端:流式渲染
'use client';
import { Renderer, StateProvider, VisibilityProvider, useUIStream } from '@json-render/react';
import { registry } from '@/lib/registry';
export default function Page() {
const { spec, isStreaming, send } = useUIStream({
api: '/api/generate',
});
return (
<StateProvider initialState={{}}>
<VisibilityProvider>
<input
placeholder="描述你想要的界面..."
onKeyDown={(e) => {
if (e.key === 'Enter') send(e.currentTarget.value);
}}
/>
<Renderer spec={spec} registry={registry} loading={isStreaming} />
</VisibilityProvider>
</StateProvider>
);
}
用户输入 "创建一个登录表单",AI 会流式输出类似这样的 JSONL:
{"op":"add","path":"/root","value":"card-1"}
{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"登录"},"children":["email","pwd","btn"]}}
{"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"邮箱","name":"email","type":"email"}}}
{"op":"add","path":"/elements/pwd","value":{"type":"Input","props":{"label":"密码","name":"password","type":"password"}}}
{"op":"add","path":"/elements/btn","value":{"type":"Button","props":{"label":"登录"}}}
每一行到达,UI 就多渲染一个组件,用户看到界面在眼前"生长"出来。
5.4 秒用 shadcn/ui —— 36 个开箱即用组件
不想从头写组件?直接用预构建的 shadcn/ui 套件:
import { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';
import { shadcnComponents } from '@json-render/shadcn';
// Catalog:从 36 个组件中挑选你需要的
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Button: shadcnComponentDefinitions.Button,
Input: shadcnComponentDefinitions.Input,
Table: shadcnComponentDefinitions.Table,
// ... 一共 36 个可选
},
actions: {},
});
// Registry:对应实现一一映射
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Button: shadcnComponents.Button,
Input: shadcnComponents.Input,
Table: shadcnComponents.Table,
},
});
从 Accordion 到 Tooltip,Table 到 LineGraph,基本覆盖了 Web 应用的全部常见 UI 元素。
六、数据绑定 —— 让界面活起来(🌿 进阶级)
静态 JSON 只是起点。json-render 的表达式系统让 AI 生成的界面能绑定到运行时数据。
6.1 表达式速查表
┌──────────────┬──────────────────────────────────────┬──────────────┐
│ 表达式 │ 语法 │ 用途 │
├──────────────┼──────────────────────────────────────┼──────────────┤
│ $state │ { "$state": "/user/name" } │ 读取状态 │
│ $bindState │ { "$bindState": "/form/email" } │ 双向绑定 │
│ $item │ { "$item": "title" } │ 列表项字段 │
│ $index │ { "$index": true } │ 列表项索引 │
│ $cond │ { "$cond":..,"$then":..,"$else":..} │ 条件选择 │
│ $template │ { "$template": "Hi, ${/user/name}!" │ 字符串插值 │
│ $computed │ { "$computed": "fn", "args": {...} } │ 计算函数 │
└──────────────┴──────────────────────────────────────┴──────────────┘
6.2 实战:带状态的设置表单
这个例子展示了 $bindState 双向绑定——表单组件既能读取状态,也能写回状态:
{
"root": "card",
"state": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"notifications": true
},
"elements": {
"card": {
"type": "Card",
"props": { "title": "账户设置" },
"children": ["nameInput", "emailInput", "notifSwitch"]
},
"nameInput": {
"type": "Input",
"props": {
"label": "姓名",
"name": "name",
"value": { "$bindState": "/name" }
}
},
"emailInput": {
"type": "Input",
"props": {
"label": "邮箱",
"name": "email",
"type": "email",
"value": { "$bindState": "/email" }
}
},
"notifSwitch": {
"type": "Switch",
"props": {
"label": "接收邮件通知",
"name": "notifications",
"checked": { "$bindState": "/notifications" }
}
}
}
}
所有路径都是 JSON Pointer(RFC 6901):/name 指向 state.name,/notifications 指向 state.notifications。用户在输入框里修改内容,状态自动更新;状态变化后,所有引用该路径的组件自动重新渲染。
6.3 实战:repeat 列表渲染
{
"root": "todo-list",
"state": {
"todos": [
{ "id": "1", "title": "买牛奶", "done": false },
{ "id": "2", "title": "遛狗", "done": true }
]
},
"elements": {
"todo-list": {
"type": "Stack",
"props": { "direction": "vertical", "gap": "sm" },
"repeat": { "statePath": "/todos", "key": "id" },
"children": ["todo-item"]
},
"todo-item": {
"type": "Card",
"props": {
"title": { "$item": "title" }
},
"children": ["toggle"]
},
"toggle": {
"type": "Switch",
"props": {
"label": "完成",
"checked": { "$bindItem": "done" }
}
}
}
}
repeat 告诉渲染器:"遍历 /todos 数组,每一项都渲染 todo-item 和它的子元素"。$item 读取当前项的字段,$bindItem 实现列表项内的双向绑定。
6.4 表达式解析流程
原始 props(含表达式)
│
▼
resolvePropValue() ← core/props.ts 中的核心函数
│
├── 是 { $state } ? → getByPath(stateModel, path) 读值
├── 是 { $bindState } ? → 读值 + 暴露路径给组件写回
├── 是 { $item } ? → 从 repeatItem 中读字段
├── 是 { $index } ? → 返回当前循环索引
├── 是 { $cond } ? → evaluateVisibility(条件) → 选 $then 或 $else
├── 是 { $template } ? → 正则替换 ${/path} 为状态值
├── 是 { $computed } ? → 找到注册函数 → 递归解析 args → 调用函数
├── 是数组? → 递归解析每个元素
├── 是普通对象? → 递归解析每个值
└── 其他 → 原样返回(字面量)
所有表达式的解析在单次遍历中完成,且支持任意嵌套深度。
七、条件可见性(🌿 进阶级)
visible 字段让 AI 生成的界面可以根据状态条件显示/隐藏元素,而不需要写一行逻辑代码。
7.1 简单条件
{
"type": "Alert",
"props": { "message": "表单有错误" },
"visible": { "$state": "/form/hasErrors" }
}
当 /form/hasErrors 为真值时显示。
7.2 组合条件(AND + OR)
{
"type": "Button",
"props": { "label": "退款" },
"visible": [
{ "$state": "/auth/isSignedIn" },
{ "$state": "/user/role", "eq": "support" },
{ "$state": "/order/amount", "gt": 0 },
{ "$state": "/order/isRefunded", "not": true }
]
}
数组 = 隐式 AND。这个按钮只在"已登录 + 角色为客服 + 订单金额 > 0 + 未被退款"时才可见。
7.3 条件求值引擎
visible 条件
│
▼ evaluateVisibility() ← core/visibility.ts
│
├── undefined → true(无条件 = 可见)
├── boolean → 直接返回
├── 数组 → 隐式 AND(every)
├── { $and } → 显式 AND(every,支持嵌套)
├── { $or } → OR(some,支持嵌套)
└── 单条件 → evaluateCondition()
│
├── 无运算符 → Boolean(value) 真值判断
├── eq / neq → 相等 / 不等
├── gt / gte / lt / lte → 数值比较
└── not: true → 对结果取反
八、高级特性实战(🔥 高级)
8.1 Watchers + $computed:级联选择器
这是仓库 examples/no-ai 中的真实示例。当用户选择国家时,城市列表自动更新:
{
"root": "card",
"state": {
"form": { "country": "", "city": "" },
"availableCities": []
},
"elements": {
"card": {
"type": "Card",
"props": { "title": "收货地址" },
"children": ["countrySelect", "citySelect", "preview"]
},
"countrySelect": {
"type": "Select",
"props": {
"label": "国家",
"options": ["US", "Canada", "UK", "Germany", "Japan"],
"value": { "$bindState": "/form/country" }
},
"watch": {
"/form/country": [
{
"action": "setState",
"params": {
"statePath": "/availableCities",
"value": {
"$computed": "citiesForCountry",
"args": { "country": { "$state": "/form/country" } }
}
}
},
{
"action": "setState",
"params": { "statePath": "/form/city", "value": "" }
}
]
}
},
"citySelect": {
"type": "Select",
"props": {
"label": "城市",
"options": { "$state": "/availableCities" },
"value": { "$bindState": "/form/city" }
}
},
"preview": {
"type": "Heading",
"props": {
"text": {
"$computed": "formatAddress",
"args": {
"city": { "$state": "/form/city" },
"country": { "$state": "/form/country" }
}
},
"level": "h3"
}
}
}
}
交互流程图:
用户选择 "Japan"
│
▼ $bindState 写入 /form/country = "Japan"
│
▼ watch 触发
│
├── ① setState: /availableCities = citiesForCountry("Japan")
│ → ["Tokyo","Osaka","Kyoto",...]
│
└── ② setState: /form/city = "" (重置城市选择)
│
▼ citySelect 的 options 读取 $state: /availableCities → 下拉更新
▼ preview 的 $computed: formatAddress 重新计算 → 显示 "Japan"
注册 $computed 函数:
const computedFunctions = {
citiesForCountry: (args) => {
const cityData = { US: ["New York", "LA"], Japan: ["Tokyo", "Osaka"] };
return cityData[args.country] ?? [];
},
formatAddress: (args) => {
if (!args.city && !args.country) return "未选择地址";
if (!args.city) return args.country;
return `${args.city}, ${args.country}`;
},
};
8.2 跨字段表单验证 + validateForm
注册表单示例,展示了 json-render 的完整表单能力:
{
"type": "Input",
"props": {
"label": "确认密码",
"type": "password",
"value": { "$bindState": "/form/confirmPassword" },
"checks": [
{ "type": "required", "message": "请确认密码" },
{
"type": "matches",
"args": { "other": { "$state": "/form/password" } },
"message": "两次密码不一致"
}
],
"validateOn": "blur"
}
}
提交按钮使用内置的 validateForm 动作一键校验所有字段:
{
"type": "Button",
"props": { "label": "注册" },
"on": {
"press": [
{ "action": "validateForm", "params": { "statePath": "/result" } }
]
}
}
验证结果写入 /result,然后用 $cond 条件显示不同的提示:
{
"type": "Alert",
"props": {
"title": "验证结果",
"message": {
"$cond": { "$state": "/result/valid", "eq": true },
"$then": "所有字段验证通过,可以提交!",
"$else": "请修正上方的错误后再提交。"
},
"type": {
"$cond": { "$state": "/result/valid", "eq": true },
"$then": "success",
"$else": "error"
}
},
"visible": { "$state": "/result", "neq": null }
}
8.3 Inline 模式:聊天中的 Generative UI
仓库的 examples/chat 展示了最接近生产的用法——AI 聊天机器人在对话中嵌入动态 UI:
┌──────────────────────────────────────────────────┐
│ 用户: 比较纽约、伦敦和东京的天气 │
├──────────────────────────────────────────────────┤
│ AI: 这是三个城市的实时天气对比: │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ New York │ │ London │ │ Tokyo │ │
│ │ 22°C ☀️ │ │ 15°C 🌧 │ │ 28°C ⛅ │ │
│ │ Humidity: │ │ Humidity: │ │ Humidity: │ │
│ │ 65% │ │ 82% │ │ 70% │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ 纽约今天晴朗适合户外活动... │
└──────────────────────────────────────────────────┘
服务端使用 pipeJsonRender 分离文字和 JSONL patch:
import { pipeJsonRender } from '@json-render/core';
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeJsonRender(result.toUIMessageStream()));
},
});
客户端用 useJsonRenderMessage 从聊天消息中提取 spec:
function ChatMessage({ message }) {
const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);
return (
<div>
{/* 文字部分正常渲染 */}
{text && <p>{text}</p>}
{/* UI 部分用 Renderer 渲染 */}
{hasSpec && <Renderer spec={spec} registry={registry} />}
</div>
);
}
8.4 自定义 Action Handler:安全的交互模型
Actions 是 json-render 安全性的关键。AI 不生成代码,只声明意图:
┌──────────┐ JSON声明 ┌──────────────┐ 实际执行 ┌──────────┐
│ AI │───────────────▶│ Action 名称 │──────────────▶│ 你的代码 │
│"触发 │ { action: │ "submitForm" │ handler 里 │ fetch() │
│ submit" │ "submitForm" }│ │ 才有真正逻辑 │ 处理业务 │
└──────────┘ └──────────────┘ └──────────┘
❌ 不生成代码 ✅ 只是个名字 ✅ 你完全控制
const { registry, handlers } = defineRegistry(catalog, {
components: { /* ... */ },
actions: {
submitForm: async (params, setState) => {
const res = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(params),
});
const result = await res.json();
setState((prev) => ({ ...prev, formResult: result }));
},
confetti: () => {
// 放烟花!🎉
confettiListener?.();
},
},
});
九、状态管理深潜(🔥 高级)
9.1 内置 StateStore 的工作原理
immutableSetByPath("/user/name", "Bob")
│
├── 解析 JSON Pointer → ["user", "name"]
├── 浅拷贝 root → { ...root }
├── 浅拷贝 root.user → { ...root.user } ← 只拷贝受影响路径
├── 设置 root.user.name = "Bob"
└── 通知所有订阅者 → React 重新渲染
使用结构共享(structural sharing),只浅拷贝变更路径上的对象,未改变的分支保持原引用。这意味着 React 的 === 比较能正确跳过未变化部分。
9.2 接入外部状态管理
通过 createStoreAdapter 可以接入任何外部状态库,只需提供三个回调:
import { createStoreAdapter } from '@json-render/core';
// 只需实现 3 个方法
const store = createStoreAdapter({
getSnapshot: () => myZustandStore.getState(),
setSnapshot: (next) => myZustandStore.setState(next),
subscribe: (listener) => myZustandStore.subscribe(listener),
});
官方已提供 Redux、Zustand、Jotai、XState 四个适配器包。
十、跨平台能力矩阵
同一份 Catalog 定义,可以驱动完全不同的输出:
┌─────────────┬────────────────────────────────────┐
│ 渲染器 │ 输出 │
├─────────────┼────────────────────────────────────┤
│ /react │ 浏览器 DOM │
│ /vue │ Vue 3 组件树 │
│ /svelte │ Svelte 5 组件树(runes 响应式) │
│ /solid │ SolidJS 细粒度响应式组件 │
│ /react-native │ iOS/Android 原生视图 │
│ /shadcn │ 36 个精美预构建组件(Radix+Tailwind)│
│ /react-pdf │ PDF 文档(发票、报告) │
│ /react-email│ HTML 邮件 │
│ /remotion │ 视频合成(时间轴+轨道+转场) │
│ /image │ SVG/PNG(OG 图、社交卡片) │
│ /react-three-fiber │ 3D 场景(19 个内置组件) │
└─────────────┴────────────────────────────────────┘
生成 PDF 示例:
import { renderToBuffer } from '@json-render/react-pdf';
const spec = {
root: "doc",
elements: {
doc: { type: "Document", props: { title: "发票" }, children: ["page-1"] },
"page-1": { type: "Page", props: { size: "A4" }, children: ["heading", "table"] },
heading: { type: "Heading", props: { text: "发票 #1234", level: "h1" } },
table: {
type: "Table",
props: {
columns: [
{ header: "商品", width: "60%" },
{ header: "价格", width: "40%", align: "right" },
],
rows: [["Widget A", "¥68.00"], ["Widget B", "¥172.00"]],
},
},
},
};
const buffer = await renderToBuffer(spec);
生成 OG 图片:
import { renderToPng } from '@json-render/image/render';
const png = await renderToPng(spec, { fonts });
十一、两种生成模式对比
┌────────────────────┬────────────────────────────────┐
│ Standalone 模式 │ Inline 模式 │
├────────────────────┼────────────────────────────────┤
│ AI 只输出 JSONL │ AI 先写文字,需要时嵌入 JSONL │
│ 整个页面都是 UI │ UI 内嵌在聊天对话中 │
│ 适合:Playground │ 适合:聊天机器人 / Copilot │
│ 仪表盘构建器 │ 教育助手 / 智能客服 │
│ 表单生成器 │ │
├────────────────────┼────────────────────────────────┤
│ catalog.prompt() │ catalog.prompt({mode:"inline"})│
│ useUIStream │ pipeJsonRender + useChat │
└────────────────────┴────────────────────────────────┘
十二、设计哲学总结
┌──────────────────────────────────────────────────────────────┐
│ json-render 设计原则 │
├──────────────┬───────────────────────────────────────────────┤
│ 数据非代码 │ AI 生成 JSON 而非可执行代码,消除安全风险 │
│ 契约优先 │ Catalog = AI 与应用之间的严格契约, │
│ │ Zod schema 保证编译时 + 运行时双重类型安全 │
│ 渐进增强 │ 从最简单的静态渲染开始,逐步加入数据绑定、 │
│ │ 条件可见性、动作处理、表单验证等能力 │
│ 平台无关核心 │ core 包含所有共享逻辑(表达式解析、可见性 │
│ │ 求值、状态管理、流编译),渲染器只做组件映射 │
│ 声明式交互 │ AI 声明意图(action 名称),开发者提供实现, │
│ │ 永远不会有未经授权的代码执行 │
└──────────────┴───────────────────────────────────────────────┘
十三、完整实战:从零搭建一个 AI Dashboard Builder
下面把所有知识串起来,用一个完整示例展示 json-render 在真实项目中的全貌。
13.1 项目结构
my-dashboard/
├── app/
│ ├── api/generate/route.ts ← AI 生成接口
│ └── page.tsx ← 前端页面
├── lib/
│ ├── catalog.ts ← 组件目录定义
│ └── registry.tsx ← 组件实现 + 动作处理
└── package.json
13.2 Catalog:定义 AI 的"工具箱"
// lib/catalog.ts
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';
import { z } from 'zod';
export const catalog = defineCatalog(schema, {
components: {
// 布局类
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Grid: shadcnComponentDefinitions.Grid,
// 展示类
Heading: shadcnComponentDefinitions.Heading,
Text: shadcnComponentDefinitions.Text,
Badge: shadcnComponentDefinitions.Badge,
Table: shadcnComponentDefinitions.Table,
// 图表类
BarGraph: shadcnComponentDefinitions.BarGraph,
LineGraph: shadcnComponentDefinitions.LineGraph,
// 交互类
Button: shadcnComponentDefinitions.Button,
Input: shadcnComponentDefinitions.Input,
Select: shadcnComponentDefinitions.Select,
// 反馈类
Alert: shadcnComponentDefinitions.Alert,
Progress: shadcnComponentDefinitions.Progress,
},
actions: {
refresh_data: {
params: z.object({ source: z.string() }),
description: '刷新指定数据源',
},
export_report: {
params: z.object({ format: z.enum(['csv', 'pdf']) }),
description: '导出报告',
},
},
functions: {
formatCurrency: {
description: '将数字格式化为货币',
},
},
});
13.3 Registry:组件实现 + 动作处理
// lib/registry.tsx
import { defineRegistry } from '@json-render/react';
import { shadcnComponents } from '@json-render/shadcn';
import { catalog } from './catalog';
import type { ComputedFunction } from '@json-render/core';
export const { registry, handlers } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Grid: shadcnComponents.Grid,
Heading: shadcnComponents.Heading,
Text: shadcnComponents.Text,
Badge: shadcnComponents.Badge,
Table: shadcnComponents.Table,
BarGraph: shadcnComponents.BarGraph,
LineGraph: shadcnComponents.LineGraph,
Button: shadcnComponents.Button,
Input: shadcnComponents.Input,
Select: shadcnComponents.Select,
Alert: shadcnComponents.Alert,
Progress: shadcnComponents.Progress,
},
actions: {
refresh_data: async (params, setState) => {
const res = await fetch(`/api/data?source=${params.source}`);
const data = await res.json();
setState((prev) => ({ ...prev, [params.source]: data }));
},
export_report: async (params) => {
const blob = await fetch(`/api/export?format=${params.format}`)
.then(r => r.blob());
const url = URL.createObjectURL(blob);
window.open(url);
},
},
});
export const computedFunctions: Record<string, ComputedFunction> = {
formatCurrency: (args) => {
const value = Number(args.value ?? 0);
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(value);
},
};
13.4 API Route:对接 AI
// app/api/generate/route.ts
import { streamText } from 'ai';
import { catalog } from '@/lib/catalog';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: 'anthropic/claude-haiku-4.5',
system: catalog.prompt({
customRules: [
'用 Card 作为每个独立区块的容器',
'用 Grid 做多列布局,columns 根据内容数量合理选择',
'数值指标使用 Text + Badge 组合展示',
'始终提供 refresh_data 按钮让用户刷新数据',
],
}),
prompt,
});
return result.toTextStreamResponse();
}
13.5 前端页面:组装一切
// app/page.tsx
'use client';
import { useState } from 'react';
import {
Renderer, JSONUIProvider, useUIStream,
} from '@json-render/react';
import { registry, handlers, computedFunctions } from '@/lib/registry';
export default function DashboardBuilder() {
const [prompt, setPrompt] = useState('');
const { spec, isStreaming, send, clear } = useUIStream({
api: '/api/generate',
});
return (
<div className="min-h-screen bg-gray-50">
{/* 顶部输入栏 */}
<header className="border-b bg-white px-6 py-4">
<div className="max-w-4xl mx-auto flex gap-3">
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isStreaming) {
send(prompt);
setPrompt('');
}
}}
placeholder="描述你想要的仪表盘,比如:创建一个电商销售数据看板..."
className="flex-1 border rounded-lg px-4 py-2"
/>
<button
onClick={() => { send(prompt); setPrompt(''); }}
disabled={isStreaming || !prompt.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{isStreaming ? '生成中...' : '生成'}
</button>
<button onClick={clear} className="px-4 py-2 border rounded-lg">
重置
</button>
</div>
</header>
{/* 渲染区域 */}
<main className="max-w-6xl mx-auto p-6">
<JSONUIProvider
registry={registry}
initialState={spec?.state ?? {}}
handlers={handlers}
functions={computedFunctions}
>
<Renderer spec={spec} registry={registry} loading={isStreaming} />
</JSONUIProvider>
</main>
</div>
);
}
13.6 效果:用户输入 → AI 生成 → 即时渲染
用户输入: "创建一个电商销售数据看板,包含总收入、订单量、转化率,
以及最近7天的销售趋势图和热销商品排行表"
AI 逐行输出 JSONL patch:
→ /root = "dashboard"
→ /elements/dashboard = Grid(columns:2) [metrics, chart, table]
→ /elements/metrics = Stack [revenue, orders, conversion]
→ /elements/revenue = Card > Text "总收入 ¥128,450"
→ /elements/orders = Card > Text "订单量 1,234" ← 每行到达,UI 多一块
→ /elements/conversion = Card > Badge "转化率 3.2%"
→ /elements/chart = Card > LineGraph(7天趋势)
→ /elements/table = Card > Table(热销商品)
→ /elements/refresh = Button "刷新数据"
整个过程:用户看到界面在屏幕上一块一块地"生长"出来
十四、与其他方案的对比
┌────────────────┬───────────────┬──────────────┬──────────────┐
│ │ json-render │ AI 生成代码 │ AI 填充数据 │
│ │ (Generative │ (v0/Bolt │ (传统方式) │
│ │ UI) │ 等) │ │
├────────────────┼───────────────┼──────────────┼──────────────┤
│ AI 生成的是什么 │ JSON 数据 │ 源代码 │ 文本/数据 │
│ 运行时安全 │ ✅ 无代码执行 │ ❌ 需沙箱 │ ✅ 安全 │
│ 实时流式渲染 │ ✅ 逐行渲染 │ ❌ 整体编译 │ N/A │
│ UI 可变性 │ ✅ 每次不同 │ ✅ 每次不同 │ ❌ 固定布局 │
│ 跨平台 │ ✅ 12+ 渲染器 │ ❌ 单平台 │ ❌ 单平台 │
│ 类型安全 │ ✅ Zod + TS │ ⚠️ 不确定 │ ✅ 可控 │
│ 适合场景 │ 运行时动态UI │ 开发时生成 │ 数据展示 │
└────────────────┴───────────────┴──────────────┴──────────────┘
json-render 的定位是运行时的 Generative UI——界面在用户使用过程中由 AI 实时生成,而不是在开发阶段生成代码。这与 v0 等代码生成工具互补而非竞争。
十五、适用场景速查
✅ 非常适合:
• AI 聊天机器人需要展示丰富 UI(不只是文字)
• 动态仪表盘 / 数据看板生成器
• 表单生成器(AI 根据需求自动构建表单)
• CMS 后台(JSON 驱动的页面渲染)
• 多端统一(同一份 Spec 驱动 Web + Mobile + PDF + Email)
⚠️ 需要评估:
• 高度定制化的交互(复杂拖拽、画布编辑器等)
• 性能极致敏感的场景(每次渲染都经过表达式解析层)
❌ 不太适合:
• 完全静态的、不需要动态生成的页面
• 需要像素级精确控制的设计稿还原
结语
json-render 代表了一种有趣的范式转移:从"AI 辅助开发者写代码"到"AI 直接为用户生成界面"。它的核心智慧在于找到了一个平衡点——让 AI 拥有足够的创造自由(可以自由组合组件、选择布局、绑定数据),同时保持绝对的安全边界(只能用你定义的组件、只能触发你实现的动作)。
如果你正在构建 AI 驱动的产品,json-render 至少值得你花一个下午深入了解。从一个简单的 Renderer + 手写 Spec 开始,逐步加入 AI 生成和流式渲染,你会发现这套"JSON 驱动 UI"的思路打开了一个全新的产品设计空间。
🔗 GitHub: github.com/vercel-labs… 🔗 官方文档: json-render.dev 📦 核心安装:
npm install @json-render/core @json-render/react📦 快速体验:npm install @json-render/shadcn(36 个预构建组件)