跳到主要内容

AI 生成 UI

问题

什么是 Generative UI?AI 如何动态生成前端界面?v0.dev 等工具的原理是什么?如何安全地执行 AI 生成的代码?Generative UI 与 React Server Components 是如何协作的?

答案

Generative UI(AI 生成 UI)有两层含义:一是使用 AI 开发阶段生成 UI 组件代码(如 v0.dev、Bolt.new),二是在运行时让 LLM 动态返回 React 组件而非纯文本(如 Vercel AI SDK 的 streamUI)。此外还涉及 Structured Output 驱动的 UI 渲染、AI 驱动的设计系统生成、运行时代码沙箱执行安全等话题。

核心理解

AI 生成 UI 不只是"让 AI 写代码",更重要的是建立一套从自然语言到可交互界面的端到端管线:需求理解 → 代码/结构化数据生成 → 安全执行/渲染 → 用户交互 → 状态回传。

一、开发阶段:AI 生成组件代码(v0.dev 原理)

1.1 整体架构

1.2 System Prompt 设计模式

v0.dev 等工具的核心竞争力在于精心设计的 System Prompt,它决定了代码生成的质量和一致性:

lib/v0-system-prompt.ts
// v0.dev 风格的 System Prompt 设计(简化还原)
const V0_SYSTEM_PROMPT = `
你是一个专业的 React UI 组件生成器。

## 技术栈约束
- 框架:React 18+ 函数组件,TypeScript
- 样式:Tailwind CSS(不使用 CSS 文件或 CSS-in-JS)
- 组件库:shadcn/ui(基于 Radix UI 的组件原语)
- 图标:lucide-react
- 动画:framer-motion(可选)

## 代码规范
1. 每个生成只输出一个默认导出组件
2. 所有 Props 必须有 TypeScript 类型定义
3. 使用 cn() 工具函数合并 className(来自 shadcn/ui)
4. 响应式设计:移动优先,使用 sm: md: lg: 断点
5. 可访问性:使用语义化 HTML、ARIA 属性、键盘导航
6. 暗色模式:使用 dark: 变体
7. 不要引入外部依赖(仅限上述技术栈)

## 组件库映射
当需要以下 UI 元素时,使用 shadcn/ui 组件:
- 按钮 → <Button>
- 输入框 → <Input>
- 卡片 → <Card> <CardHeader> <CardContent> <CardFooter>
- 对话框 → <Dialog> <DialogTrigger> <DialogContent>
- 下拉菜单 → <DropdownMenu>
- 表格 → <Table> <TableHeader> <TableBody> <TableRow> <TableCell>
- 标签页 → <Tabs> <TabsList> <TabsTrigger> <TabsContent>

## 输出格式
只输出组件代码,不要解释。代码必须可直接运行。
`.trim();
shadcn/ui 的关键作用

v0.dev 选择 shadcn/ui 而非 Ant Design、MUI 等,原因是:

  1. 代码即组件:shadcn/ui 的组件是直接复制到项目中的代码,LLM 可以自由修改
  2. Tailwind 原生:与 Tailwind CSS 完美配合,不需要额外 CSS 系统
  3. Radix 原语:底层基于无样式的 Radix UI,可访问性开箱即用
  4. token 高效:组件 API 简洁,LLM 生成时消耗更少的 token

1.3 Sandpack 实时预览

v0.dev 使用 Sandpack(CodeSandbox 开源的浏览器内打包器)实现生成代码的实时预览:

components/CodePreview.tsx
import {
SandpackProvider,
SandpackPreview,
SandpackCodeEditor,
} from '@codesandbox/sandpack-react';

interface CodePreviewProps {
generatedCode: string;
dependencies?: Record<string, string>; // 额外依赖
}

export function CodePreview({ generatedCode, dependencies }: CodePreviewProps) {
return (
<SandpackProvider
template="react-ts"
files={{
// 将 AI 生成的代码注入为入口文件
'/App.tsx': {
code: generatedCode,
active: true,
},
// shadcn/ui 的 cn 工具函数
'/lib/utils.ts': {
code: `
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}`,
},
}}
customSetup={{
dependencies: {
'react': '^18.2.0',
'react-dom': '^18.2.0',
'tailwindcss': '^3.4.0',
'clsx': '^2.0.0',
'tailwind-merge': '^2.0.0',
'lucide-react': '^0.300.0',
...dependencies,
},
}}
options={{
externalResources: [
'https://cdn.tailwindcss.com', // Tailwind CDN 用于即时编译
],
}}
>
<div className="grid grid-cols-2 h-[600px]">
{/* 左侧:代码编辑器(用户可手动修改) */}
<SandpackCodeEditor showLineNumbers showTabs />
{/* 右侧:实时预览 */}
<SandpackPreview showRefreshButton showOpenInCodeSandbox={false} />
</div>
</SandpackProvider>
);
}

1.4 迭代修改与上下文累积

v0.dev 的多轮迭代核心在于将之前生成的代码作为上下文回传给 LLM:

lib/iterative-generation.ts
interface GenerationContext {
systemPrompt: string;
currentCode: string; // 当前版本的完整代码
modificationHistory: Array<{
instruction: string; // 用户修改指令
codeSnapshot: string; // 修改前的代码快照
}>;
}

async function iterativeGenerate(
ctx: GenerationContext,
userInstruction: string
): Promise<string> {
const messages = [
{ role: 'system' as const, content: ctx.systemPrompt },
// 将当前代码作为上下文
{
role: 'user' as const,
content: `当前代码如下:\n\`\`\`tsx\n${ctx.currentCode}\n\`\`\``,
},
// 最近 5 轮修改历史(避免 token 溢出)
...ctx.modificationHistory.slice(-5).flatMap(h => [
{ role: 'user' as const, content: `修改指令:${h.instruction}` },
{
role: 'assistant' as const,
content: `\`\`\`tsx\n${h.codeSnapshot}\n\`\`\``,
},
]),
// 本次修改指令
{
role: 'user' as const,
content: `请根据以下指令修改代码:${userInstruction}\n只输出完整的修改后代码。`,
},
];

const response = await streamText({ model: openai('gpt-4o'), messages });
return extractCodeFromResponse(response);
}

二、运行时:Generative UI(streamUI 动态组件流)

Vercel AI SDK 的 streamUI 是 Generative UI 的核心实现,它结合了 Function Calling 和 React Server Components,让 LLM 工具调用的返回值从数据变为 JSX 组件。

2.1 streamUI 内部工作原理

