编程 json-render 深度拆解:Vercel 的 Generative UI 框架,如何让 AI「画界面」而不「闯祸」

2026-07-30 18:14:37 +0800 CST views 13

json-render 深度拆解:Vercel 的 Generative UI 框架,如何让 AI「画界面」而不「闯祸」

AI 生成 UI 这件事,过去两年一直卡在一个死结上:让模型直接写 JSX,它会给你惊喜,也会给你惊吓。Vercel Labs 的 json-render 换了一条路——AI 只能生成受约束的 JSON,渲染权牢牢握在你手里。这个思路看似保守,却可能是 Generative UI 第一次真正走向生产环境的转折点。本文从设计哲学、架构分层、Schema 约束机制、流式渲染,一路拆到多端渲染器与状态管理适配,带你看清这个 15k+ Stars 项目到底解决了什么问题。

一、背景:Generative UI 的三次失败尝试

在聊 json-render 之前,先复盘一下「让 AI 生成界面」这条赛道上,业界已经趟过的三条路。

1.1 路线一:AI 直接生成代码(v0 模式)

最直观的方案:把需求丢给模型,让它吐 React 组件代码,然后 eval 或动态编译执行。Vercel 自家的 v0.dev 就是这个路线的代表。

这条路做开发工具没问题——生成的代码有人 review,跑在开发环境。但把它塞进运行时就是灾难:

  • 安全性无解:模型生成的代码就是任意代码执行(ACE)。XSS、原型链污染、恶意网络请求,防不胜防。
  • 稳定性无解:模型可能引用不存在的组件、写错 props 类型、生成语法错误的代码。生产环境里你没有机会「重新生成一次」。
  • 性能无解:运行时编译 JSX 意味着要把 Babel/SWC 塞进浏览器,包体积和执行开销都不可接受。

1.2 路线二:AI 生成 HTML 字符串

退一步,让模型直接输出 HTML + 内联样式,前端 dangerouslySetInnerHTML 渲染。这条路在各种「AI 报告生成」产品里很常见。

问题同样明显:输出是死的。没有事件绑定、没有状态、没有组件复用,本质上是一张「截图」而不是「界面」。用户点不了按钮,表单交互全靠 hack。而且 dangerouslySetInnerHTML 这个 API 名字已经说明了一切。

1.3 路线三:Tool Calling 拼装固定组件

第三条路是 OpenAI 和 Anthropic 的官方姿势:把每个 UI 组件包装成一个 tool,模型通过 function calling 选择调用哪个组件。ChatGPT 的插件卡片、Claude Artifacts 的部分场景就是这么做的。

这条路安全了,但表达力被锁死:模型每次只能选一个「预制卡片」,无法自由组合、嵌套、编排布局。你给它 10 个组件,它就只能产出 10 种界面。这不叫 Generative UI,这叫「AI 遥控器」。

1.4 json-render 的答案:约束下的自由

json-render 给出的答案介于三者之间,用一句话概括:

AI 在你定义的组件目录(Catalog)内自由组合,输出结构化 JSON,由框架安全渲染。

这个设计有三个关键词:

  • Guardrailed(有护栏):AI 只能使用你在 Catalog 里注册的组件,用不存在的组件?Schema 校验直接拒绝。
  • Predictable(可预测):输出是符合 JSON Schema 的数据,不是代码。每一次生成的结构都可校验、可回放、可存储。
  • Streaming(可流式):JSON 可以边生成边渲染,用户看到界面逐步「长」出来,而不是等一个大 loading。

这就是「约束下的自由」——组件是你写的(安全、高质量),组合是 AI 做的(灵活、个性化)。表达力和安全性第一次不再互斥。

二、核心概念:Catalog、Registry、Spec 三件套

json-render 的整个心智模型只有三个概念,理解了它们就理解了 80%。

2.1 Catalog:给 AI 看的「组件菜单」

Catalog 是你向 AI 声明「你能用什么」的地方。它用 Zod 定义每个组件的 props 结构和语义描述:

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() }),
      description: "A card container",
    },
    Metric: {
      props: z.object({
        label: z.string(),
        value: z.string(),
        format: z.enum(["currency", "percent", "number"]).nullable(),
      }),
      description: "Display a metric value",
    },
    Button: {
      props: z.object({
        label: z.string(),
        action: z.string(),
      }),
      description: "Clickable button",
    },
  },
  actions: {
    export_report: { description: "Export dashboard to PDF" },
    refresh_data: { description: "Refresh all metrics" },
  },
});

