Coze Studio 源码解读(七):前端架构与组件设计
Coze Studio 源码解读(七):前端架构与组件设计
深入理解 React 前端的设计与实现
一、前端架构概述
1.1 Monorepo 架构
Coze Studio 前端采用 Rush 管理的 Monorepo 架构,支持多应用和共享包的统一管理。
Monorepo 结构
frontend/
├── apps/ # 应用入口
│ └── coze-studio/ # 主应用
│ ├── src/
│ │ ├── pages/ # 页面
│ │ ├── components/ # 组件
│ │ ├── hooks/ # Hooks
│ │ ├── stores/ # 状态
│ │ └── utils/ # 工具
│ └── package.json
│
├── packages/ # 共享包
│ ├── ui/ # UI 组件库
│ ├── workflow/ # 工作流组件
│ ├── chat-sdk/ # Chat SDK
│ └── shared/ # 共享工具
│
├── config/ # 配置
├── infra/ # 基础设施
└── rush.json # Rush 配置
1.2 技术栈
前端技术栈
框架
├── React 18
├── TypeScript 5
└── Vite
状态管理
├── Zustand (轻量状态)
├── React Query (服务端状态)
└── Context API
UI 组件
├── 自研组件库
├── Tailwind CSS
└── Framer Motion
工具链
├── Rush (Monorepo)
├── pnpm (包管理)
└── ESLint + Prettier
二、状态管理
2.1 Zustand Store
// stores/workflowStore.ts
import { create } from 'zustand';
interface WorkflowState {
// 状态
nodes: Node[];
edges: Edge[];
selectedNode: string | null;
// 操作
addNode: (node: Node) => void;
updateNode: (id: string, data: Partial<Node>) => void;
deleteNode: (id: string) => void;
setSelectedNode: (id: string | null) => void;
// 边操作
addEdge: (edge: Edge) => void;
deleteEdge: (id: string) => void;
}
export const useWorkflowStore = create<WorkflowState>((set) => ({
nodes: [],
edges: [],
selectedNode: null,
addNode: (node) => set((state) => ({
nodes: [...state.nodes, node],
})),
updateNode: (id, data) => set((state) => ({
nodes: state.nodes.map((node) =>
node.id === id ? { ...node, ...data } : node
),
})),
deleteNode: (id) => set((state) => ({
nodes: state.nodes.filter((node) => node.id !== id),
edges: state.edges.filter(
(edge) => edge.source !== id && edge.target !== id
),
})),
setSelectedNode: (id) => set({ selectedNode: id }),
addEdge: (edge) => set((state) => ({
edges: [...state.edges, edge],
})),
deleteEdge: (id) => set((state) => ({
edges: state.edges.filter((edge) => edge.id !== id),
})),
}));
2.2 React Query
// hooks/useBots.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 获取 Agent 列表
export function useBots() {
return useQuery({
queryKey: ['bots'],
queryFn: () => api.get('/bots'),
});
}
// 获取单个 Agent
export function useBot(id: string) {
return useQuery({
queryKey: ['bots', id],
queryFn: () => api.get(`/bots/${id}`),
enabled: !!id,
});
}
// 创建 Agent
export function useCreateBot() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateBotRequest) => api.post('/bots', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bots'] });
},
});
}
// 更新 Agent
export function useUpdateBot(id: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateBotRequest) => api.put(`/bots/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bots'] });
queryClient.invalidateQueries({ queryKey: ['bots', id] });
},
});
}
三、工作流画布
3.1 FlowGram.ai 集成
// components/WorkflowCanvas.tsx
import { FlowEditor } from '@flowgram.ai';
interface WorkflowCanvasProps {
workflowId: string;
readOnly?: boolean;
}
export function WorkflowCanvas({ workflowId, readOnly }: WorkflowCanvasProps) {
const { data: workflow } = useWorkflow(workflowId);
const updateWorkflow = useUpdateWorkflow(workflowId);
const handleNodesChange = useCallback((nodes: Node[]) => {
updateWorkflow.mutate({ nodes });
}, [updateWorkflow]);
const handleEdgesChange = useCallback((edges: Edge[]) => {
updateWorkflow.mutate({ edges });
}, [updateWorkflow]);
return (
<FlowEditor
nodes={workflow?.nodes}
edges={workflow?.edges}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
readOnly={readOnly}
nodeTypes={customNodeTypes}
edgeTypes={customEdgeTypes}
/>
);
}
3.2 自定义节点
// components/nodes/LLMNode.tsx
import { memo } from 'react';
import { Handle, Position, NodeProps } from '@flowgram.ai';
interface LLMNodeData {
model: string;
systemPrompt: string;
temperature: number;
}
export const LLMNode = memo(({ data, selected }: NodeProps<LLMNodeData>) => {
return (
<div className={`node llm-node ${selected ? 'selected' : ''}`}>
<Handle type="target" position={Position.Left} />
<div className="node-header">
<span className="node-icon">🤖</span>
<span className="node-title">LLM</span>
</div>
<div className="node-content">
<div className="field">
<label>模型</label>
<span>{data.model}</span>
</div>
<div className="field">
<label>温度</label>
<span>{data.temperature}</span>
</div>
</div>
<Handle type="source" position={Position.Right} />
</div>
);
});
// 注册节点类型
const customNodeTypes = {
llm: LLMNode,
code: CodeNode,
condition: ConditionNode,
knowledge: KnowledgeNode,
};
四、组件设计
4.1 组件层次
组件层次结构
页面级组件 (Pages)
├── AgentListPage
├── AgentDetailPage
├── WorkflowEditorPage
└── KnowledgePage
功能组件 (Features)
├── AgentForm
├── WorkflowCanvas
├── ChatPanel
└── KnowledgeUploader
UI 组件 (UI)
├── Button
├── Input
├── Modal
├── Table
└── Form
4.2 组件设计原则
// 组件设计示例
// 1. Props 类型定义
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
// 2. 组件实现
export function Button({
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
children,
onClick,
}: ButtonProps) {
const classNames = cn(
'button',
`button--${variant}`,
`button--${size}`,
{
'button--loading': loading,
'button--disabled': disabled,
}
);
return (
<button
className={classNames}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? <Spinner size="sm" /> : children}
</button>
);
}
// 3. 样式
// button.module.css
.button {
@apply inline-flex items-center justify-center rounded-md font-medium;
}
.button--primary {
@apply bg-blue-500 text-white hover:bg-blue-600;
}
.button--secondary {
@apply bg-gray-100 text-gray-900 hover:bg-gray-200;
}
.button--sm { @apply px-3 py-1.5 text-sm; }
.button--md { @apply px-4 py-2 text-base; }
.button--lg { @apply px-6 py-3 text-lg; }
五、聊天界面
5.1 聊天组件
// components/Chat/ChatPanel.tsx
export function ChatPanel({ botId }: { botId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const sendMessage = useCallback(async () => {
if (!input.trim() || isStreaming) return;
const userMessage: Message = {
id: uuid(),
role: 'user',
content: input,
};
setMessages((prev) => [...prev, userMessage]);
setInput('');
setIsStreaming(true);
try {
// 流式请求
const response = await fetch(`/api/chat/stream?bot_id=${botId}`, {
method: 'POST',
body: JSON.stringify({ message: input }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let assistantMessage: Message = {
id: uuid(),
role: 'assistant',
content: '',
};
setMessages((prev) => [...prev, assistantMessage]);
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
const data = JSON.parse(chunk.replace('data: ', ''));
assistantMessage.content += data.content;
setMessages((prev) => [...prev.slice(0, -1), assistantMessage]);
}
} finally {
setIsStreaming(false);
}
}, [input, botId, isStreaming]);
return (
<div className="chat-panel">
<MessageList messages={messages} />
<ChatInput
value={input}
onChange={setInput}
onSend={sendMessage}
disabled={isStreaming}
/>
</div>
);
}
5.2 消息渲染
// components/Chat/MessageList.tsx
export function MessageList({ messages }: { messages: Message[] }) {
const listRef = useRef<HTMLDivElement>(null);
// 自动滚动到底部
useEffect(() => {
listRef.current?.scrollTo({
top: listRef.current.scrollHeight,
behavior: 'smooth',
});
}, [messages]);
return (
<div ref={listRef} className="message-list">
{messages.map((message) => (
<MessageItem key={message.id} message={message} />
))}
</div>
);
}
// components/Chat/MessageItem.tsx
export function MessageItem({ message }: { message: Message }) {
const isUser = message.role === 'user';
return (
<div className={`message-item ${isUser ? 'user' : 'assistant'}`}>
{!isUser && <Avatar icon="🤖" />}
<div className="message-content">
<MarkdownRenderer content={message.content} />
</div>
{isUser && <Avatar icon="👤" />}
</div>
);
}
六、性能优化
6.1 代码分割
// 路由级代码分割
import { lazy, Suspense } from 'react';
const AgentListPage = lazy(() => import('./pages/AgentListPage'));
const AgentDetailPage = lazy(() => import('./pages/AgentDetailPage'));
const WorkflowEditorPage = lazy(() => import('./pages/WorkflowEditorPage'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/agents" element={<AgentListPage />} />
<Route path="/agents/:id" element={<AgentDetailPage />} />
<Route path="/workflows/:id" element={<WorkflowEditorPage />} />
</Routes>
</Suspense>
);
}
6.2 虚拟列表
// 使用 react-window 实现虚拟列表
import { FixedSizeList } from 'react-window';
interface VirtualListProps {
items: any[];
renderItem: (item: any) => React.ReactNode;
itemHeight: number;
}
export function VirtualList({ items, renderItem, itemHeight }: VirtualListProps) {
const Row = ({ index, style }) => (
<div style={style}>
{renderItem(items[index])}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={itemHeight}
width="100%"
>
{Row}
</FixedSizeList>
);
}
七、本章小结
7.1 知识点回顾
本章要点
1. 前端架构
├── Monorepo 结构
└── 技术栈选择
2. 状态管理
├── Zustand Store
└── React Query
3. 工作流画布
├── FlowGram.ai 集成
└── 自定义节点
4. 组件设计
├── 组件层次
└── 设计原则
5. 聊天界面
├── 流式响应
└── 消息渲染
6. 性能优化
├── 代码分割
└── 虚拟列表
下一节预告
下一节我们将学习:部署与二次开发指南
内容包括:
-
生产部署 -
性能优化 -
自定义扩展 -
最佳实践
软件定制开发,微信联系:rustgopy
夜雨聆风