streamUI 与 streamText 的本质区别
  • streamText:LLM → 文本 token 流 → 客户端渲染为字符串(参考 流式渲染与 SSE
  • streamUI:LLM → tool_call → 服务端执行 generate 返回 JSX → RSC 协议序列化 → 客户端渲染为 React 组件

streamUI 的 "流" 不是文本 token 流,而是 React 组件流——通过 RSC 协议将服务端组件树序列化并流式发送到客户端。

2.2 完整的多工具 streamUI 示例

app/actions/chat.tsx
'use server';

import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

// ===== UI 组件定义(服务端组件,不需要 'use client')=====

function WeatherCard({ city, temp, condition, humidity, wind }: {
city: string;
temp: number;
condition: string;
humidity: number;
wind: string;
}) {
return (
<div className="p-6 rounded-xl bg-gradient-to-br from-blue-50 to-sky-100 border shadow-sm">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900">{city}</h3>
<p className="text-4xl font-bold text-blue-600">{temp}°C</p>
<p className="text-gray-600 mt-1">{condition}</p>
</div>
<div className="text-right text-sm text-gray-500 space-y-1">
<p>湿度:{humidity}%</p>
<p>风速:{wind}</p>
</div>
</div>
</div>
);
}

function StockCard({ symbol, name, price, change, volume }: {
symbol: string;
name: string;
price: number;
change: number;
volume: string;
}) {
const isUp = change > 0;
return (
<div className="p-6 rounded-xl border shadow-sm bg-white">
<div className="flex justify-between items-start">
<div>
<span className="text-xs font-mono bg-gray-100 px-2 py-1 rounded">{symbol}</span>
<h3 className="font-semibold mt-2">{name}</h3>
</div>
<div className="text-right">
<p className="text-2xl font-bold">${price.toFixed(2)}</p>
<p className={`text-sm font-medium ${isUp ? 'text-green-600' : 'text-red-600'}`}>
{isUp ? '+' : ''}{change.toFixed(2)}%
</p>
</div>
</div>
<p className="text-xs text-gray-400 mt-3">成交量:{volume}</p>
</div>
);
}

function DataChart({ title, chartType, data }: {
title: string;
chartType: 'bar' | 'line' | 'pie';
data: Array<{ label: string; value: number }>;
}) {
// 简化版:真实项目中使用 recharts 或 d3
const maxValue = Math.max(...data.map(d => d.value));
return (
<div className="p-6 rounded-xl border shadow-sm bg-white">
<h3 className="font-semibold mb-4">{title}</h3>
<div className="space-y-2">
{data.map((item, i) => (
<div key={i} className="flex items-center gap-3">
<span className="w-20 text-sm text-gray-600 truncate">{item.label}</span>
<div className="flex-1 h-6 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${(item.value / maxValue) * 100}%` }}
/>
</div>
<span className="text-sm font-mono w-16 text-right">{item.value}</span>
</div>
))}
</div>
</div>
);
}

function ErrorCard({ message }: { message: string }) {
return (
<div className="p-4 rounded-lg bg-red-50 border border-red-200 text-red-700">
<p className="font-medium">出错了</p>
<p className="text-sm mt-1">{message}</p>
</div>
);
}

// ===== streamUI 主逻辑 =====

export async function chatAction(userMessage: string) {
const result = streamUI({
model: openai('gpt-4o'),
system: '你是一个智能助手,可以查询天气、股票和数据分析。根据用户的需求选择合适的工具。',
prompt: userMessage,

// 纯文本回复(非工具调用时)
text: ({ content, done }) => {
if (done) {
return <p className="whitespace-pre-wrap leading-relaxed">{content}</p>;
}
// 流式文本输出中,显示打字指示器
return (
<p className="whitespace-pre-wrap leading-relaxed">
{content}<span className="animate-pulse">|</span>
</p>
);
},

tools: {
// ---- 工具 1:天气查询 ----
getWeather: {
description: '获取指定城市的天气信息',
parameters: z.object({
city: z.string().describe('城市名称'),
}),
generate: async function* ({ city }) {
// yield 返回加载状态(立即发送给客户端)
yield (
<div className="p-4 rounded-lg bg-blue-50 animate-pulse">
<div className="h-4 bg-blue-200 rounded w-1/3 mb-2" />
<div className="h-8 bg-blue-200 rounded w-1/4" />
<p className="text-sm text-blue-400 mt-2">正在查询 {city} 的天气...</p>
</div>
);

try {
const data = await fetchWeather(city);
// return 返回最终 UI(替换加载状态)
return (
<WeatherCard
city={city}
temp={data.temp}
condition={data.condition}
humidity={data.humidity}
wind={data.wind}
/>
);
} catch (error) {
return <ErrorCard message={`无法获取 ${city} 的天气信息`} />;
}
},
},

// ---- 工具 2:股票查询 ----
getStock: {
description: '获取股票实时信息',
parameters: z.object({
symbol: z.string().describe('股票代码,如 AAPL'),
}),
generate: async function* ({ symbol }) {
yield (
<div className="p-4 rounded-lg bg-gray-50 animate-pulse">
<p className="text-sm text-gray-400">正在查询 {symbol} 的行情...</p>
</div>
);

try {
const data = await fetchStock(symbol);
return (
<StockCard
symbol={symbol}
name={data.name}
price={data.price}
change={data.change}
volume={data.volume}
/>
);
} catch (error) {
return <ErrorCard message={`无法获取 ${symbol} 的股票信息`} />;
}
},
},

// ---- 工具 3:数据分析(带图表)----
analyzeData: {
description: '分析数据并生成可视化图表',
parameters: z.object({
query: z.string().describe('数据分析需求'),
chartType: z.enum(['bar', 'line', 'pie']).describe('图表类型'),
}),
generate: async function* ({ query, chartType }) {
yield (
<div className="p-4 rounded-lg bg-purple-50 animate-pulse">
<p className="text-sm text-purple-400">正在分析数据并生成图表...</p>
</div>
);

const data = await queryDatabase(query);
return <DataChart title={query} chartType={chartType} data={data} />;
},
},
},
});

return result.value;
}

2.3 客户端消费 Generative UI

components/GenerativeChat.tsx
'use client';

import { useState } from 'react';
import { useActions, useUIState } from 'ai/rsc';
import type { AI } from '@/app/actions/chat';

interface Message {
id: string;
role: 'user' | 'assistant';
display: React.ReactNode; // 注意:不是 string,而是 ReactNode
}

export function GenerativeChat() {
const [messages, setMessages] = useUIState<typeof AI>();
const { chatAction } = useActions<typeof AI>();
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;

const userMessage = input;
setInput('');
setIsLoading(true);

// 添加用户消息
setMessages(prev => [
...prev,
{
id: crypto.randomUUID(),
role: 'user',
display: <p>{userMessage}</p>,
},
]);

// chatAction 返回的是 React 组件(不是字符串)
const response = await chatAction(userMessage);

setMessages(prev => [
...prev,
{
id: crypto.randomUUID(),
role: 'assistant',
display: response, // 直接渲染 JSX
},
]);

setIsLoading(false);
};

return (
<div className="flex flex-col h-screen max-w-2xl mx-auto">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(msg => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-[80%] ${
msg.role === 'user'
? 'bg-blue-500 text-white rounded-2xl px-4 py-2'
: 'w-full'
}`}>
{/* 直接渲染 React 组件 */}
{msg.display}
</div>
</div>
))}
</div>

<form onSubmit={handleSubmit} className="p-4 border-t flex gap-2">
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="输入消息..."
className="flex-1 px-4 py-2 border rounded-lg"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading}
className="px-6 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
>
发送
</button>
</form>
</div>
);
}

三、Structured Output 生成 UI

另一种模式是 LLM 返回结构化数据(JSON),前端根据数据类型渲染对应的预定义组件。这种方式不依赖 RSC,适用于任何框架。

3.1 增强的 UI Schema 定义

lib/ui-schema.ts
import { z } from 'zod';

// ===== 基础组件 Schema =====

const CardSchema = z.object({
type: z.literal('card'),
title: z.string(),
description: z.string(),
image: z.string().optional(),
actions: z.array(z.object({
label: z.string(),
url: z.string(),
})).optional(),
});

const TableSchema = z.object({
type: z.literal('table'),
title: z.string().optional(),
headers: z.array(z.string()),
rows: z.array(z.array(z.string())),
sortable: z.boolean().optional(),
});

const ChartSchema = z.object({
type: z.literal('chart'),
title: z.string(),
chartType: z.enum(['bar', 'line', 'pie', 'area']),
data: z.array(z.object({
label: z.string(),
value: z.number(),
})),
});

const FormSchema = z.object({
type: z.literal('form'),
title: z.string().optional(),
fields: z.array(z.object({
name: z.string(),
label: z.string(),
type: z.enum(['text', 'email', 'number', 'select', 'textarea', 'date']),
options: z.array(z.string()).optional(),
required: z.boolean().optional(),
placeholder: z.string().optional(),
})),
submitLabel: z.string().default('提交'),
submitAction: z.string(), // 后端 Action 标识
});

// ===== 新增组件类型 =====

const CarouselSchema = z.object({
type: z.literal('carousel'),
items: z.array(z.object({
image: z.string(),
title: z.string(),
description: z.string().optional(),
link: z.string().optional(),
})),
autoPlay: z.boolean().optional(),
});

const AccordionSchema = z.object({
type: z.literal('accordion'),
items: z.array(z.object({
title: z.string(),
content: z.string(),
defaultOpen: z.boolean().optional(),
})),
});

const TimelineSchema = z.object({
type: z.literal('timeline'),
items: z.array(z.object({
date: z.string(),
title: z.string(),
description: z.string(),
status: z.enum(['completed', 'current', 'pending']).optional(),
})),
});

const StatCardSchema = z.object({
type: z.literal('stat-card'),
stats: z.array(z.object({
label: z.string(),
value: z.string(),
change: z.number().optional(), // 百分比变化
icon: z.string().optional(), // lucide 图标名
})),
});

// ===== 组合 Schema =====

const UIComponentSchema = z.discriminatedUnion('type', [
CardSchema,
TableSchema,
ChartSchema,
FormSchema,
CarouselSchema,
AccordionSchema,
TimelineSchema,
StatCardSchema,
]);

// 支持递归嵌套:布局容器可以包含子组件
const LayoutSchema: z.ZodType<LayoutComponent> = z.object({
type: z.literal('layout'),
direction: z.enum(['row', 'column', 'grid']),
columns: z.number().optional(), // grid 列数
gap: z.number().optional(),
children: z.array(z.lazy(() => UIComponentSchema.or(LayoutSchema))),
});

type UIComponent = z.infer<typeof UIComponentSchema>;
interface LayoutComponent {
type: 'layout';
direction: 'row' | 'column' | 'grid';
columns?: number;
gap?: number;
children: (UIComponent | LayoutComponent)[];
}
type RenderableComponent = UIComponent | LayoutComponent;

export { UIComponentSchema, LayoutSchema };
export type { UIComponent, LayoutComponent, RenderableComponent };

3.2 动态渲染器

components/DynamicRenderer.tsx
import type { RenderableComponent, UIComponent, LayoutComponent } from '@/lib/ui-schema';

// 组件注册表模式
const COMPONENT_REGISTRY: Record<string, React.FC<any>> = {
card: CardComponent,
table: TableComponent,
chart: ChartComponent,
form: FormComponent,
carousel: CarouselComponent,
accordion: AccordionComponent,
timeline: TimelineComponent,
'stat-card': StatCardComponent,
};

// 递归渲染器:支持布局嵌套
function DynamicRenderer({ component }: { component: RenderableComponent }) {
// 布局容器:递归渲染子组件
if (component.type === 'layout') {
return <LayoutRenderer layout={component as LayoutComponent} />;
}

// 叶子组件:从注册表查找并渲染
const Component = COMPONENT_REGISTRY[component.type];
if (!Component) {
return <div className="text-red-500">未知组件类型:{component.type}</div>;
}
return <Component {...component} />;
}

function LayoutRenderer({ layout }: { layout: LayoutComponent }) {
const styles: Record<string, string> = {
row: 'flex flex-row gap-4',
column: 'flex flex-col gap-4',
grid: `grid gap-4 grid-cols-${layout.columns ?? 2}`,
};

return (
<div className={styles[layout.direction]}>
{layout.children.map((child, i) => (
<DynamicRenderer key={i} component={child} />
))}
</div>
);
}

// ===== 各组件实现 =====

function TimelineComponent({ items }: {
type: 'timeline';
items: Array<{ date: string; title: string; description: string; status?: string }>;
}) {
const statusColors = {
completed: 'bg-green-500',
current: 'bg-blue-500 animate-pulse',
pending: 'bg-gray-300',
};

return (
<div className="space-y-4 pl-6 border-l-2 border-gray-200">
{items.map((item, i) => (
<div key={i} className="relative">
<div className={`absolute -left-[25px] w-3 h-3 rounded-full ${
statusColors[item.status as keyof typeof statusColors] ?? 'bg-gray-400'
}`} />
<p className="text-xs text-gray-400">{item.date}</p>
<p className="font-medium">{item.title}</p>
<p className="text-sm text-gray-600">{item.description}</p>
</div>
))}
</div>
);
}

function StatCardComponent({ stats }: {
type: 'stat-card';
stats: Array<{ label: string; value: string; change?: number }>;
}) {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, i) => (
<div key={i} className="p-4 bg-white rounded-lg border text-center">
<p className="text-sm text-gray-500">{stat.label}</p>
<p className="text-2xl font-bold mt-1">{stat.value}</p>
{stat.change !== undefined && (
<p className={`text-sm mt-1 ${stat.change >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{stat.change >= 0 ? '+' : ''}{stat.change}%
</p>
)}
</div>
))}
</div>
);
}