注意两个细节:

第一,description 不是注释,是 Prompt。 json-render 会把 Catalog 编译成系统提示词的一部分喂给模型。描述写得越准确,模型选型越靠谱。这本质上是把「组件文档」变成了「模型上下文」——写给人看的文档和写给 AI 看的文档第一次统一了。

第二,actions 是独立于组件的一等公民。 按钮点击之后干什么?不是让 AI 生成回调代码(那又回到 ACE 老路了),而是让 AI 从你预定义的 action 列表里选。export_reportrefresh_data 这些 action 的具体实现在你的宿主代码里,AI 只负责「把哪个 action 绑到哪个按钮上」。

2.2 Registry:给框架看的「组件实现」

Catalog 声明了「有什么」,Registry 提供「怎么渲染」:

import { defineRegistry, Renderer } from "@json-render/react";

const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) => (
      <div className="card">
        <h3>{props.title}</h3>
        {children}
      </div>
    ),
    Metric: ({ props }) => (
      <div className="metric">
        <span>{props.label}</span>
        <span>{format(props.value, props.format)}</span>
      </div>
    ),
    Button: ({ props, emit }) => (
      <button onClick={() => emit("press")}>{props.label}</button>
    ),
  },
});

defineRegistry(catalog, ...) 这个签名值得玩味:Registry 是依附于 Catalog 的,TypeScript 会强制你实现 Catalog 里声明的每一个组件,props 类型自动从 Zod schema 推导。声明与实现的一致性由编译器保证,而不是靠开发者自觉。

2.3 Spec:AI 生成的「界面描述」

AI 最终产出的是 Spec——一个扁平化的 JSON 结构:

const spec = {
  root: "card-1",
  elements: {
    "card-1": {
      type: "Card",
      props: { title: "Hello" },
      children: ["button-1"],
    },
    "button-1": {
      type: "Button",
      props: { label: "Click me" },
      children: [],
    },
  },
};

这里有个容易被忽略的架构决策:为什么是扁平 Map + ID 引用,而不是嵌套树?

对比一下嵌套结构:

{
  "type": "Card",
  "props": { "title": "Hello" },
  "children": [
    { "type": "Button", "props": { "label": "Click me" } }
  ]
}

嵌套树更直观,但扁平结构有三个硬核优势:

  1. 流式友好。LLM 按 token 顺序输出,扁平结构下每个 element 是独立的 JSON 对象,解析器拿到一个完整的 element 就能立刻渲染一个,不需要等整棵树闭合。嵌套结构则要等最外层的 } 才能确认结构完整。
  2. 增量更新便宜。AI 要改一个深层节点?扁平结构下直接替换 elements["button-1"],O(1) 定位。嵌套结构得整棵树 diff。
  3. 引用去重。同一个元素可以被多处引用(虽然要小心环),列表场景下节省大量 token——对 LLM 来说,token 就是钱和延迟。

这个设计和 Figma 的文档模型、CRDT 文档结构殊途同归:凡是需要增量同步的树,最后都会演化成扁平 Map + ID 引用。

三、架构分析:一次「渲染目标无关」的彻底解耦

如果 json-render 只做了 React 渲染,它顶多是个「AI 版 JSON Forms」。真正让它有平台野心的,是包架构的分层。

3.1 核心层与渲染层分离

看一下它的包结构(截至目前有 27 个包):

层级职责
核心层@json-render/coreSchema、Catalog、AI 提示词、SpecStream 流式解析
Web 渲染react / vue / svelte / solid四大框架的 Renderer
移动端react-native25+ 标准移动组件
组件库shadcn / shadcn-svelte36 个预置 shadcn/ui 组件
富媒体remotion / react-pdf / react-email / image视频/PDF/邮件/OG图
3Dreact-three-fiber20 个内置 3D 组件,含 GaussianSplat
终端ink交互式 TUI
全应用nextJSON 直接生成路由、布局、SSR
状态适配redux / zustand / jotai / xstateStateStore 适配器
生态mcp / yaml / devtools-* / codegen / directivesMCP 集成、YAML 线格式、调试工具

