跳到主要内容

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 灵活选择
运行时React19.2.1Server Components、并发模式
语言TypeScript5.x (strict)类型安全
样式Tailwind CSS4.x最新版,OKLch 色彩空间,@theme 语法
组件库Shadcn/uiNew York 主题源码可控、Radix UI 无障碍基础、高度可定制
国际化next-intl4.7.0Next.js App Router 深度集成
主题next-themes0.4.6SSR 兼容的主题切换,避免 FOUC
API 客户端Orval7.17.2OpenAPI → TypeScript 全自动生成
图标Lucide React0.562.0Tree-shakeable SVG 图标库
ToastSonner2.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 色彩空间

OKLch 色彩优势

采用 Tailwind CSS 4 的 OKLch(Oklab Lightness Chroma Hue)色彩模型,相比传统 HSL:

  • 感知均匀:同亮度值的颜色在视觉上亮度一致
  • 过渡平滑:明暗主题切换时颜色变化更自然
  • 无色偏:不同色相在相同明度下看起来一样亮

4.2 CSS 变量驱动

src/app/globals.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 基础组件

组件基础增强功能
ButtonRadix Slotloading 状态、icon 支持、异步 onClick 自动 loading
DialogRadix Dialog无障碍、键盘导航
AlertDialogRadix AlertDialog确认弹窗,支持链式调用
SelectRadix Select下拉选择,原生辅助功能
CheckboxRadix Checkbox表单控件
TooltipRadix 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 功能:

Button 异步 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.revalidatenext.tags ISR 配置
认证跳过skipAuth 选项跳过 Token 注入

6.3 认证管理

src/lib/auth.ts
// 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 多阶段构建

Dockerfile(概览)
# 阶段 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 — 会话级 UUID
  • APP_TITLE — "Swee"
  • 环境检测:isClient / isServer / isProduction / isMobile / isIos / isApp


九、技术方案深入剖析(面试细节)

9.1 Button 双模式 Loading:受控 vs 自管理

设计亮点

标准 Shadcn/ui Button 不支持 async loading,这里做了增强设计——同一组件同时支持受控和非受控两种模式,灵感来自 React 的 <input value> vs <input defaultValue> 设计。

src/components/ui/button.tsx
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 的关键设计

src/lib/request/request.ts
// 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 语言的构建策略

src/app/[locale]/layout.tsx
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 语言检测逻辑

src/middleware.ts
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 的特殊约束。

src/app/global-error.tsx
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 的设计哲学:

Compound Components 用法
// 使用方式:像搭积木一样组合
<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 模式 vs Compound 模式
// Props 模式:参数爆炸,难以扩展
<Empty
icon={<SearchIcon />}
title="No results"
description="Try different..."
actions={<Button>Go back</Button>}
/>

// Compound 模式:灵活组合、可选可缺、易于扩展新 slot

9.6 Docker 构建优化:Turbo Prune + Standalone

Dockerfile
# 阶段 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 自动注入:

src/middleware.ts(改进方案)
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,如果文件缺失会直接报错。

改进方案

src/i18n/request.ts(改进方案)
// 添加 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 readinessProbelivenessProbe 被注释掉了。

改进方案

deploy/kustomize/base/deployment.yaml(改进方案)
readinessProbe:
httpGet:
path: /api/health # 新增 API route
port: 3106
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/health
port: 3106
initialDelaySeconds: 30
periodSeconds: 10
src/app/api/health/route.ts(新增)
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

十一、技术亮点总结

  1. Next.js 16 + React 19 最新架构:全面采用 App Router + Server Components,默认服务端渲染,最小化客户端 JS
  2. 13 种语言国际化:Middleware 四级语言检测 + generateStaticParams 构建时预生成 + as-needed 默认语言无前缀
  3. OKLch 色彩系统:Tailwind CSS 4 感知均匀色彩空间,暗色主题仅调节明度不偏色
  4. 类型安全 API 层:Orval + Custom Mutator 全自动生成,零手写 API 代码,后端变更编译时报错
  5. Button 双模式 Loading:受控/非受控统一设计,async onClick 自动管理 loading 状态
  6. Compound Components:Empty 组件采用组合模式替代 Props 爆炸,灵活可扩展
  7. Docker 构建优化:Turbo Prune 裁剪 Monorepo + standalone 输出,镜像从 1.2GB → ~200MB
  8. 多层错误边界:4 层错误捕获分工明确,global-error 用内联样式兜底
  9. 改进空间:Token 迁移至 Cookie 解锁 RSC API 调用、启用 K8s 健康探针、接入错误追踪服务