3.3 使用 Structured Output 生成 UI

lib/generate-ui.ts
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { UIComponentSchema, LayoutSchema } from './ui-schema';

export async function generateUI(userMessage: string) {
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: LayoutSchema, // LLM 必须返回符合此 Schema 的 JSON
system: `你是一个 UI 生成器。根据用户需求,返回合适的 UI 组件结构。
可用组件类型:card、table、chart、form、carousel、accordion、timeline、stat-card。
可以用 layout 容器嵌套组件,支持 row/column/grid 布局。`,
prompt: userMessage,
});

return object; // 类型安全的 LayoutComponent
}

// 使用示例
const ui = await generateUI('展示本月销售数据仪表盘,包含关键指标、趋势图和明细表');
// LLM 返回:
// {
// type: 'layout', direction: 'column', children: [
// { type: 'stat-card', stats: [...] },
// { type: 'layout', direction: 'row', children: [
// { type: 'chart', chartType: 'line', ... },
// { type: 'chart', chartType: 'pie', ... },
// ]},
// { type: 'table', headers: [...], rows: [...] },
// ]
// }

四、AI 组件库与设计系统生成

AI 不仅能生成单个组件,还能生成整套设计系统——包括色彩体系、排版系统、组件变体等。

4.1 主题感知的组件生成

lib/design-system-generator.ts
import { generateObject } from 'ai';
import { z } from 'zod';

// 设计 Token Schema
const DesignTokenSchema = z.object({
colors: z.object({
primary: z.string().describe('主色调,如 #3B82F6'),
secondary: z.string(),
accent: z.string(),
background: z.string(),
foreground: z.string(),
muted: z.string(),
border: z.string(),
}),
typography: z.object({
fontFamily: z.string(),
fontSize: z.object({
xs: z.string(),
sm: z.string(),
base: z.string(),
lg: z.string(),
xl: z.string(),
}),
}),
spacing: z.object({
unit: z.number().describe('基础间距单位(px)'),
}),
borderRadius: z.object({
sm: z.string(),
md: z.string(),
lg: z.string(),
full: z.string(),
}),
});

// 组件变体 Schema
const ComponentVariantsSchema = z.object({
button: z.object({
variants: z.array(z.object({
name: z.string(), // primary, secondary, outline, ghost, destructive
className: z.string(), // Tailwind 类名
})),
sizes: z.array(z.object({
name: z.string(), // sm, md, lg
className: z.string(),
})),
}),
input: z.object({
variants: z.array(z.object({
name: z.string(),
className: z.string(),
})),
}),
card: z.object({
variants: z.array(z.object({
name: z.string(),
className: z.string(),
})),
}),
});

export async function generateDesignSystem(brandDescription: string) {
// 第一步:生成设计 Token
const { object: tokens } = await generateObject({
model: openai('gpt-4o'),
schema: DesignTokenSchema,
prompt: `根据以下品牌描述生成设计 Token:${brandDescription}`,
});

// 第二步:基于 Token 生成组件变体
const { object: variants } = await generateObject({
model: openai('gpt-4o'),
schema: ComponentVariantsSchema,
prompt: `基于以下设计 Token 生成 Tailwind CSS 组件变体:
${JSON.stringify(tokens, null, 2)}
确保所有变体在视觉上协调一致。`,
});

return { tokens, variants };
}

4.2 响应式变体生成

lib/responsive-generator.ts
import { streamText } from 'ai';

// 生成具有完整响应式变体的组件
async function generateResponsiveComponent(
componentDescription: string
): Promise<string> {
const { text } = await generateText({
model: openai('gpt-4o'),
system: `你是一个 React 组件生成器。生成的组件必须:
1. 完整的 TypeScript 类型定义
2. 响应式设计(支持 mobile/tablet/desktop 三个断点)
3. 使用 Tailwind CSS 响应式前缀:默认(mobile), sm:(tablet), lg:(desktop)
4. 暗色模式支持(dark: 前缀)
5. 每个组件导出 Props 接口`,
prompt: `生成以下组件的完整代码,包含所有响应式变体:
${componentDescription}

示例:移动端单列堆叠 → 平板双列 → 桌面三列,带合理的间距和字号调整。`,
});

return text;
}

