SWAI-主站
一、项目概览
SWAI Site 是 AI 换脸应用的面向用户的 Web 主站,基于 Next.js 16(App Router)+ React 19 + TypeScript 构建。目前处于技术选型与基础设施搭建完成阶段,已建立完整的国际化、主题系统、组件库、API 层和 DevOps 流程,为后续业务功能开发提供了生产就绪的工程基座。
二、技术选型与架构决策
2.1 核心技术栈
| 类别 | 选型 | 版本 | 选型理由 |
|---|---|---|---|
| 框架 | Next.js (App Router) | 16.0.10 | 最新 RSC 架构,SSR/SSG/ISR 灵活选择 |
| 运行时 | React | 19.2.1 | Server Components、并发模式 |
| 语言 | TypeScript | 5.x (strict) | 类型安全 |
| 样式 | Tailwind CSS | 4.x | 最新版,OKLch 色彩空间,@theme 语法 |
| 组件库 | Shadcn/ui | New York 主题 | 源码可控、Radix UI 无障碍基础、高度可定制 |
| 国际化 | next-intl | 4.7.0 | Next.js App Router 深度集成 |
| 主题 | next-themes | 0.4.6 | SSR 兼容的主题切换,避免 FOUC |
| API 客户端 | Orval | 7.17.2 | OpenAPI → TypeScript 全自动生成 |
| 图标 | Lucide React | 0.562.0 | Tree-shakeable SVG 图标库 |
| Toast | Sonner | 2.0.7 | 轻量美观的通知组件 |
2.2 关键架构决策
| 决策 | 选择 | 考量 |
|---|---|---|
| 路由方案 | App Router(无 pages 目录) | 全面拥抱 RSC,Server Components 为默认 |
| 渲染策略 | Server-First | 默认服务端组件,仅交互部分 'use client' |
| 构建输出 | output: "standalone" | Docker 优化,独立部署不依赖 node_modules |
| 国际化路由 | localePrefix: 'as-needed' | 默认语言(英文)无前缀,其他语言有前缀 |
| 色彩系统 | OKLch CSS 变量 | 感知均匀色彩空间,主题切换更自然 |
| 组件策略 | Shadcn/ui 源码复制 | 完全掌控组件代码,避免第三方库升级风险 |
三、国际化体系(13 种语言)
实现要点
语言检测优先级:URL path → Cookie → Accept-Language header → 默认英文
服务端/客户端使用方式:
- Server Components:
const t = await getTranslations('namespace') - Client Components:
const t = useTranslations('namespace') - 导航组件:
<Link>/useRouter()/usePathname()自动处理语言前缀
语言切换器:3 种展示模式
full:国旗 + 语言名称compact:仅国旗icon:地球图标
四、主题系统
4.1 OKLch 色彩空间
采用 Tailwind CSS 4 的 OKLch(Oklab Lightness Chroma Hue)色彩模型,相比传统 HSL:
- 感知均匀:同亮度值的颜色在视觉上亮度一致
- 过渡平滑:明暗主题切换时颜色变化更自然
- 无色偏:不同色相在相同明度下看起来一样亮
4.2 CSS 变量驱动
@theme inline {
--color-background: oklch(1 0 0); /* 亮色背景 */
--color-primary: oklch(0.21 0.006 285); /* 主色 */
/* ... 20+ 语义化颜色变量 */
}
.dark {
--color-background: oklch(0.15 0 0); /* 暗色背景 */
--color-primary: oklch(0.98 0 0); /* 暗色主色 */
}
主题模式:Light / Dark / System(跟随系统偏好),基于 class 策略(.dark 选择器)
五、组件体系
5.1 Shadcn/ui 基础组件
| 组件 | 基础 | 增强功能 |
|---|---|---|
| Button | Radix Slot | loading 状态、icon 支持、异步 onClick 自动 loading |
| Dialog | Radix Dialog | 无障碍、键盘导航 |
| AlertDialog | Radix AlertDialog | 确认弹窗,支持链式调用 |
| Select | Radix Select | 下拉选择,原生辅助功能 |
| Checkbox | Radix Checkbox | 表单控件 |
| Tooltip | Radix Tooltip | 悬浮提示 |
5.2 自定义组件
| 组件 | 功能 |
|---|---|
| ThemeToggle | 三按钮主题切换(亮/暗/系统) |
| LanguageSwitcher | 多变体语言切换器(full/compact/icon) |
| Spinner | 加载动画(Loader 图标 + CSS 旋转) |
| Empty | 空状态组件(Header + Media + Title + Description 组合式) |
| Sonner (Toaster) | 主题感知的 Toast 通知 |
5.3 Button 增强 ⭐
Button 组件在标准 Shadcn/ui 基础上增加了 异步 onClick 自动 loading 功能:
// 传入 async onClick,Button 自动管理 loading 状态
<Button onClick={async () => {
await submitForm(); // 执行期间自动显示 loading
}}>
提交
</Button>
六、API 请求层
6.1 Orval 代码生成
4 个微服务的 API 自动生成,从 OpenAPI/Swagger 文档直接生成:
- TypeScript 函数(每个 API 端点 → 一个函数)
- 请求/响应类型定义
- 参数类型校验
6.2 请求处理器特性
| 特性 | 实现 |
|---|---|
| 超时控制 | AbortController,默认 10 分钟,可自定义 |
| 认证注入 | 自动附加 Authorization: Bearer {token} 和 X-App: {appId} |
| 错误分级 | HttpError(HTTP 错误)/ BusinessError(业务错误 code≠200)/ TimeoutError(超时) |
| 开发日志 | 彩色 console.log 输出请求/响应/错误详情 |
| Next.js 缓存 | 支持 next.revalidate 和 next.tags ISR 配置 |
| 认证跳过 | skipAuth 选项跳过 Token 注入 |
6.3 认证管理
// Token 获取优先级
getAccessToken(): query param > sessionStorage > localStorage
// 用户信息持久化
setUserInfo(info: UserBaseInfoVo): localStorage
getUserInfo(): UserBaseInfoVo | null
clearAuth(): 清除 Token + 用户信息
七、工程基础设施
7.1 项目结构
src/
├── app/ # Next.js App Router
│ ├── layout.tsx # 根布局(ThemeProvider)
│ ├── global-error.tsx # 全局错误边界
│ ├── not-found.tsx # 全局 404
│ └── [locale]/ # 动态语言路由
│ ├── layout.tsx # 语言布局(NextIntlClientProvider)
│ ├── error.tsx # 语言级错误页(含 i18n)
│ ├── loading.tsx # 加载骨架屏
│ └── not-found.tsx # 语言级 404(含 i18n)
├── components/
│ ├── ui/ # Shadcn/ui 组件集
│ ├── providers/ # ThemeProvider
│ └── shared/ # LanguageSwitcher 等
├── i18n/
│ ├── config.ts # 语言配置与元数据
│ ├── routing.ts # 路由配置
│ ├── request.ts # 服务端 i18n
│ ├── navigation.ts # 语言感知导航
│ └── messages/ # 13 种语言 JSON 文件
├── apis/ # Orval 生成的 API 客户端
└── lib/
├── auth.ts # 认证工具
├── utils.ts # cn() 工具函数
└── request/ # 请求层
├── request.ts # 自定义 Fetch Mutator
└── error.ts # 错误类定义
7.2 多层错误处理
7.3 Docker 多阶段构建
# 阶段 1: prepare - 安装依赖
FROM node:24.12.0-slim AS prepare
# 阶段 2: builder - 构建应用
FROM prepare AS builder
# Next.js standalone output
# 阶段 3: runner - 生产运行
FROM node:24.12.0-alpine AS runner
USER nextjs (UID 1001) # 非 root 运行
CMD pm2-runtime server.js # PM2 进程管理
7.4 Kubernetes 部署
deploy/kustomize/
├── base/
│ ├── deployment.yaml # 基础 Deployment
│ ├── service.yaml # Service (端口 3106)
│ └── kustomization.yaml
└── overlays/
├── test/ # 测试环境 overlay
└── production/ # 生产环境 overlay (含 Ingress)
资源配置:Memory 1Gi2Gi request / 2Gi4Gi limit
八、Monorepo 共享包
constant 包:
EAppId.Swai(2412413) — 应用标识deviceId— 基于 UUID 的设备指纹(localStorage 持久化)sessionId— 会话级 UUIDAPP_TITLE— "Swee"- 环境检测:
isClient/isServer/isProduction/isMobile/isIos/isApp
九、技术方案深入剖析(面试细节)
9.1 Button 双模式 Loading:受控 vs 自管理
标准 Shadcn/ui Button 不支持 async loading,这里做了增强设计——同一组件同时支持受控和非受控两种模式,灵感来自 React 的 <input value> vs <input defaultValue> 设计。
type ButtonProps = {
loading?: boolean; // 受控模式:外部传入 loading 状态
onClick?: (e: MouseEvent) => void | Promise<unknown>; // 非受控模式:返回 Promise 自动管理
};
function Button({ loading: loadingProp, onClick, children, ...props }) {
const [internalLoading, setInternalLoading] = useState(false);
const isControlled = loadingProp !== undefined; // 判断模式
const isLoading = isControlled ? loadingProp : internalLoading;
const handleClick = async (e) => {
const result = onClick?.(e);
// 仅在非受控模式 + onClick 返回 Promise 时自动管理 loading
if (result instanceof Promise && !isControlled) {
setInternalLoading(true);
try { await result; }
finally { setInternalLoading(false); }
}
};
return (
<button disabled={isLoading || props.disabled} onClick={handleClick}>
{isLoading ? <Spinner /> : icon}
{children}
</button>
);
}
面试可聊的点:
- 受控/非受控模式的设计灵感来自 React 的
<input value>vs<input defaultValue>模式 finally确保即使 Promise reject 也能恢复 loading 状态instanceof Promise检测:为什么不用typeof result?.then === 'function'?→ 后者更严谨(支持 thenable),但instanceof语义更清晰
9.2 Orval + Custom Mutator:API 层的零手写方案
整个 API 层的核心设计是从 OpenAPI 到可调用函数的全自动管线:
Custom Mutator 的关键设计:
// Orval 生成的函数签名(自动生成,不手写)
export const loginByPassword = (body: LoginByPasswordReq) =>
request<LoginByPasswordRes>({
url: '/platform-user/api/user/login_by_password',
method: 'POST',
data: body,
});
// Custom Mutator:所有生成的函数都经过这个统一入口
export const request = async <T>(config: RequestConfig): Promise<T> => {
const { url, method, data, params, ...options } = config;
// 1. 自动注入认证头
const headers: Record<string, string> = {
'X-App': String(EAppId.Swai),
'X-Device': deviceId,
};
if (!options.skipAuth) {
const token = getAccessToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
}
// 2. AbortController 超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeout || 600000);
// 3. Next.js 缓存集成
const fetchOptions: RequestInit = {
method,
headers,
body: data ? JSON.stringify(data) : undefined,
signal: controller.signal,
next: options.next, // { revalidate: 60, tags: ['user'] }
};
// 4. 三级错误分类
const response = await fetch(fullUrl, fetchOptions);
if (!response.ok) throw new HttpError(response.status);
const result = await response.json();
if (result.code !== 200) throw new BusinessError(result.code, result.msg);
return result as T;
};
面试可聊的点:
- 为什么用 Orval 而不是手写 API?→ 类型安全(后端改接口 → 前端编译报错)、开发效率(一条命令更新所有 API)
- Custom Mutator 的意义?→ 将 Orval 生成的纯函数签名与实际请求实现解耦,切换 axios/fetch 只需改 mutator
- Next.js
next选项的用途?→ 利用 App Router 的内置缓存,revalidate: 60实现 ISR
9.3 i18n 静态生成:13 语言的构建策略
export function generateStaticParams() {
return routing.locales.map(locale => ({ locale }));
}
// 构建时生成 13 个静态路由变体:/, /zh, /ja, /ko, ...
// 每个页面也需要调用 setRequestLocale
export default async function Page({ params }) {
const { locale } = await params;
setRequestLocale(locale); // 告知 next-intl 当前请求的语言
// ...
}
Middleware 语言检测逻辑:
export default createMiddleware(routing);
// 检测优先级:
// 1. URL path: /zh/about → locale = 'zh'
// 2. Cookie: NEXT_LOCALE=ja → locale = 'ja'
// 3. Accept-Language: zh-TW,zh;q=0.9 → locale = 'zh'
// 4. Default: 'en'
// 匹配规则:排除静态资源
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)',]
};
localePrefix: 'as-needed' 的路由效果:
| 语言 | URL | 说明 |
|---|---|---|
| English | /about | 默认语言无前缀 |
| 繁體中文 | /zh/about | 非默认语言有前缀 |
| 日本語 | /ja/about | 非默认语言有前缀 |
9.4 多层错误处理的分工
错误类型 → 捕获层 → 用户体验
───────────────────────────────────────────────────────────────
无效语言 URL → app/not-found.tsx → 英文 404(无 i18n)
页面不存在 → [locale]/not-found.tsx → 本地化 404 页面
页面渲染异常 → [locale]/error.tsx → 本地化错误页 + 重试按钮
全局不可恢复异常 → global-error.tsx → 内联样式兜底页(不依赖 CSS)
页面加载中 → [locale]/loading.tsx → Spinner 加载动画
global-error.tsx 的特殊设计:
global-error.tsx 必须自行渲染 <html> 和 <body>(因为外层 layout 可能已崩溃),且必须使用内联样式(因为 CSS 文件可能加载失败)。这是 Next.js App Router 的特殊约束。
export default function GlobalError({ error, reset }) {
return (
<html>
<body style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ background: 'linear-gradient(...)' }}>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
</body>
</html>
);
}
9.5 Empty 组合组件模式
Empty 组件采用 Compound Components 模式,类似 Radix UI 的设计哲学:
// 使用方式:像搭积木一样组合
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon" /> {/* 图标或图片 */}
<EmptyTitle>No results</EmptyTitle>
<EmptyDescription>Try different search terms</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Go back</Button> {/* 操作区域 */}
</EmptyContent>
</Empty>
// 每个子组件通过 data-slot 标识,父组件可通过 CSS 选择器统一控制布局
// 例如:[data-slot="empty-header"] { text-align: center; }
对比传统 Props 模式:
// Props 模式:参数爆炸,难以扩展
<Empty
icon={<SearchIcon />}
title="No results"
description="Try different..."
actions={<Button>Go back</Button>}
/>
// Compound 模式:灵活组合、可选可缺、易于扩展新 slot
9.6 Docker 构建优化:Turbo Prune + Standalone
# 阶段 1: prepare — Turbo Prune 裁剪 Monorepo
RUN npx turbo prune ${APP_NAME} --docker
# 仅提取目标 app 及其依赖包(非全量 monorepo),减小构建上下文
# 阶段 2: builder — 构建
COPY --from=prepare /app/out/json/ . # 只有 package.json 们
RUN pnpm install --frozen-lockfile # 利用 Docker layer cache
COPY --from=prepare /app/out/full/ . # 再复制源码
RUN pnpm turbo build:${APP_ENV} # 构建
# 阶段 3: runner — 最小化生产镜像
FROM node:24-alpine # 更小的基础镜像
COPY --from=builder .next/standalone ./ # 只复制 standalone 产物
# standalone 包含 server.js + 内嵌的 node_modules,不需要完整 node_modules
- 未优化:~1.2GB(全量 node_modules)
- standalone:~150MB(仅必要依赖)
- 加上 alpine 基础镜像:~200MB 总计(缩减约 83%)
十、改进建议(面试加分项)
10.1 Server Components API 调用
现状:getAccessToken() 仅在客户端工作(依赖 localStorage),Server Components 无法携带认证发起 API 请求。
改进方案:将 Token 存入 Cookie,Middleware 自动注入:
export function middleware(request: NextRequest) {
const token = request.cookies.get('access_token')?.value;
if (token) {
// 注入到请求头,Server Components 可通过 headers() 读取
request.headers.set('authorization', `Bearer ${token}`);
}
}
// Server Component 中
import { cookies } from 'next/headers';
const token = cookies().get('access_token')?.value;
const data = await fetchWithAuth('/api/user', token);
收益:解锁 Server Components 直接调用后端 API,减少客户端 JS、提升首屏速度。
10.2 翻译文件加载容错
现状:(await import(`./messages/${locale}.json`)).default,如果文件缺失会直接报错。
改进方案:
// 添加 try-catch + fallback
let messages;
try {
messages = (await import(`./messages/${locale}.json`)).default;
} catch {
console.warn(`Missing translations for locale: ${locale}, falling back to en`);
messages = (await import('./messages/en.json')).default;
}
10.3 健康检查探针
现状:Kubernetes readinessProbe 和 livenessProbe 被注释掉了。
改进方案:
readinessProbe:
httpGet:
path: /api/health # 新增 API route
port: 3106
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/health
port: 3106
initialDelaySeconds: 30
periodSeconds: 10
export async function GET() {
return Response.json({ status: 'ok', timestamp: Date.now() });
}
10.4 其他可讨论的改进方向
| 方向 | 现状 | 改进建议 |
|---|---|---|
| 错误追踪 | console.error 输出 | 接入 Sentry 等错误追踪服务,error.tsx 中上报 |
| 生产日志 | 彩色 console.log 始终开启 | 添加 process.env.NODE_ENV 判断,生产环境关闭 |
| ISR 利用 | 仅基座,未使用 revalidate | 对低频变更页面(如 /about)使用 revalidate: 3600 |
| 请求去重 | 同一 API 可能被多个组件同时调用 | 利用 React cache() 或 SWR/React Query 去重 |
| 翻译按需加载 | 全量加载语言文件 | 按 namespace 拆分,页面级按需 import |
| SEO | 基础 meta 标签 | 添加 generateMetadata() 动态 SEO + Open Graph + hreflang |
十一、技术亮点总结
- Next.js 16 + React 19 最新架构:全面采用 App Router + Server Components,默认服务端渲染,最小化客户端 JS
- 13 种语言国际化:Middleware 四级语言检测 +
generateStaticParams构建时预生成 +as-needed默认语言无前缀 - OKLch 色彩系统:Tailwind CSS 4 感知均匀色彩空间,暗色主题仅调节明度不偏色
- 类型安全 API 层:Orval + Custom Mutator 全自动生成,零手写 API 代码,后端变更编译时报错
- Button 双模式 Loading:受控/非受控统一设计,async onClick 自动管理 loading 状态
- Compound Components:Empty 组件采用组合模式替代 Props 爆炸,灵活可扩展
- Docker 构建优化:Turbo Prune 裁剪 Monorepo + standalone 输出,镜像从 1.2GB → ~200MB
- 多层错误边界:4 层错误捕获分工明确,global-error 用内联样式兜底
- 改进空间:Token 迁移至 Cookie 解锁 RSC API 调用、启用 K8s 健康探针、接入错误追踪服务