json-render:Generative UI 的终极框架 —— 让 AI 安全地生成界面

引言:当 AI 想要"画"界面

如果你用过 ChatGPT 或 Claude,你会发现它们回复的都是文字------无论多复杂的数据,最终呈现给用户的要么是 Markdown,要么是代码块。这就像请了一个天才设计师,却只允许他用打字机工作。

如果 AI 能直接生成界面本身呢? 不是生成描述界面的代码,而是生成一个可以立即渲染的 UI 结构?

这就是 Generative UI 的愿景,也正是 Vercel 开源的 json-render 要解决的核心问题。


一、传统方式的困境

为什么不直接让 AI 生成 React 代码?

最直觉的做法是让 LLM 直接输出 JSX 或 HTML,然后 eval 执行。但这条路有三个致命缺陷:

arduino 复制代码
❌ 安全性  → AI 生成的代码可能包含任意 JavaScript 执行
❌ 可预测性 → LLM 可能"幻觉"出不存在的组件、无效的属性
❌ 跨平台  → React 代码无法直接跑在 React Native / Vue / Svelte 上

json-render 的核心洞察是:不要让 AI 生成代码,让它生成数据(JSON)。这份 JSON 严格约束在你预定义的组件范围内,然后由各平台的渲染器将其转化为原生 UI。

一句话概括:你设置围栏,AI 在围栏里自由发挥。


二、全局架构总览

在深入细节之前,先用两张图建立全局认知。

2.1 核心工作流(三步走)

scss 复制代码
┌─────────────┐    ┌─────────────────┐    ┌───────────────┐    ┌──────────────┐
│  用户 Prompt │───▶│ AI + Catalog     │───▶│  JSON Spec    │───▶│  Renderer    │
│ "创建仪表盘" │    │ (受限生成)       │    │ (结构化数据)   │    │ (原生UI)     │
└─────────────┘    └─────────────────┘    └───────────────┘    └──────────────┘
                        │                       │                     │
                   ✅ 有围栏的              ✅ 可预测的           ✅ 可流式的

第一步 :你定义 Catalog("AI 能用什么组件和动作") 第二步 :AI 根据 Catalog 的约束生成 JSON Spec("用这些组件搭出什么界面") 第三步:Renderer 把 JSON Spec 渲染成原生 UI("在屏幕上画出来")

2.2 包架构全景图

bash 复制代码
                        ┌─────────────────────────────────┐
                        │       @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 ------ 先搞清这三兄弟

这三个概念经常被混淆,用一个类比就能记住:

ini 复制代码
┌──────────────────────────────────────────────────────┐
│  类比:写作文                                         │
│                                                      │
│  Schema  = 语法规则(主谓宾怎么排列)                   │
│  Catalog = 词汇表 (你能用哪些词)                      │
│  Spec    = 作文本身(AI 按语法用词汇写出的文章)          │
└──────────────────────────────────────────────────────┘

Schema 定义 JSON 的骨架结构。内置的 React schema 使用扁平元素树:一个 root 键 + 一个 elements map。这种扁平结构是刻意设计的------比深层嵌套更适合 AI 生成和流式传输。

Catalog 定义"词汇"------有哪些组件、各自接受什么属性、有哪些可用动作。用 Zod 做类型约束。

Spec 就是 AI 最终产出的 JSON 文档,遵守 Schema 的结构,使用 Catalog 中的组件。


四、从零开始:Hello World(🌱 入门级)

4.1 安装

bash 复制代码
npm install @json-render/core @json-render/react

4.2 最简三步

tsx 复制代码
// ① 定义 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 渲染流程图解

bash 复制代码
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 数据流全景

scss 复制代码
┌──────────┐  prompt   ┌───────────┐  system prompt   ┌──────────┐
│  浏览器   │─────────▶│  API Route │───────────────▶│   LLM    │
│  (React)  │          │  (Next.js) │                 │(Claude等)│
│           │◀─────────│            │◀────────────────│          │
│  useUI    │ JSONL    │  stream    │  JSONL patches  │          │
│  Stream   │ patches  │  Text     │                 │          │
└──────────┘          └───────────┘                 └──────────┘
     │
     ▼
  Renderer ──▶ 原生 UI(边生成边渲染)

5.2 服务端:API Route

ts 复制代码
// 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 客户端:流式渲染

tsx 复制代码
'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:

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 套件:

tsx 复制代码
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 表达式速查表