// 示例:生成响应式定价卡片
const code = await generateResponsiveComponent(
'定价方案卡片,3个套餐(基础/专业/企业),包含价格、功能列表、CTA按钮,专业套餐高亮推荐'
);

五、运行时代码执行安全

当 AI 生成的代码需要在浏览器中实际执行时(如 v0.dev 的预览、代码生成工具的结果展示),安全是首要考量。

核心风险

AI 生成的代码可能包含:

  • <script> 注入和 XSS 攻击向量
  • eval() / Function() 执行恶意逻辑
  • fetch() 将用户数据外泄到第三方服务器
  • document.cookie / localStorage 窃取敏感信息
  • 无限循环或大量内存分配导致 DoS

参考 AI 应用安全 了解更多 AI 安全威胁。

5.1 iframe 沙箱隔离

components/SafeCodePreview.tsx
'use client';

import { useRef, useEffect, useMemo } from 'react';

interface SafeCodePreviewProps {
code: string; // AI 生成的代码
dependencies?: string; // 外部依赖的 importmap
}

export function SafeCodePreview({ code, dependencies }: SafeCodePreviewProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);

// 构建沙箱 HTML
const sandboxHtml = useMemo(() => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<!-- CSP 头限制沙箱能力 -->
<meta http-equiv="Content-Security-Policy"
content="
default-src 'none';
script-src 'unsafe-inline' https://esm.sh https://cdn.tailwindcss.com;
style-src 'unsafe-inline' https://cdn.tailwindcss.com;
img-src https: data:;
font-src https:;
"
/>
<script src="https://cdn.tailwindcss.com"><\/script>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client"
${dependencies ? ',' + dependencies : ''}
}
}
<\/script>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';

// 执行超时保护
const TIMEOUT = 5000;
const timer = setTimeout(() => {
document.getElementById('root').innerHTML =
'<p style="color:red">执行超时(${TIMEOUT}ms)</p>';
}, TIMEOUT);