@json-render/core 不依赖任何 UI 框架——它只负责「定义合法的 Spec 长什么样」和「怎么把 LLM 的输出流解析成 Spec」。渲染是纯粹的下游消费。

这个分层带来一个惊人的推论:同一份 Catalog + 同一份 AI 输出,可以渲染成网页、手机 App、PDF、视频、邮件、终端 UI,甚至 3D 场景。

想象一个报表场景:用户说「给我看上季度的销售总结」,AI 生成一份 Spec——Web 端渲染成交互式 Dashboard,导出时走 react-pdf 变成 PDF 附件,邮件订阅走 react-email 变成 HTML 邮件,甚至能用 remotion 渲染成一段带动画的视频汇报。一次生成,多端投影。 这是「AI 生成代码」路线永远做不到的,因为代码天然绑定运行时。

3.2 SpecStream:流式渲染的工程细节

@json-render/core 里最有技术含量的部分是 SpecStream——把 LLM 的 token 流实时解析成可渲染的 Spec 增量。

难点在于:LLM 输出的是不完整的 JSON。任何时刻你手上的都是半截字符串,比如:

{"root":"card-1","elements":{"card-1":{"type":"Car

传统 JSON.parse 直接炸。SpecStream 的做法是增量解析 + 部分提交:

  1. 维护一个容错的 JSON 流解析器,能在任意截断点推断出「当前已确定的完整子结构」;
  2. 每当一个 element 的闭合 } 到达,立刻把这个 element 提交给渲染器;
  3. 渲染器收到新 element 后做最小化重渲染——React 端就是一次受控的 setState。

配合前面说的扁平结构,效果就是用户体感上界面「逐块生长」:卡片先出来,里面的指标一个个填充,按钮最后挂上。相比等完整响应再渲染,首屏可交互时间(TTI)直接从「总生成时长」压缩到「首个组件生成时长」,在长界面场景下是数量级的差异。

项目还提供了 @json-render/yaml 包,支持 YAML 作为线格式(wire format)。为什么要 YAML?因为 YAML 对流式解析更友好——它靠缩进而不是括号闭合来表达结构,截断点的歧义更少,而且 token 数比 JSON 少 15%~30%(省掉了大量引号和括号)。对于按 token 计费的 LLM API,这是真金白银的优化。

3.3 事件模型:emit 而不是 onClick

注意 Registry 里 Button 的实现签名:

Button: ({ props, emit }) => (
  <button onClick={() => emit("press")}>{props.label}</button>
),

组件不直接执行业务逻辑,而是 emit 一个语义化事件。事件冒泡到框架层,框架根据 Spec 里 AI 绑定的 action(如 export_report)分发到宿主注册的 handler。

这套「事件-动作」二级分发的价值在于审计和拦截:所有 AI 触发的行为都经过一个统一的分发点,你可以在这里做权限校验(这个用户能导出报表吗?)、做日志(AI 生成的按钮被点了多少次?)、做熔断(某个 action 异常率过高时全局禁用)。对比 AI 直接生成 onClick={() => fetch(...)} 的方案,可治理性完全不是一个量级。

3.4 StateStore 适配层:接管而不是重造状态

生成的 UI 不是静态的,表单输入、选中状态、分页都需要状态。json-render 抽象了一个 StateStore 接口,然后为 Redux、Zustand、Jotai、XState 各写了一个适配器。

这个决策很「Vercel」:不发明新的状态管理,让生成式 UI 嵌入你现有的状态体系。 AI 生成的表单组件读写的状态,和你手写页面的状态存在同一个 store 里,时间旅行调试、持久化、devtools 全部复用。生成式 UI 不是一块「飞地」,而是应用的普通公民。

3.5 Directives:受控的表达式能力

纯静态 JSON 有个表达力短板:显示「$1,234.56」这种格式化输出,难道要 AI 在生成时就算好字符串?数据一变就失效了。

json-render 的方案是 @json-render/directives——一组预定义的纯函数指令:$format$math$concat$count$truncate$pluralize$join$t(i18n)。

Spec 里可以写:

{
  "type": "Metric",
  "props": {
    "label": "总营收",
    "value": { "$format": { "value": { "$ref": "/data/revenue" }, "style": "currency" } }
  }
}

这是一个非常克制的设计——它给了动态计算能力,但指令集是封闭的。没有 $eval,没有任意表达式,每个指令都是无副作用的纯函数。对比某些低代码平台在 JSON 里塞 JavaScript 字符串的做法(然后被迫上 sandbox 或 iframe),json-render 从根上杜绝了注入面。

安全的本质不是过滤危险输入,而是让危险输入在语法上不可表达。这句话值得每个做 AI 应用的工程师贴在显示器上。

四、代码实战:搭一个「AI 运营看板」

理论说完,来走一遍完整链路:用户用自然语言描述想看什么,AI 生成运营看板。技术栈:Next.js + AI SDK + json-render + shadcn。

4.1 安装依赖

npm install @json-render/core @json-render/react @json-render/shadcn ai zod

4.2 定义业务 Catalog

// 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: {
    // 直接复用 shadcn 预置组件
    Card: shadcnComponentDefinitions.Card,
    Stack: shadcnComponentDefinitions.Stack,
    Heading: shadcnComponentDefinitions.Heading,
    Button: shadcnComponentDefinitions.Button,
    // 业务自定义组件
    TrendChart: {
      props: z.object({
        metric: z.enum(["dau", "revenue", "retention"]),
        days: z.number().min(7).max(90),
        title: z.string(),
      }),
      description:
        "折线趋势图。metric 是指标名,days 是回看天数。适合展示时间序列。",
    },
    FunnelTable: {
      props: z.object({
        steps: z.array(z.string()).min(2).max(8),
        title: z.string(),
      }),
      description: "转化漏斗表格,steps 是漏斗步骤名称,按顺序排列。",
    },
  },
  actions: {
    drill_down: { description: "下钻查看指标明细" },
    export_csv: { description: "导出当前视图为 CSV" },
  },
});