bash 复制代码
┌──────────────┬──────────────────────────────────────┬──────────────┐
│  表达式       │  语法                                │  用途         │
├──────────────┼──────────────────────────────────────┼──────────────┤
│  $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 双向绑定------表单组件既能读取状态,也能写回状态:

json 复制代码
{
  "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 列表渲染

json 复制代码
{
  "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 表达式解析流程

bash 复制代码
原始 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 简单条件

json 复制代码
{
  "type": "Alert",
  "props": { "message": "表单有错误" },
  "visible": { "$state": "/form/hasErrors" }
}

/form/hasErrors 为真值时显示。

7.2 组合条件(AND + OR)

json 复制代码
{
  "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 条件求值引擎

scss 复制代码
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 中的真实示例。当用户选择国家时,城市列表自动更新:

json 复制代码
{
  "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"
      }
    }
  }
}

交互流程图:

bash 复制代码
用户选择 "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 函数:

ts 复制代码
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 的完整表单能力:

json 复制代码
{
  "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 动作一键校验所有字段:

json 复制代码
{
  "type": "Button",
  "props": { "label": "注册" },
  "on": {
    "press": [
      { "action": "validateForm", "params": { "statePath": "/result" } }
    ]
  }
}

验证结果写入 /result,然后用 $cond 条件显示不同的提示:

json 复制代码
{
  "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:

yaml 复制代码
┌──────────────────────────────────────────────────┐
│  用户: 比较纽约、伦敦和东京的天气                     │
├──────────────────────────────────────────────────┤
│  AI: 这是三个城市的实时天气对比:                     │
│                                                  │
│  ┌────────────┐ ┌────────────┐ ┌────────────┐   │
│  │  New York   │ │  London    │ │  Tokyo     │   │
│  │   22°C ☀️   │ │  15°C 🌧   │ │  28°C ⛅   │   │
│  │  Humidity:  │ │  Humidity: │ │  Humidity: │   │
│  │    65%      │ │    82%     │ │    70%     │   │
│  └────────────┘ └────────────┘ └────────────┘   │
│                                                  │
│  纽约今天晴朗适合户外活动...                         │
└──────────────────────────────────────────────────┘

服务端使用 pipeJsonRender 分离文字和 JSONL patch:

ts 复制代码
import { pipeJsonRender } from '@json-render/core';

const stream = createUIMessageStream({
  execute: async ({ writer }) => {
    writer.merge(pipeJsonRender(result.toUIMessageStream()));
  },
});

客户端用 useJsonRenderMessage 从聊天消息中提取 spec:

tsx 复制代码
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 不生成代码,只声明意图:

javascript 复制代码
┌──────────┐  JSON声明      ┌──────────────┐  实际执行     ┌──────────┐
│   AI     │───────────────▶│  Action 名称  │──────────────▶│ 你的代码  │
│"触发     │ { action:      │ "submitForm"  │ handler 里    │ fetch()  │
│ submit"  │  "submitForm" }│              │ 才有真正逻辑   │ 处理业务  │
└──────────┘               └──────────────┘              └──────────┘
     ❌ 不生成代码               ✅ 只是个名字              ✅ 你完全控制
tsx 复制代码
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 的工作原理

javascript 复制代码
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 可以接入任何外部状态库,只需提供三个回调:

ts 复制代码
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 定义,可以驱动完全不同的输出:

bash 复制代码
┌─────────────┬────────────────────────────────────┐
│  渲染器      │  输出                              │
├─────────────┼────────────────────────────────────┤
│  /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 示例:

ts 复制代码
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 图片:

ts 复制代码
import { renderToPng } from '@json-render/image/render';

const png = await renderToPng(spec, { fonts });

十一、两种生成模式对比

javascript 复制代码
┌────────────────────┬────────────────────────────────┐
│   Standalone 模式   │        Inline 模式             │
├────────────────────┼────────────────────────────────┤
│  AI 只输出 JSONL    │  AI 先写文字,需要时嵌入 JSONL   │
│  整个页面都是 UI    │  UI 内嵌在聊天对话中              │
│  适合:Playground   │  适合:聊天机器人 / Copilot      │
│       仪表盘构建器   │       教育助手 / 智能客服        │
│       表单生成器     │                                │
├────────────────────┼────────────────────────────────┤
│  catalog.prompt()   │  catalog.prompt({mode:"inline"})│
│  useUIStream        │  pipeJsonRender + useChat       │
└────────────────────┴────────────────────────────────┘

十二、设计哲学总结

ini 复制代码
┌──────────────────────────────────────────────────────────────┐
│                    json-render 设计原则                        │
├──────────────┬───────────────────────────────────────────────┤
│  数据非代码   │ AI 生成 JSON 而非可执行代码,消除安全风险        │
│  契约优先    │ Catalog = AI 与应用之间的严格契约,              │
│             │ Zod schema 保证编译时 + 运行时双重类型安全        │
│  渐进增强    │ 从最简单的静态渲染开始,逐步加入数据绑定、        │
│             │ 条件可见性、动作处理、表单验证等能力              │
│  平台无关核心 │ core 包含所有共享逻辑(表达式解析、可见性        │
│             │ 求值、状态管理、流编译),渲染器只做组件映射       │
│  声明式交互   │ AI 声明意图(action 名称),开发者提供实现,     │
│             │ 永远不会有未经授权的代码执行                     │
└──────────────┴───────────────────────────────────────────────┘

十三、完整实战:从零搭建一个 AI Dashboard Builder

下面把所有知识串起来,用一个完整示例展示 json-render 在真实项目中的全貌。

13.1 项目结构

bash 复制代码
my-dashboard/
├── app/
│   ├── api/generate/route.ts    ← AI 生成接口
│   └── page.tsx                 ← 前端页面
├── lib/
│   ├── catalog.ts               ← 组件目录定义
│   └── registry.tsx             ← 组件实现 + 动作处理
└── package.json

13.2 Catalog:定义 AI 的"工具箱"

ts 复制代码
// 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:组件实现 + 动作处理

tsx 复制代码
// 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

ts 复制代码
// 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 前端页面:组装一切

tsx 复制代码
// 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 生成 → 即时渲染

ini 复制代码
用户输入: "创建一个电商销售数据看板,包含总收入、订单量、转化率,
          以及最近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 "刷新数据"

整个过程:用户看到界面在屏幕上一块一块地"生长"出来

十四、与其他方案的对比

css 复制代码
┌────────────────┬───────────────┬──────────────┬──────────────┐
│                │  json-render  │ AI 生成代码   │  AI 填充数据  │
│                │  (Generative  │ (v0/Bolt     │  (传统方式)    │
│                │   UI)         │  等)          │              │
├────────────────┼───────────────┼──────────────┼──────────────┤
│ AI 生成的是什么 │ JSON 数据     │ 源代码       │ 文本/数据     │
│ 运行时安全     │ ✅ 无代码执行  │ ❌ 需沙箱    │ ✅ 安全       │
│ 实时流式渲染   │ ✅ 逐行渲染   │ ❌ 整体编译  │ N/A          │
│ UI 可变性      │ ✅ 每次不同   │ ✅ 每次不同  │ ❌ 固定布局   │
│ 跨平台         │ ✅ 12+ 渲染器 │ ❌ 单平台    │ ❌ 单平台     │
│ 类型安全       │ ✅ Zod + TS   │ ⚠️ 不确定    │ ✅ 可控       │
│ 适合场景       │ 运行时动态UI  │ 开发时生成   │ 数据展示      │
└────────────────┴───────────────┴──────────────┴──────────────┘

json-render 的定位是运行时的 Generative UI------界面在用户使用过程中由 AI 实时生成,而不是在开发阶段生成代码。这与 v0 等代码生成工具互补而非竞争。


十五、适用场景速查

swift 复制代码
✅ 非常适合:
  • 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 个预构建组件)

相关推荐
DigitalOcean2 小时前
OpenClaw 用不了 Claude?90%团队都卡在这一步
openai·agent·claude
王小酱2 小时前
PageAgent-住在网页里的 AI 操控员
openai·ai编程·aiops
王小酱2 小时前
A2UI 深度解读:让 AI Agent "说出"用户界面的开放协议
openai·ai编程·aiops
米小虾3 小时前
hackerbot-claw 攻击事件深度解析:AI Agent 时代的安全警钟
github·ai编程
进击的野人3 小时前
Prompt工程入门指南:写给AI学习新手的提示词秘籍
人工智能·aigc·ai编程
甲维斯3 小时前
用完火山,腾讯,阿里的编程模型,我失眠了!
ai编程
树獭叔叔4 小时前
别再盲目堆残差了!Moonshot AI 的 AttnRes 如何让 LLM 训练提速 25%?
后端·aigc·openai
码路飞4 小时前
GPT-5.4 mini 和 nano 昨天刚发,我连夜测了一下,说说真实感受
gpt·openai·api
KevinZhang135794 小时前
第 8 节:集成 CubeJS 数据模型
ai编程·vibecoding