try {
${code}
// 假设代码导出了默认组件
const root = createRoot(document.getElementById('root'));
root.render(React.createElement(App));
clearTimeout(timer);
} catch (error) {
clearTimeout(timer);
document.getElementById('root').innerHTML =
'<pre style="color:red">' + error.message + '<\/pre>';
}
<\/script>
</body>
</html>`;
}, [code, dependencies]);

return (
<iframe
ref={iframeRef}
sandbox="allow-scripts" // 只允许脚本,禁止其他所有能力
// 不包含:allow-same-origin(阻止访问父页面)
// allow-forms(阻止表单提交)
// allow-popups(阻止弹窗)
// allow-top-navigation(阻止页面跳转)
srcDoc={sandboxHtml}
className="w-full h-[500px] border rounded-lg"
title="代码预览"
/>
);
}

5.2 安全评估与代码清洗

lib/code-sanitizer.ts
// AI 生成代码的安全检查
interface SecurityCheckResult {
safe: boolean;
risks: Array<{
type: 'critical' | 'warning';
pattern: string;
description: string;
line: number;
}>;
}

export function checkCodeSecurity(code: string): SecurityCheckResult {
const risks: SecurityCheckResult['risks'] = [];
const lines = code.split('\n');

const CRITICAL_PATTERNS = [
{ regex: /eval\s*\(/, desc: 'eval() 可执行任意代码' },
{ regex: /new\s+Function\s*\(/, desc: 'Function 构造器可执行任意代码' },
{ regex: /document\.cookie/, desc: '可能窃取 Cookie' },
{ regex: /localStorage|sessionStorage/, desc: '可能访问存储数据' },
{ regex: /window\.open/, desc: '可能打开恶意页面' },
{ regex: /fetch\s*\(|XMLHttpRequest|axios/, desc: '可能发送网络请求泄露数据' },
{ regex: /innerHTML\s*=/, desc: 'innerHTML 赋值可能导致 XSS' },
{ regex: /document\.write/, desc: 'document.write 可能注入内容' },
{ regex: /\.postMessage\s*\(/, desc: '可能与父窗口通信' },
{ regex: /crypto|SubtleCrypto/, desc: '可能进行加密操作' },
];

const WARNING_PATTERNS = [
{ regex: /setInterval|setTimeout/, desc: '定时器可能用于持久化攻击' },
{ regex: /while\s*\(\s*true\s*\)/, desc: '无限循环可能导致 DoS' },
{ regex: /import\s+.*from\s+['"]http/, desc: '远程模块导入可能不安全' },
{ regex: /navigator\.(geolocation|clipboard|mediaDevices)/, desc: '访问敏感浏览器 API' },
];

lines.forEach((line, index) => {
CRITICAL_PATTERNS.forEach(({ regex, desc }) => {
if (regex.test(line)) {
risks.push({ type: 'critical', pattern: regex.source, description: desc, line: index + 1 });
}
});
WARNING_PATTERNS.forEach(({ regex, desc }) => {
if (regex.test(line)) {
risks.push({ type: 'warning', pattern: regex.source, description: desc, line: index + 1 });
}
});
});

return {
safe: risks.filter(r => r.type === 'critical').length === 0,
risks,
};
}

// 使用示例
const result = checkCodeSecurity(aiGeneratedCode);
if (!result.safe) {
console.error('AI 生成的代码包含安全风险:', result.risks);
// 拒绝执行或要求用户确认
}

5.3 ShadowRealm 提案(未来方向)

lib/shadow-realm-sandbox.ts
// ShadowRealm 是 TC39 Stage 3 提案,提供轻量级的 JS 沙箱
// 比 iframe 更高效,比 eval 更安全

// ⚠️ 注意:截至 2026 年,浏览器支持仍有限
// 目前可用 Polyfill: https://github.com/nicolo-ribaudo/tc39-proposal-shadowrealm

// 未来用法示例
async function executeSandboxed(code: string): Promise<unknown> {
const realm = new ShadowRealm();

// ShadowRealm 中的代码无法访问宿主的全局对象
// 没有 window、document、fetch、localStorage 等
const result = await realm.importValue(
`data:text/javascript,${encodeURIComponent(code)}`,
'default'
);

return result;
}

// ShadowRealm vs iframe 对比
// | 特性 | ShadowRealm | iframe sandbox |
// |-------------------|----------------|-------------------|
// | 全局对象隔离 | 完全隔离 | sandbox 属性控制 |
// | DOM 访问 | 无 DOM | 有独立 DOM |
// | 性能开销 | 低(同进程) | 高(可能跨进程) |
// | 内存共享 | 不共享 | 不共享 |
// | 浏览器支持(2026) | 有限 | 全面 |
// | 适用场景 | 纯逻辑沙箱 | 需要渲染 UI |

六、Generative UI 状态管理

Generative UI 最大的挑战之一是处理 AI 生成的 UI 组件中的用户交互——表单提交、按钮点击、列表选择等操作需要将数据回传给 AI 或后端。

6.1 交互回调模式

app/actions/interactive-chat.tsx
'use server';

import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { createStreamableUI, createStreamableValue } from 'ai/rsc';

// 核心思路:在服务端组件中创建 Server Actions 作为回调
// 用户交互触发 Server Action → 更新 UI 或继续 AI 对话

export async function interactiveChatAction(userMessage: string) {
const result = streamUI({
model: openai('gpt-4o'),
prompt: userMessage,
tools: {
// 工具返回带交互的表单组件
collectFeedback: {
description: '收集用户反馈信息',
parameters: z.object({
topic: z.string(),
options: z.array(z.string()),
}),
generate: async function* ({ topic, options }) {
yield <p className="animate-pulse text-gray-400">正在生成反馈表单...</p>;

// 创建一个可流式更新的 UI 容器
const uiStream = createStreamableUI(
<FeedbackForm
topic={topic}
options={options}
onSubmit={handleFeedbackSubmit}
/>
);

// 回调函数:处理用户提交
async function handleFeedbackSubmit(data: {
rating: number;
selected: string;
comment: string;
}) {
'use server';

// 更新 UI 为感谢状态
uiStream.update(
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="font-medium text-green-700">感谢您的反馈!</p>
<p className="text-sm text-green-600 mt-1">
您选择了「{data.selected}」,评分 {data.rating}/5
</p>
</div>
);

// 将反馈存入数据库
await saveFeedback(data);
uiStream.done();
}

return uiStream.value;
},
},

// 工具返回带操作按钮的确认卡片
confirmAction: {
description: '需要用户确认的操作',
parameters: z.object({
action: z.string(),
description: z.string(),
consequences: z.array(z.string()),
}),
generate: async function* ({ action, description, consequences }) {
yield <p className="animate-pulse">准备确认对话框...</p>;

const uiStream = createStreamableUI(null);

async function handleConfirm(confirmed: boolean) {
'use server';

if (confirmed) {
uiStream.update(
<div className="p-4 bg-blue-50 border rounded-lg">
<p className="text-blue-700">正在执行:{action}...</p>
</div>
);
await executeAction(action);
uiStream.update(
<div className="p-4 bg-green-50 border rounded-lg">
<p className="text-green-700">操作已完成!</p>
</div>
);
} else {
uiStream.update(
<div className="p-4 bg-gray-50 border rounded-lg">
<p className="text-gray-500">操作已取消。</p>
</div>
);
}
uiStream.done();
}

uiStream.update(
<ConfirmCard
action={action}
description={description}
consequences={consequences}
onConfirm={() => handleConfirm(true)}
onCancel={() => handleConfirm(false)}
/>
);

return uiStream.value;
},
},
},
});

return result.value;
}

6.2 客户端表单组件

components/FeedbackForm.tsx
'use client';

import { useState } from 'react';

interface FeedbackFormProps {
topic: string;
options: string[];
onSubmit: (data: {
rating: number;
selected: string;
comment: string;
}) => Promise<void>;
}

export function FeedbackForm({ topic, options, onSubmit }: FeedbackFormProps) {
const [rating, setRating] = useState(0);
const [selected, setSelected] = useState('');
const [comment, setComment] = useState('');
const [submitting, setSubmitting] = useState(false);

const handleSubmit = async () => {
setSubmitting(true);
await onSubmit({ rating, selected, comment }); // 调用 Server Action
};

return (
<div className="p-6 border rounded-xl bg-white shadow-sm space-y-4">
<h3 className="font-semibold text-lg">{topic}</h3>

{/* 星级评分 */}
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map(star => (
<button
key={star}
onClick={() => setRating(star)}
className={`text-2xl transition-colors ${
star <= rating ? 'text-yellow-400' : 'text-gray-300'
}`}
>

</button>
))}
</div>

{/* 选项列表 */}
<div className="flex flex-wrap gap-2">
{options.map(option => (
<button
key={option}
onClick={() => setSelected(option)}
className={`px-3 py-1 rounded-full text-sm border transition-colors ${
selected === option
? 'bg-blue-500 text-white border-blue-500'
: 'bg-white text-gray-700 hover:bg-gray-50'
}`}
>
{option}
</button>
))}
</div>

{/* 文本评论 */}
<textarea
value={comment}
onChange={e => setComment(e.target.value)}
placeholder="补充说明(可选)"
className="w-full p-3 border rounded-lg text-sm resize-none"
rows={3}
/>

<button
onClick={handleSubmit}
disabled={!rating || !selected || submitting}
className="w-full py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
>
{submitting ? '提交中...' : '提交反馈'}
</button>
</div>
);
}

七、streamUI 与 React Server Components 集成

streamUI 的核心能力依赖于 React Server Components(RSC)协议。理解两者的集成方式是深入掌握 Generative UI 的关键。

7.1 RSC 协议与 streamUI 的数据流

序列化边界

RSC 协议能序列化的数据类型有限:

  • 可序列化:原始类型、纯对象、数组、Date、Map、Set、Server Components
  • 不可序列化:函数、类实例、Symbol、DOM 节点、Client Components 的 实例

这意味着 streamUIgenerate 函数返回的 JSX 中:

  • Server Components 可以直接包含
  • Client Components 必须通过 'use client' 标记,并且 只传递可序列化的 Props
  • 函数 Props(如 onClick)不能直接传递——需要使用 Server Actions 或 createStreamableUI 模式

7.2 Server/Client 边界处理

app/actions/rsc-boundary.tsx
'use server';

import { streamUI } from 'ai/rsc';

// Server Component — 可以直接在 streamUI generate 中返回
function ServerPriceTable({ data }: { data: Array<{ name: string; price: number }> }) {
// 可以直接访问数据库、文件系统等
return (
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-50">
<th className="p-3 text-left border">商品</th>
<th className="p-3 text-right border">价格</th>
</tr>
</thead>
<tbody>
{data.map((item, i) => (
<tr key={i} className="hover:bg-gray-50">
<td className="p-3 border">{item.name}</td>
<td className="p-3 border text-right font-mono">
¥{item.price.toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
);
}

// 混合使用 Server Component 和 Client Component
// Client Component 用于需要交互的部分
export async function chatWithRSC(message: string) {
const result = streamUI({
model: openai('gpt-4o'),
prompt: message,
tools: {
showProducts: {
description: '展示商品列表',
parameters: z.object({ category: z.string() }),
generate: async function* ({ category }) {
yield <p className="animate-pulse">加载商品列表...</p>;

const products = await db.query('SELECT * FROM products WHERE category = ?', [category]);

// 返回 Server Component(数据展示)+ Client Component(交互按钮)
return (
<div className="space-y-4">
{/* Server Component:直接渲染数据 */}
<ServerPriceTable data={products} />

{/* Client Component:交互逻辑 */}
<AddToCartButtons
productIds={products.map(p => p.id)} // 只传可序列化数据
// onAdd={...} ❌ 不能直接传函数 — 使用 Server Actions 替代
/>
</div>
);
},
},
},
});

return result.value;
}
components/AddToCartButtons.tsx
'use client';

import { addToCart } from '@/app/actions/cart'; // Server Action

export function AddToCartButtons({ productIds }: { productIds: string[] }) {
return (
<div className="flex gap-2">
{productIds.map(id => (
<form key={id} action={addToCart}>
<input type="hidden" name="productId" value={id} />
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600"
>
加入购物车
</button>
</form>
))}
</div>
);
}

八、AI 驱动的布局生成

AI 不仅能生成组件代码,还能从设计稿(线框图、Figma)推断布局结构,实现从设计到代码的自动化。

8.1 从线框图到代码

lib/wireframe-to-code.ts
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

// 布局结构 Schema
const LayoutTreeSchema: z.ZodType<LayoutNode> = z.object({
tag: z.enum(['div', 'header', 'nav', 'main', 'section', 'aside', 'footer', 'article']),
className: z.string().describe('Tailwind CSS 类名'),
text: z.string().optional(),
children: z.array(z.lazy(() => LayoutTreeSchema)).optional(),
component: z.string().optional().describe('如果是特定组件,标注组件名称'),
});

interface LayoutNode {
tag: string;
className: string;
text?: string;
children?: LayoutNode[];
component?: string;
}

// 从截图提取布局结构
export async function wireframeToLayout(imageUrl: string): Promise<LayoutNode> {
const { object } = await generateObject({
model: openai('gpt-4o'), // 支持视觉的多模态模型
schema: LayoutTreeSchema,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: `分析这个线框图/UI 截图,输出一个布局结构树。
要求:
1. 使用语义化 HTML 标签
2. className 使用 Tailwind CSS(包含布局、间距、颜色)
3. 识别出可复用的组件(如导航栏、卡片列表、侧边栏)
4. 保持布局的嵌套层级关系`,
},
{
type: 'image',
image: imageUrl,
},
],
},
],
});

return object;
}

// 将布局树转换为 React 代码
function layoutToReactCode(node: LayoutNode, indent = 0): string {
const pad = ' '.repeat(indent);

if (node.component) {
return `${pad}<${node.component} />`;
}

const children = node.children
?.map(child => layoutToReactCode(child, indent + 1))
.join('\n') ?? '';

const textContent = node.text ? `\n${pad} ${node.text}` : '';

return `${pad}<${node.tag} className="${node.className}">${textContent}
${children}
${pad}</${node.tag}>`;
}

8.2 设计 Token 提取

lib/design-token-extractor.ts
import { generateObject } from 'ai';
import { z } from 'zod';

const ExtractedTokensSchema = z.object({
colors: z.array(z.object({
name: z.string().describe('语义化颜色名,如 primary, text-muted'),
hex: z.string(),
usage: z.string().describe('使用场景描述'),
})),
typography: z.array(z.object({
name: z.string(),
fontSize: z.string(),
fontWeight: z.string(),
lineHeight: z.string(),
usage: z.string(),
})),
spacing: z.array(z.object({
name: z.string(),
value: z.string(),
usage: z.string(),
})),
borderRadius: z.array(z.object({
name: z.string(),
value: z.string(),
})),
});

// 从 Figma 截图或 URL 提取设计 Token
export async function extractDesignTokens(imageUrl: string) {
const { object: tokens } = await generateObject({
model: openai('gpt-4o'),
schema: ExtractedTokensSchema,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: `分析这个 UI 设计稿,提取出所有设计 Token:
1. 识别所有使用的颜色及其语义用途
2. 识别排版层级(标题、正文、注释等)
3. 识别间距规律
4. 识别圆角规律`,
},
{ type: 'image', image: imageUrl },
],
},
],
});

// 输出为 Tailwind 配置
return generateTailwindConfig(tokens);
}

function generateTailwindConfig(tokens: z.infer<typeof ExtractedTokensSchema>): string {
const colors: Record<string, string> = {};
tokens.colors.forEach(c => { colors[c.name] = c.hex; });

return `
import type { Config } from 'tailwindcss';

