主题
前端技术栈与应用结构
本章目标:
- 讲清 DeerFlow 前端为什么选择 Next.js App Router,并理解
src/app(路由层)、src/core(业务逻辑层)、src/components(UI 层)三层划分的边界。- 掌握
(auth)、[lang]/docs、workspace、blog、mock、api六大路由分区各自的职责与守卫机制。- 理解前端如何通过
next.config.js的 rewrites 经 nginx 连到后端 Gateway,以及NEXT_PUBLIC_*环境变量与构建配置的解析路径。
TL;DR
DeerFlow 前端是一个 Next.js 16 + React 19 + TypeScript 5.8 + Tailwind 4 的有状态聊天应用。代码按 app/(路由与布局守卫)、core/(线程流、API 单例、i18n、设置等纯业务逻辑)、components/(ui/ai-elements 自动生成的原子组件 + workspace/landing 业务组件)三层划分。生产环境下后端 URL 默认留空,由 next.config.js 的 rewrites() 把 /api/* 反代到内部 Gateway(经 nginx),前端无需感知后端地址。环境变量经 src/env.js(@t3-oss/env-nextjs + Zod)做构建期校验。
Overview
DeerFlow 前端选 Next.js App Router 而非传统 SPA,核心动机有三点:
第一,布局即守卫。聊天页需要登录、首次安装页需要拦截、文档页需要多语言外壳——这些都用 App Router 的嵌套 layout.tsx 在 Server Component 中完成。workspace/layout.tsx 在渲染前调用 getServerSideUser(),根据返回的 tag 直接 redirect("/login") 或 redirect("/setup"),未鉴权用户连页面 JS 都拿不到 frontend/src/app/workspace/layout.tsx:15-29。
第二,路由分区隔离关注点。落地页(静态营销)、文档/博客(Nextra MDX)、工作区(动态聊天)在交互模型上完全不同。App Router 用路由组 (auth)、动态段 [lang]、并行嵌套把它们物理隔开,每个分区有独立的 layout 和数据获取策略。
第三,core/components 分层让业务逻辑可测试。所有线程流、流式解析、API 调用都收敛进 src/core,以 React Hook(useThreadStream、useThreads)对外暴露 frontend/src/core/threads/hooks.ts:161;组件只负责渲染。这样 tests/unit/ 能直接对 core 做单元测试,而无需挂载 React 树。
Architecture
前端代码全部位于 frontend/src/,顶层目录是稳定的三层结构外加若干辅助目录:
app/— Next.js App Router 路由与布局,每个分区有独立layout.tsx/page.tsxfrontend/src/app/layout.tsx:15-28。core/— 业务逻辑层,21 个子模块(threads/api/i18n/auth/settings/memory/skills/mcp等),每个带index.ts桶导出。components/— 四类组件:ui/(45 个 Shadcn 原子)、ai-elements/(27 个 Vercel AI SDK 元素)、workspace/(聊天业务组件)、landing/(落地页区块)。hooks/— 跨页面共享 Hook(use-mobile、use-global-shortcuts)。lib/— 工具函数,cn()(clsx + tailwind-merge)与输入法处理。content/— Nextra 文档/博客的 MDX 源,按en/zh分语言。styles/— 全局 CSS,Tailwind v4@import语法 + CSS 变量主题。
Source 列表:
| 层 | 路径 | 职责 |
|---|---|---|
| 路由层 | frontend/src/app/ | 路由、布局守卫、API mock/proxy |
| 业务层 | frontend/src/core/ | 线程流、API 单例、i18n、设置 |
| UI 层 | frontend/src/components/ | 原子组件 + 业务组件 |
| 配置 | frontend/src/env.js | 环境变量 Zod 校验 |
Components / Subsystems
src/app 路由分区
根布局 app/layout.tsx 是全局外壳:它在 Server Component 中调用 detectLocaleServer() 解析语言 Cookie,再用 ThemeProvider + I18nProvider 包裹整个应用 frontend/src/app/layout.tsx:15-28。
(auth)路由组 — 包含login与setup两页。(auth)/layout.tsx同样先查getServerSideUser():已登录用户访问登录页会被redirect("/workspace"),needs_setup状态放行到 setup 页,Gateway 不可用时渲染降级提示 frontend/src/app/(auth)/layout.tsx:16-46。login/page.tsx带validateNextParam()防开放重定向攻击 frontend/src/app/(auth)/login/page.tsx:19-40;setup/page.tsx有init_admin与change_password两种模式 frontend/src/app/(auth)/setup/page.tsx:14-31。[lang]/docs— 多语言文档,[lang]动态段取en/zh。docs/layout.tsx用 Nextra 的getPageMap()构建侧边栏,套上nextra-theme-docs的Layoutfrontend/src/app/[lang]/docs/layout.tsx:27-50。[[...mdxPath]]/page.tsx通过generateStaticParamsFor("mdxPath")静态化所有 MDX 路径,并用generateMetadata注入页面元数据 frontend/src/app/[lang]/docs/[[...mdxPath]]/page.tsx:5-11。workspace— 核心聊天工作区。workspace/layout.tsx是登录守卫(见 Overview),内层WorkspaceContent提供QueryClientProvider、侧边栏、命令面板与Toasterfrontend/src/app/workspace/workspace-content.tsx:17-34。其下分chats/[thread_id](标准聊天)与agents/[agent_name]/chats/[thread_id](指定 agent 聊天),两者都有相同的ChatLayout,包裹SubtasksProvider/ArtifactsProvider/PromptInputProvider三层上下文 frontend/src/app/workspace/chats/[thread_id]/layout.tsx:7-19。聊天页本身用useThreadStream驱动流式渲染,创建线程后用原生history.replaceState而非路由跳转以避免组件重挂载 frontend/src/app/workspace/chats/[thread_id]/page.tsx:86-91。blog— 同样基于 Nextra,blog/layout.tsx用getBlogIndexData()取文章列表构建侧边栏 frontend/src/app/blog/layout.tsx:8-22。api/memory— 服务端代理路由,把GET/DELETE /api/memory透传到NEXT_PUBLIC_BACKEND_BASE_URL(默认http://127.0.0.1:8001),并剥离host/connection/content-length头 frontend/src/app/api/memory/route.ts:10-35。mock/api— 演示模式的假后端,覆盖models/skills/mcp/threads,如mock/api/models/route.ts直接返回硬编码模型列表 frontend/src/app/mock/api/models/route.ts:1-25。
src/core 业务逻辑层
core 把所有非 UI 逻辑收敛,关键模块:
api/— LangGraph SDK 客户端单例。getAPIClient()按mock/default缓存 client,构造时挂injectCsrfHeader钩子在每次状态变更请求前从csrf_tokenCookie 注入X-CSRF-Token头 frontend/src/core/api/api-client.ts:34-72。fetcher.ts提供带credentials: "include"与 CSRF 双提交保护的fetch包装,401 自动跳登录 frontend/src/core/api/fetcher.ts:39-60。config/— URL 解析中枢。getLangGraphBaseURL()在未配置NEXT_PUBLIC_LANGGRAPH_BASE_URL时回退到${origin}/api/langgraph(交给 rewrites 反代),mock 模式回退到${origin}/mock/apifrontend/src/core/config/index.ts:21-44。i18n/— 国际化。locale.ts定义SUPPORTED_LOCALES = ["en-US","zh-CN"]与归一化逻辑 frontend/src/core/i18n/locale.ts:1-7;server.ts从localeCookie 读语言(detectLocaleServer)并提供setLocale写一年期 Cookie frontend/src/core/i18n/server.ts:6-30;context.tsx是客户端I18nProvider,切换语言时同步写 Cookie frontend/src/core/i18n/context.tsx:14-33;locales/下en-US.ts/zh-CN.ts是文案表。threads/— 应用心脏。hooks.ts导出useThreadStream(单线程流式)与useThreads(线程列表)等 Hook frontend/src/core/threads/hooks.ts:161-787,细节见第 29 章。- 其余
auth/settings/memory/skills/mcp/uploads/artifacts/tasks/todos各管一块领域逻辑,详见第 31 章。
src/components UI 层
四类组件职责泾渭分明:ui/(45 个)与 ai-elements/(27 个)由 Shadcn / Vercel AI SDK registry 自动生成,ESLint 显式忽略,不可手改 frontend/eslint.config.js:10-16;workspace/ 是聊天业务组件(messages/artifacts/input-box/command-palette/workspace-sidebar 等);landing/ 是落地页区块,app/page.tsx 顺序组合 Header/Hero/CaseStudySection/SkillsSection 等 frontend/src/app/page.tsx:10-24。registry 来源在 components.json 中声明 frontend/components.json:21-25。
Data Flow
前端发起的 API 调用在生产环境下不直连后端,而是经 Next.js rewrites + nginx 反代到 Gateway。下面是一次聊天请求的端到端解析:
关键点:next.config.js 的 rewrites() 用 getInternalServiceURL() 读 DEER_FLOW_INTERNAL_GATEWAY_BASE_URL(缺省 http://127.0.0.1:8001),当 NEXT_PUBLIC_LANGGRAPH_BASE_URL 未设置时,把 /api/langgraph → ${gatewayURL}/api,并用兜底规则 /api/:path* → ${gatewayURL}/api/:path* 覆盖 models/threads/memory 等所有未单独开关的路由;此兜底规则必须排在 langgraph 规则之后以保留公共前缀 frontend/next.config.js:24-74。
速查表
src/app 路由分区矩阵
| 路由 | 类型 | 守卫/数据源 | Source |
|---|---|---|---|
/(landing) | 静态营销 | 无,纯组件组合 | page.tsx:10-24 |
(auth)/login (auth)/setup | 鉴权 | getServerSideUser 重定向 | (auth)/layout.tsx:16-46 |
[lang]/docs | Nextra MDX | getPageMap + 静态参数 | docs/layout.tsx:27-50 |
workspace/chats/[thread_id] | 动态聊天 | 登录守卫 + 上下文链 | chats layout.tsx:7-19 |
workspace/agents/... | 指定 agent 聊天 | 同上 | agent layout.tsx:7-19 |
blog | Nextra MDX | getBlogIndexData | blog/layout.tsx:8-22 |
api/memory | 服务端代理 | 透传到后端 | api/memory/route.ts:10-35 |
mock/api/* | 假后端 | 硬编码响应 | mock models/route.ts:1-25 |
src/core 模块矩阵
| 模块 | 职责 | Source |
|---|---|---|
api | LangGraph SDK 单例 + CSRF 注入 | api-client.ts:34-72 |
config | 后端/LangGraph base URL 解析 | config/index.ts:21-44 |
threads | 线程流 Hook 与状态 | threads/hooks.ts:161 |
i18n | 多语言解析与 Provider | i18n/server.ts:6-30 |
auth | 服务端用户判定与守卫 | workspace/layout.tsx:15-29 |
settings/memory/skills/mcp/uploads | 各领域逻辑(见第 31 章) | core/ 桶导出 |
Configuration
NEXT_PUBLIC_* 与服务端环境变量
环境变量经 src/env.js 用 @t3-oss/env-nextjs + Zod 在构建期校验,SKIP_ENV_VALIDATION=1 可跳过(Docker 构建用) frontend/src/env.js:4-50。
| 变量 | 作用域 | 默认 | 说明 | Source |
|---|---|---|---|---|
NEXT_PUBLIC_BACKEND_BASE_URL | client | 空(走 nginx) | 后端 REST 直连地址,api/memory 代理也用它 | env.js:22 |
NEXT_PUBLIC_LANGGRAPH_BASE_URL | client | 空(走 rewrites) | LangGraph 流式直连地址 | env.js:23 |
NEXT_PUBLIC_STATIC_WEBSITE_ONLY | client | 空 | "true" 时禁用输入框,演示站模式 | env.js:24 |
DEER_FLOW_INTERNAL_GATEWAY_BASE_URL | server | http://127.0.0.1:8001 | rewrites 反代目标 | next.config.js:26-29 |
NODE_ENV | server | development | 运行环境枚举 | env.js:11-13 |
构建与质量配置
| 配置 | 关键点 | Source |
|---|---|---|
next.config.js | i18n locales en/zh、Nextra 包裹、rewrites 反代 | next.config.js:18-77 |
tsconfig.json | strict、@/*→src/*、moduleResolution: Bundler | tsconfig.json:12-33 |
eslint.config.js | 忽略 ui//ai-elements/,强制 import 排序 | eslint.config.js:10-82 |
vitest.config.ts | 单测匹配 tests/unit/**,@→src 别名 | vitest.config.ts:5-14 |
playwright.config.ts | Chromium,启动时 SKIP_ENV_VALIDATION=1 + DEER_FLOW_AUTH_DISABLED=1 | playwright.config.ts:24-33 |
package.json | Next 16 / React 19 / pnpm 10.26.2,pnpm check = lint + tsc | package.json:6-20 |
Common Pitfalls / Tips
- 生产别配
NEXT_PUBLIC_*后端地址。配了就绕过 nginx/rewrites 直连,容易跨域和 CSRF 失败;默认留空让next.config.js反代是推荐路径 frontend/next.config.js:31-71。 - 创建线程后不要用
router.push跳转。聊天页用history.replaceState改 URL,用 Next.js 路由会导致组件重挂载、丢失流式状态 frontend/src/app/workspace/chats/[thread_id]/page.tsx:89-91。 - 别手改
ui/与ai-elements/。它们由 registry 生成且被 ESLint 忽略,手改会在下次重新生成时被覆盖 frontend/eslint.config.js:10-16。 - 状态变更请求必须走封装的 fetch/SDK client。裸
fetch()不带X-CSRF-Token,Gateway 双提交校验会返回 403 frontend/src/core/api/fetcher.ts:39-55。 - i18n 用
localeCookie 单一来源。服务端detectLocaleServer与客户端I18nProvider都读写同名 Cookie,保持 SSR/CSR 一致避免水合不匹配 frontend/src/core/i18n/context.tsx:23-26。 - 业务逻辑写进
core,组件只渲染。这样tests/unit/能脱离 React 树直接测,且 import 别名统一用@/。
References
- frontend/package.json:1-117 — 技术栈与脚本
- frontend/next.config.js:1-77 — i18n / rewrites 反代
- frontend/src/env.js:1-50 — 环境变量 Zod 校验
- frontend/tsconfig.json:1-45 — TS 严格模式与路径别名
- frontend/src/app/workspace/layout.tsx:1-60 — 工作区登录守卫
- frontend/src/core/api/api-client.ts:1-72 — LangGraph 客户端单例
- frontend/src/core/config/index.ts:1-44 — base URL 解析
- frontend/src/core/i18n/server.ts:1-41 — 服务端语言解析
Related Pages
| 页面 | 关系 |
|---|---|
| 03-系统整体架构 | 本章是前端侧落地,03 章给出前后端整体边界与 nginx 拓扑 |
| 29-AI消息流与流式渲染 | 承接本章 useThreadStream,深入流式事件解析与渲染 |
| 30-工作区与聊天界面 | 展开本章 workspace 分区的聊天 UI 组件细节 |
| 31-前端核心服务层 | 展开本章 src/core 各业务模块的实现 |