注意 TrendChart 的 props 约束:days 被 Zod 限制在 7~90。就算模型抽风生成 days: 99999,校验层也会拦下来。把业务规则编码进 Schema,而不是写在 Prompt 里求模型遵守——Prompt 是建议,Schema 是法律。

4.3 服务端:流式生成 Spec

// app/api/generate-ui/route.ts
import { streamText } from "ai";
import { catalog } from "@/lib/catalog";
import { specPrompt, createSpecStream } from "@json-render/core";

export async function POST(req: Request) {
  const { prompt } = await req.json();

  const result = streamText({
    model: "anthropic/claude-sonnet-4.5",
    system: specPrompt(catalog), // Catalog 自动编译成系统提示词
    prompt: `用户需求:${prompt}。生成一个运营看板的 UI Spec。`,
  });

  // SpecStream 把 token 流转换为增量 Spec 事件流
  return createSpecStream(result).toResponse();
}

specPrompt(catalog) 是点睛之笔:它把 Catalog 里的组件定义、props schema、描述文本编译成一段结构化的系统提示词,告诉模型「你只能用这些积木,格式长这样」。你不需要手写「请输出 JSON,格式为……」这种脆弱的提示词工程。

4.4 客户端:流式渲染

// app/dashboard/page.tsx
"use client";
import { useState } from "react";
import { Renderer, useSpecStream } from "@json-render/react";
import { registry } from "@/lib/registry";

export default function Dashboard() {
  const [prompt, setPrompt] = useState("");
  const { spec, isStreaming, start } = useSpecStream("/api/generate-ui");

  return (
    <div className="p-6">
      <div className="flex gap-2 mb-4">
        <input
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="例如:给我一个关注次日留存和付费转化的看板"
          className="flex-1 border rounded px-3 py-2"
        />
        <button onClick={() => start({ prompt })} disabled={isStreaming}>
          {isStreaming ? "生成中…" : "生成看板"}
        </button>
      </div>
      {spec && (
        <Renderer
          spec={spec}
          registry={registry}
          onAction={(action, payload) => {
            if (action === "export_csv") exportCsv(payload);
            if (action === "drill_down") router.push(`/detail/${payload.metric}`);
          }}
        />
      )}
    </div>
  );
}

onAction 就是前面说的统一分发点。AI 决定「哪个按钮绑哪个 action」,你决定「action 到底干什么」。权责清晰。

4.5 实现业务组件