export default {
theme: {
extend: {
colors: ${JSON.stringify(colors, null, 6)},
},
},
} satisfies Config;`;
}

九、完整 DataChat 组件(图表渲染)

以下是一个综合示例,展示如何构建一个完整的 DataChat 组件,支持自然语言查询数据并用图表展示:

app/actions/data-chat.tsx
'use server';

import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

// 数据查询工具返回图表组件
export async function dataChatAction(query: string) {
const result = streamUI({
model: openai('gpt-4o'),
system: `你是一个数据分析助手。用户会用自然语言描述数据需求,你需要:
1. 理解用户的分析意图
2. 使用 queryData 工具查询数据
3. 使用 showChart 工具生成图表
4. 可以组合多个工具完成复杂分析`,
prompt: query,
tools: {
queryData: {
description: '执行 SQL 查询并返回数据表格',
parameters: z.object({
sql: z.string().describe('SQL 查询语句'),
title: z.string().describe('数据表标题'),
}),
generate: async function* ({ sql, title }) {
yield (
<div className="p-4 rounded-lg bg-gray-50 animate-pulse">
<p className="text-sm text-gray-400 font-mono">{sql}</p>
<p className="text-sm text-gray-500 mt-2">正在执行查询...</p>
</div>
);

// 实际执行 SQL(需要做好安全防护,只允许 SELECT)
const result = await executeReadOnlyQuery(sql);

return (
<div className="border rounded-xl overflow-hidden">
<div className="bg-gray-50 px-4 py-3 border-b">
<h4 className="font-medium">{title}</h4>
<p className="text-xs text-gray-400 font-mono mt-1">{sql}</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50">
{result.columns.map((col: string) => (
<th key={col} className="px-4 py-2 text-left font-medium border-b">
{col}
</th>
))}
</tr>
</thead>
<tbody>
{result.rows.map((row: string[], i: number) => (
<tr key={i} className="hover:bg-gray-50">
{row.map((cell, j) => (
<td key={j} className="px-4 py-2 border-b">{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="px-4 py-2 bg-gray-50 text-xs text-gray-400">
{result.rows.length} 条结果
</div>
</div>
);
},
},

showChart: {
description: '生成数据可视化图表',
parameters: z.object({
title: z.string(),
chartType: z.enum(['bar', 'line', 'pie', 'area']),
xAxis: z.string().describe('X 轴标签'),
yAxis: z.string().describe('Y 轴标签'),
data: z.array(z.object({
label: z.string(),
value: z.number(),
color: z.string().optional(),
})),
}),
generate: async function* ({ title, chartType, xAxis, yAxis, data }) {
yield <p className="animate-pulse text-gray-400">正在生成图表...</p>;

// 使用简化的 SVG 柱状图(实际项目用 recharts)
const maxValue = Math.max(...data.map(d => d.value));
const barWidth = Math.floor(400 / data.length) - 8;

return (
<div className="p-6 border rounded-xl bg-white">
<h4 className="font-semibold mb-4">{title}</h4>
{chartType === 'bar' ? (
<div>
<svg viewBox="0 0 440 240" className="w-full">
{/* Y 轴 */}
<line x1="40" y1="10" x2="40" y2="200" stroke="#e5e7eb" />
{/* X 轴 */}
<line x1="40" y1="200" x2="430" y2="200" stroke="#e5e7eb" />
{/* 柱子 */}
{data.map((d, i) => {
const height = (d.value / maxValue) * 180;
const x = 50 + i * (barWidth + 8);
return (
<g key={i}>
<rect
x={x}
y={200 - height}
width={barWidth}
height={height}
fill={d.color ?? '#3B82F6'}
rx="4"
/>
<text
x={x + barWidth / 2}
y="218"
textAnchor="middle"
className="text-[10px] fill-gray-500"
>
{d.label}
</text>
<text
x={x + barWidth / 2}
y={195 - height}
textAnchor="middle"
className="text-[10px] fill-gray-700 font-medium"
>
{d.value}
</text>
</g>
);
})}
{/* 轴标签 */}
<text x="235" y="238" textAnchor="middle" className="text-[11px] fill-gray-500">
{xAxis}
</text>
</svg>
<p className="text-xs text-gray-400 text-center mt-1">{yAxis}</p>
</div>
) : (
// 其他图表类型:真实项目中使用 recharts
<div className="space-y-2">
{data.map((d, i) => (
<div key={i} className="flex items-center gap-3">
<span className="w-24 text-sm truncate">{d.label}</span>
<div className="flex-1 h-5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{
width: `${(d.value / maxValue) * 100}%`,
backgroundColor: d.color ?? '#3B82F6',
}}
/>
</div>
<span className="text-sm font-mono w-16 text-right">{d.value}</span>
</div>
))}
</div>
)}
</div>
);
},
},
},
});

return result.value;
}

十、性能优化

AI 生成的 UI 可能包含大量组件和代码,需要针对性的性能优化。

10.1 懒加载生成的组件

components/LazyGeneratedUI.tsx
'use client';

import { lazy, Suspense, useMemo } from 'react';

interface LazyGeneratedUIProps {
code: string; // AI 生成的组件代码
cacheKey?: string; // 缓存标识
}

// 组件代码缓存:避免重复编译同一段代码
const componentCache = new Map<string, React.ComponentType>();

export function LazyGeneratedUI({ code, cacheKey }: LazyGeneratedUIProps) {
const Component = useMemo(() => {
const key = cacheKey ?? hashCode(code);

if (componentCache.has(key)) {
return componentCache.get(key)!;
}

// 使用 React.lazy 包装动态编译的组件
const LazyComponent = lazy(async () => {
// 在 Web Worker 中编译代码(避免阻塞主线程)
const compiled = await compileInWorker(code);
const module = { default: compiled };
return module;
});

componentCache.set(key, LazyComponent);
return LazyComponent;
}, [code, cacheKey]);

return (
<Suspense fallback={
<div className="animate-pulse p-4 bg-gray-50 rounded-lg">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
</div>
}>
<Component />
</Suspense>
);
}

// 简单的字符串哈希
function hashCode(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0;
}
return hash.toString(36);
}

10.2 生成代码的服务端缓存

lib/generation-cache.ts
import { LRUCache } from 'lru-cache';
import { createHash } from 'crypto';

interface CacheEntry {
code: string;
generatedAt: number;
model: string;
promptHash: string;
}

// 服务端 LRU 缓存:避免重复生成相同/相似需求的代码
const generationCache = new LRUCache<string, CacheEntry>({
max: 500, // 最多缓存 500 个生成结果
ttl: 1000 * 60 * 60, // 1 小时过期
});

export async function generateWithCache(
prompt: string,
systemPrompt: string
): Promise<string> {
// 基于 prompt 内容生成缓存 key
const promptHash = createHash('sha256')
.update(prompt + systemPrompt)
.digest('hex')
.slice(0, 16);

// 检查缓存
const cached = generationCache.get(promptHash);
if (cached) {
console.log(`[Cache HIT] ${promptHash}`);
return cached.code;
}

// 缓存未命中,调用 LLM 生成
console.log(`[Cache MISS] ${promptHash}`);
const { text: code } = await generateText({
model: openai('gpt-4o'),
system: systemPrompt,
prompt,
});

// 写入缓存
generationCache.set(promptHash, {
code,
generatedAt: Date.now(),
model: 'gpt-4o',
promptHash,
});

return code;
}

10.3 性能优化策略总结

优化策略适用场景效果
服务端缓存相同/相似 prompt 重复请求避免重复调用 LLM,减少成本和延迟
React.lazy + Suspense大型生成组件按需加载,不阻塞首屏
Web Worker 编译代码沙箱模式编译不阻塞主线程
虚拟列表长会话中大量 AI 组件只渲染可视区域的组件
RSC 流式传输streamUI 模式组件逐步到达,提升感知性能
组件代码缓存客户端重复渲染避免重复编译相同代码
skeleton 占位所有异步 UI减少布局偏移(CLS)

方式对比总结

方式类型安全交互能力灵活性安全性框架依赖适用场景
streamUI (Generative UI)强(JSX 类型检查)完全交互最高高(服务端执行)RSC (Next.js)产品级 AI 应用
Structured Output强(Zod Schema)预定义交互中等高(只允许预定义组件)无依赖仪表盘、数据展示
代码生成 (v0 模式)不保证生成后完全交互最高低(需沙箱)无依赖开发工具、原型
Markdown 渲染仅链接最低无依赖文档、简单对话

常见面试问题

Q1: 什么是 Generative UI?和普通的 AI 对话有什么区别?

答案

普通 AI 对话返回的是纯文本/Markdown,前端只能做文本渲染。Generative UI 则让 LLM 返回 React 组件(或结构化 UI 数据),前端可以渲染出富交互界面。

对比示例:

  • 普通对话:"北京今天 25°C,晴天" → 渲染为文本
  • Generative UI:返回 <WeatherCard city="北京" temp={25} condition="晴" /> → 渲染为可交互的天气卡片

Generative UI 的关键技术是 Vercel AI SDK 的 streamUI,它结合了 Function Calling 和 React Server Components:LLM 通过 Function Calling 决定调用哪个工具(如 getWeather),工具的 generate 函数返回的不是数据 JSON,而是 JSX 组件。组件通过 RSC 协议流式传输到客户端,实现从"加载中"到"完整卡片"的无缝切换。

这种模式使 AI 应用从"聊天工具"进化为动态交互平台:用户可以在 AI 生成的表单中填写数据、点击按钮触发操作、在图表中交互查看详情。

Q2: Vercel AI SDK 的 streamUI 内部是如何与 RSC 协作的?

答案

streamUI 的内部流程可以分为五个阶段:

  1. LLM 推理阶段:将用户消息和工具定义发送给 LLM,LLM 决定是直接回复文本还是调用工具(参考 Function Calling 的 ReAct 循环)
  2. 工具执行阶段:如果 LLM 返回 tool_callstreamUI 调用对应工具的 generate 函数,这是一个 async generator 函数
  3. 中间状态流式传输generate 函数通过 yield 返回加载态 JSX,React 将其序列化为 RSC Payload(React Flight 格式)并通过 流式响应 发送到客户端
  4. 最终状态替换:异步操作完成后,generate 通过 return 返回最终 JSX,客户端的 React 将自动替换之前的加载态
  5. 客户端渲染:客户端通过 createFromFetch 反序列化 RSC Payload,将 Server Component 树渲染为 DOM

关键的序列化边界:generate 返回的 JSX 中可以包含 Server Components(直接渲染)和 Client Components('use client' 标记),但 不能传递函数 Props。交互回调需要通过 Server Actions 实现。

// ✅ 正确:使用 Server Action 作为回调
<form action={serverActionFn}>
<button type="submit">操作</button>
</form>

// ❌ 错误:直接传递函数(无法通过 RSC 序列化)
<button onClick={() => doSomething()}>操作</button>

Q3: 如何安全地在浏览器中执行 AI 生成的 React 代码?

答案

AI 生成的代码可能包含恶意内容(XSS 脚本、数据窃取、无限循环等),需要多层防御。参考 AI 应用安全 了解更多。

三层安全架构

层级技术防御内容
第一层:静态分析正则匹配 + AST 检查拦截 evaldocument.cookiefetch 等危险 API
第二层:运行时隔离iframe sandbox="allow-scripts"阻止访问父页面 DOM、Cookie、Storage
第三层:网络隔离CSP (Content-Security-Policy)限制可加载的脚本源、阻止外发请求

具体实现:

// 第一层:生成后立即检查
const securityResult = checkCodeSecurity(aiGeneratedCode);
if (!securityResult.safe) {
showWarning(securityResult.risks); // 展示风险,让用户决定
return;
}

// 第二层:iframe 沙箱渲染
<iframe
sandbox="allow-scripts" // 只允许脚本,不含 allow-same-origin
srcDoc={sandboxHtml} // 注入代码到隔离环境
/>

// 第三层:CSP 头限制
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'unsafe-inline' https://esm.sh;"
/>

未来 ShadowRealm(TC39 Stage 3 提案)将提供更轻量的 JS 沙箱方案,无需 iframe 即可实现全局对象隔离。

Q4: Generative UI、Structured Output、Markdown 渲染三种方式如何选择?

答案

维度Generative UI (streamUI)Structured OutputMarkdown 渲染
实现复杂度高(需 RSC + Next.js)中(Zod Schema + 渲染器)
组件类型无限制(任意 JSX)需预定义 Schema仅 Markdown 元素
交互能力完全交互(表单、按钮)有限交互(预定义操作)仅链接点击
类型安全编译时检查Zod 运行时验证
安全性高(服务端渲染)高(白名单组件)高(无代码执行)
流式支持原生支持(RSC 流)streamObjectstreamText
框架依赖Next.js App Router

选择建议

  • 产品级 AI 应用(如 AI 助手、数据分析平台)→ Generative UI,提供最佳用户体验
  • 仪表盘/数据展示(组件类型固定)→ Structured Output,安全且可预测
  • 文档类/简单对话(无复杂交互需求)→ Markdown,实现最简单
  • 开发工具(如 v0.dev、Bolt.new)→ 代码生成 + 沙箱预览

Q5: 如何处理用户与 AI 生成 UI 组件的交互?

答案

Generative UI 中用户交互的核心挑战是函数不能通过 RSC 协议序列化,所以不能像普通 React 组件那样直接传 onClick 回调。

三种交互模式

  1. Server Actions(推荐):将回调函数定义为 Server Action('use server'),通过 <form action={...}> 触发
// Server Action
async function addToCart(formData: FormData) {
'use server';
const productId = formData.get('productId') as string;
await db.cart.add(productId);
}

// 在 streamUI generate 中返回
return (
<form action={addToCart}>
<input type="hidden" name="productId" value={product.id} />
<button type="submit">加入购物车</button>
</form>
);
  1. createStreamableUI:创建可更新的 UI 流,Server Action 回调中更新 UI
const uiStream = createStreamableUI(<InitialUI />);

async function handleClick() {
'use server';
uiStream.update(<UpdatedUI />); // 服务端推送 UI 更新
uiStream.done(); // 标记完成
}
  1. 混合模式:Server Component 负责数据,Client Component 负责本地交互状态(如表单输入、动画),通过 Server Action 提交最终结果。

Q6: v0.dev 是如何实现生成代码的实时预览的?

答案

v0.dev 的实时预览基于 Sandpack(CodeSandbox 开源的浏览器内打包器)。核心流程:

  1. 代码注入:LLM 生成的 React + TypeScript 代码作为文件写入 Sandpack 的虚拟文件系统
  2. 浏览器内编译:Sandpack 内置了 Babel 转译器和模块打包器,完全在浏览器端运行
  3. 依赖解析:预定义好常用依赖(React、Tailwind CSS、shadcn/ui 等),通过 CDN 按需加载
  4. iframe 渲染:编译结果在独立的 iframe 中运行,与主应用隔离
  5. HMR 更新:代码变更时(用户手动修改或 AI 迭代),Sandpack 增量编译并热更新预览

迭代修改的关键:每次用户发出修改指令时,v0 将当前代码和修改历史作为上下文传给 LLM,LLM 输出完整的修改后代码,Sandpack 重新编译渲染。这种"全量替换"策略比"diff 补丁"更可靠,避免了 LLM 生成错误的 diff。

<SandpackProvider
template="react-ts"
files={{ '/App.tsx': { code: aiGeneratedCode, active: true } }}
customSetup={{
dependencies: { react: '^18', tailwindcss: '^3', 'lucide-react': '^0.300' },
}}
>
<SandpackPreview />
</SandpackProvider>

Q7: AI 生成 UI 的安全风险有哪些?如何防御?

答案

AI 生成 UI 的安全风险来自不可控的代码生成动态渲染

风险类型具体攻击防御方案
XSSAI 生成 <script>onerror 事件处理器iframe sandbox + CSP + DOMPurify
代码注入通过 eval() / Function() 执行恶意逻辑静态分析拦截 + 沙箱隔离
数据泄露fetch() 将用户数据发送到攻击者服务器CSP connect-src 'none' 禁止网络请求
Cookie 窃取document.cookie 读取认证信息sandbox 不含 allow-same-origin
DoS无限循环或大量内存分配执行超时 + Web Worker 隔离
Prompt 注入用户输入操纵 AI 生成恶意组件System Prompt 防御 + 输出审查

安全等级选择

  • 最安全:Structured Output(只允许预定义的组件类型和属性,不执行任何代码)
  • 较安全:streamUI(服务端渲染,代码不在客户端执行,但需防范工具滥用)
  • 需谨慎:代码生成 + 沙箱预览(必须多层隔离)

Q8: 如何实现组件预览沙箱?

答案

组件预览沙箱是 AI 生成 UI 工具的核心基础设施。实现方案从简单到复杂:

方案一:iframe + srcDoc(简单)

// 将代码注入 iframe 的 srcDoc
<iframe
sandbox="allow-scripts"
srcDoc={`<html><body><div id="root"></div><script type="module">${code}</script></body></html>`}
/>

优点:实现简单,隔离性好。缺点:不支持 TypeScript,模块系统受限。

方案二:Sandpack(推荐)

// CodeSandbox 的浏览器内打包器
<SandpackProvider template="react-ts" files={{ '/App.tsx': { code } }}>
<SandpackPreview />
</SandpackProvider>

优点:完整的 TypeScript + JSX 支持,类似 VS Code 的编辑体验,依赖管理。缺点:包体积较大。

方案三:WebContainer(最强大)

// StackBlitz 的浏览器内 Node.js 运行时
import { WebContainer } from '@webcontainer/api';
const container = await WebContainer.boot();
await container.mount({ 'index.tsx': { file: { contents: code } } });
await container.spawn('npx', ['vite']);

优点:完整的 Node.js 环境,支持 npm install,接近真实开发环境。缺点:启动慢,资源消耗大。

Q9: 如何用 Structured Output 实现可递归嵌套的 UI 组件系统?

答案

通过 Zod 的 z.lazy() 实现递归 Schema 定义,让 LLM 可以输出嵌套的布局结构:

// 叶子组件
const UIComponentSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('card'), title: z.string(), ... }),
z.object({ type: z.literal('chart'), chartType: z.enum(['bar','line']), ... }),
z.object({ type: z.literal('stat-card'), stats: z.array(...) }),
]);

// 布局容器(可嵌套)
const LayoutSchema = z.object({
type: z.literal('layout'),
direction: z.enum(['row', 'column', 'grid']),
columns: z.number().optional(),
children: z.array(z.lazy(() => UIComponentSchema.or(LayoutSchema))), // 递归
});

渲染器用递归函数处理嵌套:

function DynamicRenderer({ component }: { component: RenderableComponent }) {
if (component.type === 'layout') {
return (
<div className={layoutStyles[component.direction]}>
{component.children.map((child, i) => (
<DynamicRenderer key={i} component={child} /> // 递归渲染
))}
</div>
);
}
const Component = REGISTRY[component.type];
return Component ? <Component {...component} /> : null;
}

这样 LLM 可以输出复杂的仪表盘布局:顶部指标卡片 → 中间两列图表 → 底部数据表格,全部通过一次 generateObject 调用完成。

Q10: streamUI 中 yield 和 return 的区别是什么?为什么 generate 是 async generator?

答案

generate 使用 async generator(async function*)是为了实现多阶段 UI 流式传输

generate: async function* ({ city }) {
// yield:发送中间状态(加载中),不阻塞后续执行
yield <LoadingCard city={city} />; // ← 立即发送到客户端

const data = await fetchWeather(city); // ← 可能耗时 1-3 秒

// return:发送最终状态,替换之前 yield 的内容
return <WeatherCard data={data} />; // ← 替换 LoadingCard
}
操作行为使用场景
yield <JSX />发送中间 UI,继续执行加载状态、进度指示、多阶段展示
return <JSX />发送最终 UI,结束 generator数据加载完毕,展示最终结果

可以多次 yield 实现多阶段加载:

generate: async function* ({ query }) {
yield <p>正在理解你的问题...</p>;

const plan = await generatePlan(query);
yield <PlanPreview plan={plan} />; // 展示执行计划

const data = await executeQuery(plan);
yield <p>数据获取完成,正在生成图表...</p>;

const chart = await generateChart(data);
return <ChartView chart={chart} />; // 最终结果
}

这种模式是参考了 流式渲染 中的 TTFT 理念:尽早给用户反馈,即使最终数据还没准备好。

Q11: AI 组件库生成和传统组件库开发有什么区别?

答案

维度传统组件库AI 生成组件库
设计流程设计师出 Figma → 开发实现自然语言描述 → AI 生成 → 人工微调
一致性保证Design Token + 人工审查System Prompt 约束 + 自动化测试
变体扩展手动编写每个变体AI 根据设计 Token 批量生成
响应式手动编写断点AI 推断布局适配策略
维护方式人工迭代自然语言指令修改
适用场景长期维护的产品快速原型、MVP、落地页

AI 生成组件库的核心优势是速度(分钟级生成 vs 天级开发),但挑战在于一致性和质量——需要通过精确的 System Prompt、设计 Token 约束和人工审查来确保生成质量。

实际应用中通常是混合模式:用 AI 快速生成基础组件 → 人工审查和微调 → 沉淀为项目组件库 → AI 在此基础上继续生成更复杂的组件。

Q12: 从线框图/Figma 设计稿到代码,AI 是如何实现的?

答案

Figma-to-Code 的 AI 实现依赖多模态大语言模型(如 GPT-4o、Claude)的视觉理解能力:

关键步骤

  1. 输入获取:通过 Figma API 获取设计节点信息(尺寸、颜色、文本),或直接使用截图作为多模态输入
  2. 布局推断:LLM 分析图片,识别出 Flex/Grid 布局关系、组件层级、间距规律
  3. Token 提取:从设计稿中提取颜色体系、排版层级、圆角规律,映射为 Tailwind 配置
  4. 代码生成:基于布局结构和设计 Token 生成 React 组件代码
  5. 人工微调:通过自然语言指令迭代修改

目前的挑战:

  • 精度有限:间距和尺寸可能有像素级偏差
  • 交互缺失:截图无法传达动画、hover 效果等交互信息
  • 组件识别:相似的视觉元素可能被错误合并或拆分

实际项目中建议用 Figma API 的结构化数据辅助视觉信息,提高生成精度。

相关链接