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,它决定了代码生成的质量和一致性:
// 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();
v0.dev 选择 shadcn/ui 而非 Ant Design、MUI 等,原因是:
- 代码即组件:shadcn/ui 的组件是直接复制到项目中的代码,LLM 可以自由修改
- Tailwind 原生:与 Tailwind CSS 完美配合,不需要额外 CSS 系统
- Radix 原语:底层基于无样式的 Radix UI,可访问性开箱即用
- token 高效:组件 API 简洁,LLM 生成时消耗更少的 token
1.3 Sandpack 实时预览
v0.dev 使用 Sandpack(CodeSandbox 开源的浏览器内打包器)实现生成代码的实时预览:
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:
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 内部工作原理
streamText:LLM → 文本 token 流 → 客户端渲染为字符串(参考 流式渲染与 SSE)streamUI:LLM → tool_call → 服务端执行 generate 返回 JSX → RSC 协议序列化 → 客户端渲染为 React 组件
streamUI 的 "流" 不是文本 token 流,而是 React 组件流——通过 RSC 协议将服务端组件树序列化并流式发送到客户端。
2.2 完整的多工具 streamUI 示例
'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
'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 定义
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 动态渲染器
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
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 主题感知的组件生成
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 响应式变体生成
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 沙箱隔离
'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 安全评估与代码清洗
// 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 提案(未来方向)
// 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 交互回调模式
'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 客户端表单组件
'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 的 实例
这意味着 streamUI 的 generate 函数返回的 JSX 中:
- Server Components 可以直接包含
- Client Components 必须通过
'use client'标记,并且 只传递可序列化的 Props - 函数 Props(如 onClick)不能直接传递——需要使用 Server Actions 或
createStreamableUI模式
7.2 Server/Client 边界处理
'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;
}
'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 从线框图到代码
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 提取
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 组件,支持自然语言查询数据并用图表展示:
'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 懒加载生成的组件
'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 生成代码的服务端缓存
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 的内部流程可以分为五个阶段:
- LLM 推理阶段:将用户消息和工具定义发送给 LLM,LLM 决定是直接回复文本还是调用工具(参考 Function Calling 的 ReAct 循环)
- 工具执行阶段:如果 LLM 返回
tool_call,streamUI调用对应工具的generate函数,这是一个 async generator 函数 - 中间状态流式传输:
generate函数通过yield返回加载态 JSX,React 将其序列化为 RSC Payload(React Flight 格式)并通过 流式响应 发送到客户端 - 最终状态替换:异步操作完成后,
generate通过return返回最终 JSX,客户端的 React 将自动替换之前的加载态 - 客户端渲染:客户端通过
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 检查 | 拦截 eval、document.cookie、fetch 等危险 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 Output | Markdown 渲染 |
|---|---|---|---|
| 实现复杂度 | 高(需 RSC + Next.js) | 中(Zod Schema + 渲染器) | 低 |
| 组件类型 | 无限制(任意 JSX) | 需预定义 Schema | 仅 Markdown 元素 |
| 交互能力 | 完全交互(表单、按钮) | 有限交互(预定义操作) | 仅链接点击 |
| 类型安全 | 编译时检查 | Zod 运行时验证 | 无 |
| 安全性 | 高(服务端渲染) | 高(白名单组件) | 高(无代码执行) |
| 流式支持 | 原生支持(RSC 流) | 需 streamObject | 需 streamText |
| 框架依赖 | Next.js App Router | 无 | 无 |
选择建议:
- 产品级 AI 应用(如 AI 助手、数据分析平台)→ Generative UI,提供最佳用户体验
- 仪表盘/数据展示(组件类型固定)→ Structured Output,安全且可预测
- 文档类/简单对话(无复杂交互需求)→ Markdown,实现最简单
- 开发工具(如 v0.dev、Bolt.new)→ 代码生成 + 沙箱预览
Q5: 如何处理用户与 AI 生成 UI 组件的交互?
答案:
Generative UI 中用户交互的核心挑战是函数不能通过 RSC 协议序列化,所以不能像普通 React 组件那样直接传 onClick 回调。
三种交互模式:
- 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>
);
- createStreamableUI:创建可更新的 UI 流,Server Action 回调中更新 UI
const uiStream = createStreamableUI(<InitialUI />);
async function handleClick() {
'use server';
uiStream.update(<UpdatedUI />); // 服务端推送 UI 更新
uiStream.done(); // 标记完成
}
- 混合模式:Server Component 负责数据,Client Component 负责本地交互状态(如表单输入、动画),通过 Server Action 提交最终结果。
Q6: v0.dev 是如何实现生成代码的实时预览的?
答案:
v0.dev 的实时预览基于 Sandpack(CodeSandbox 开源的浏览器内打包器)。核心流程:
- 代码注入:LLM 生成的 React + TypeScript 代码作为文件写入 Sandpack 的虚拟文件系统
- 浏览器内编译:Sandpack 内置了 Babel 转译器和模块打包器,完全在浏览器端运行
- 依赖解析:预定义好常用依赖(React、Tailwind CSS、shadcn/ui 等),通过 CDN 按需加载
- iframe 渲染:编译结果在独立的 iframe 中运行,与主应用隔离
- 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 的安全风险来自不可控的代码生成和动态渲染:
| 风险类型 | 具体攻击 | 防御方案 |
|---|---|---|
| XSS | AI 生成 <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)的视觉理解能力:
关键步骤:
- 输入获取:通过 Figma API 获取设计节点信息(尺寸、颜色、文本),或直接使用截图作为多模态输入
- 布局推断:LLM 分析图片,识别出 Flex/Grid 布局关系、组件层级、间距规律
- Token 提取:从设计稿中提取颜色体系、排版层级、圆角规律,映射为 Tailwind 配置
- 代码生成:基于布局结构和设计 Token 生成 React 组件代码
- 人工微调:通过自然语言指令迭代修改
目前的挑战:
- 精度有限:间距和尺寸可能有像素级偏差
- 交互缺失:截图无法传达动画、hover 效果等交互信息
- 组件识别:相似的视觉元素可能被错误合并或拆分
实际项目中建议用 Figma API 的结构化数据辅助视觉信息,提高生成精度。
相关链接
- Vercel AI SDK - Generative UI
- Vercel AI SDK - streamUI API
- Sandpack - 浏览器内打包器
- v0.dev
- shadcn/ui
- ShadowRealm 提案
- AI SDK 与框架 - Vercel AI SDK 详解
- Function Calling 与 AI Agent - 工具调用基础
- 流式渲染与 SSE - 流式传输技术
- AI 应用安全 - 安全防御