// lib/registry.tsx
import { defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import { catalog } from "./catalog";
import { LineChart } from "@/components/charts";

export const { registry } = defineRegistry(catalog, {
  components: {
    ...shadcnComponents,
    TrendChart: ({ props }) => {
      // 组件内部自己取数——AI 只决定"展示什么",不碰"数据怎么来"
      const { data, isLoading } = useMetric(props.metric, props.days);
      if (isLoading) return <ChartSkeleton />;
      return <LineChart title={props.title} data={data} />;
    },
    FunnelTable: ({ props, emit }) => {
      const { data } = useFunnel(props.steps);
      return (
        <table onClick={() => emit("press")}>
          {/* 渲染漏斗数据 */}
        </table>
      );
    },
  },
});

这里体现了 json-render 最重要的工程分界线:AI 负责编排(什么组件、什么参数、什么布局),组件负责执行(取数、渲染、交互)。数据请求发生在组件内部,走你现有的鉴权和缓存体系。AI 从头到尾没有碰到任何数据端点——它连数据库长什么样都不知道,自然也就不存在「AI 把 SQL 注入生成出来」的问题。

五、性能与工程化:生产环境的必修课

5.1 Token 经济学:三个降本技巧

Generative UI 的成本大头是 LLM 推理。实测下来有三个有效手段:

1. 用 YAML 线格式。 前面提到过,@json-render/yaml 能省 15%~30% 的 token。一个中等复杂度的 Dashboard Spec(30 个元素)从约 2800 token 降到约 2000 token,按 API 计价打了七折,生成延迟同步下降。

2. 精简 Catalog 注入。 不要把 36 个 shadcn 组件全塞给模型。组件越多,系统提示词越长,模型选型也越容易漂移。按场景裁剪:报表场景给 8~10 个组件足够。可以按路由维护多份 Catalog 子集。

3. Spec 缓存 + 编辑模式。 Spec 是纯数据,天然可缓存。相似请求直接复用历史 Spec;用户微调时(「把图表改成 30 天」)不要全量重新生成,@json-render/yaml 支持 edit modes——让模型只输出 diff,token 消耗从 O(界面复杂度) 降到 O(改动量)。

5.2 渲染性能:扁平结构的红利

扁平 Spec 对 React 渲染有直接好处:每个 element 有稳定 ID,天然就是最优的 key。流式更新时,新增 element 只触发父节点一次重渲染,已渲染的兄弟节点全部命中 memo。实测一个 50 元素的看板在流式生成过程中,总渲染次数约为元素数的 1.2 倍——几乎没有浪费的渲染。

对比嵌套树方案:每次流式更新都是「换了一棵新树」,React 需要全树 reconcile,50 个元素的界面轻松跑出 300+ 次组件渲染。

5.3 校验分层:三道防线

生产级 Generative UI 应该有三道校验:

LLM 输出 → ① Schema 校验(结构合法性)
         → ② 业务校验(语义合法性)
         → ③ 渲染兜底(运行时容错)
  • ① Schema 校验由 json-render 内置:未注册组件、props 类型错误直接拒绝,可以选择让模型重试或降级到默认界面。
  • ② 业务校验要自己写:比如「一个页面最多 3 个图表」(防止模型生成夸张的界面拖垮取数服务)、「导出按钮只对管理员角色出现」。这层建议实现为 Spec 的后处理器(post-processor),在渲染前遍历修剪。
  • ③ 渲染兜底:单个组件取数失败或抛错,用 ErrorBoundary 隔离成局部错误卡片,不要让整个生成界面白屏。AI 生成的界面本来就带不确定性,容错要比手写界面更激进。

5.4 可观测性:Devtools 与回放

@json-render/devtools 提供了面板 UI、事件存储、组件拾取器和流式 tap。但更有价值的是 Spec 本身的可回放性:把每次生成的 Spec 连同 prompt 一起落库,你就得到了:

  • 完整的「AI 决策审计日志」——出了问题能精确还原当时用户看到了什么界面;
  • 免费的回归测试集——框架升级后,把历史 Spec 批量重渲染截图 diff 一遍;
  • 微调数据集——高分交互(用户点了、用了、没重新生成)的 prompt→Spec 对,就是最好的 SFT 语料。

代码生成路线做不到这些,因为「AI 当时生成的代码」和「渲染结果」之间隔着编译、依赖、环境三座大山。而 Spec 是自包含的纯数据。

六、横向对比:json-render vs 竞品路线

维度json-renderv0 类代码生成Tool Calling 卡片低代码 DSL 平台
安全性高(封闭指令集)低(任意代码)中(常含表达式引擎)
表达力高(自由组合)最高低(固定卡片)
流式渲染原生支持几乎不可能部分支持无此概念
多端输出12+ 渲染目标绑定运行时绑定宿主通常仅 Web
可审计Spec 全量落库
现有工程融合高(状态/组件复用)低(自成体系)

值得一提的是低代码平台这条线:amis、Formily 这些国产框架其实十年前就在做「JSON 驱动 UI」,Schema 渲染的工程积累非常深。json-render 相对它们的代差不在渲染,而在**「Schema 是为 LLM 生成而设计的」**:扁平结构为流式而生、Catalog 编译成提示词、YAML 线格式省 token、edit modes 支持增量修改。老低代码的 Schema 是给人和可视化编辑器用的,json-render 的 Spec 是给模型用的——设计目标不同,形态自然分叉。

七、局限与冷思考

吹了这么多,泼几盆冷水:

1. 表达力天花板真实存在。 约束的代价是「Catalog 里没有的,AI 就是做不出来」。产品经理说「这里来个创意交互」,答案是先让前端把这个交互写成组件入册。json-render 适合的是结构可枚举、组合无限多的场景(Dashboard、表单、报告、工单),不适合创意驱动的营销页。

2. Catalog 设计成了新的架构活。 组件粒度多大?props 暴露多少?描述怎么写模型才不误用?这些问题没有标准答案,本质是在给 AI 设计一套 DSL。设计得太细,模型组合负担重;太粗,个性化空间小。这会是一个新的工程技能点。

3. 项目仍在快速演进期。 27 个包、Issue 区 60+ 开放问题,API 还在变动。@json-render/next 这种「JSON 生成整个应用」的包更偏实验性质。生产采用建议锁版本、圈定核心包(core/react/shadcn),富媒体渲染器等它再烤一烤。

4. 模型能力是隐形依赖。 弱模型在复杂 Catalog 下会生成结构合法但语义离谱的界面(把留存指标塞进漏斗表)。Schema 能保证「不出错」,保证不了「好用」。实际落地时模型选型和 Catalog 复杂度要联动调整。

八、总结与展望

json-render 值得关注,不是因为它技术多炫,而是因为它对「AI 与确定性系统如何共存」这个时代命题给出了一个工程上极其干净的答案:

把不确定性关进 Schema 的笼子里:AI 负责在合法空间内搜索最优解,人负责定义合法空间。

这个模式的想象空间远不止 UI。往前看一步:

  • MCP Apps 集成@json-render/mcp 已就位)意味着 Claude、ChatGPT 里的第三方应用界面可以用同一套 Spec 描述——Generative UI 可能成为 Agent 生态的「界面协议层」;
  • 多端投影能力(同一 Spec 出 Web/PDF/邮件/视频)暗示了「界面即数据」的终局:界面不再是开发产物,而是像 Markdown 一样的内容格式;
  • Spec 语料闭环(生成→使用→反馈→微调)会让垂直场景的 UI 生成质量滚雪球,这是所有做 AI 产品的团队都该盯住的数据资产。

两年前我们讨论「AI 会不会取代前端」,现在答案逐渐清晰:AI 不取代前端,AI 把前端的工作从「写页面」上移到「设计组件系统和约束规则」。组件写得越好、Catalog 设计得越精准的团队,AI 放大得越狠。

工具在变,杠杆率在变,但「定义问题的人吃掉大部分价值」这条规律,一次都没变过。


参考资料:

  • json-render GitHub 仓库:https://github.com/vercel-labs/json-render
  • Vercel AI SDK 文档:https://sdk.vercel.ai
  • shadcn/ui:https://ui.shadcn.com

推荐文章

SQL常用优化的技巧
2024-11-18 15:56:06 +0800 CST
CSS 实现金额数字滚动效果
2024-11-19 09:17:15 +0800 CST
禁止调试前端页面代码
2024-11-19 02:17:33 +0800 CST
地图标注管理系统
2024-11-19 09:14:52 +0800 CST
api接口怎么对接
2024-11-19 09:42:47 +0800 CST
程序员茄子在线接单