前端技术栈的选择,本质是在交互复杂度与维护成本之间做取舍。项目要处理 GitHub 安装授权、文件树勾选、任务进度轮询这些交互,又得保证首屏快、代码好维护,还得让不认识我的人也能接手。对比过 Remix、SvelteKit,试过 Redux 又中途换掉,最后定下的组合是:Next.js 14 App Router 为主,TypeScript 强类型,shadcn/ui 提供组件,TanStack Query 管服务端状态,Zustand 管客户端状态,Tailwind 做样式。整体思路是”服务端优先、客户端按需水合”,把登录、列表等逻辑放服务端组件,把交互密集的部分放客户端组件。
一、技术选型对比
选型那两周我把每个候选都写了个 Demo,光对比记录就攒了好几屏。框架层面在 Next.js、Remix、SvelteKit 之间犹豫过,最终因为生态成熟度和 GitHub 相关库的兼容性选了 Next.js——Auth.js、TanStack Query 这些库对它的支持文档最全,遇到问题基本都能搜到答案。
| 维度 | 选项 | 本项目选择 |
|---|---|---|
| 框架 | Next.js / Remix / SvelteKit | Next.js(生态成熟) |
| 路由 | App Router / Pages | App Router |
| 组件库 | shadcn/ui / MUI / Chakra | shadcn/ui(轻、可改) |
| 状态 | Redux / Zustand / Recoil | Zustand |
| 服务端状态 | SWR / TanStack Query | TanStack Query |
| 样式 | Tailwind / CSS-in-JS | Tailwind |
| 鉴权 | NextAuth.js v5 | NextAuth.js v5 |
| 表单 | React Hook Form / Formik | React Hook Form + Zod |
这套组合里几乎没有冷门选择,社区资料多,对开源项目长期维护很重要——后来 Contributors 提 PR 时基本不用先问”这个技术栈怎么用”。唯一被反复质疑的是 Zustand 而不是 Redux,理由在状态管理那节展开说。
二、整体架构
目录结构是从失败里长出来的。最初组件全堆在 /components,业务逻辑和 UI 混在一起,改一个功能要翻三四个文件,删一个功能还怕误删公共件。后来按 route、components、features、lib、hooks 分层,边界才清晰。
/app
/(marketing) 公开页(首页、定价、文档)
/dashboard 登录后页面
/installations
/repositories
/jobs
/api 服务端 API
/components 通用 UI
/features 业务组件(按功能分)
/lib 工具与服务
/hooks 自定义 hooks
这个分层有个约定:/features 下每个功能自带组件、hooks 和 API 封装,/lib 只放无业务含义的工具函数,/hooks 放跨功能复用的自定义 hook。这样删一个功能直接删目录,不会留下僵尸代码,重构时也不用在整个仓库里翻找依赖关系。
三、关键页面实现
三个核心页面对应三类典型交互:选择、跟踪、配置。每个页面都坚持”服务端先出数据、客户端接管交互”的节奏,下面按页面拆开看。
3.1 仓库选择页
- 服务端组件拉安装列表(Installation Token);
- 客户端组件展示文件树;
- 提交后调
POST /api/translation-jobs创建任务。
文件树放在客户端,是因为它需要展开、勾选、半选这些本地状态;安装列表放在服务端,是因为它只读且每次都要鉴权。这个拆分让首屏 SSR 直接出列表,用户不用先看 loading 再等接口。
3.2 任务详情页
- 顶部:进度条 + 状态卡片;
- 中部:片段列表,每条显示状态、重试按钮;
- 底部:日志折叠区。
任务页的轮询逻辑一开始写在组件里,切页就丢进度,回到页面还得重新等一轮。后来把轮询放到 TanStack Query 的 refetchInterval 配置里,配合窗口聚焦时重新拉取,体验稳定很多,代码也只剩几行配置。
3.3 设置页
- 主题切换、API Key 管理、Webhook 配置;
- 表单用 React Hook Form + Zod 校验。
设置页表单多,用 React Hook Form 管受控值,Zod 的 schema 前后端共用一份,前端做即时校验、后端做最终校验。字段加了约束不会出现一边改一边漏的情况——之前 Webhook 地址格式前后端校验不一致,白跑过几个坏请求。
四、状态管理分层
状态管理被 Redux 教育过一次就清醒了。项目里大部分数据来自服务端接口,真正的全局 UI 状态没几个,Redux 的样板代码带来的负担远超收益。四类状态的分工如下:
| 状态类型 | 工具 | 例子 |
|---|---|---|
| 服务端缓存 | TanStack Query | 仓库列表、任务列表 |
| 客户端 UI 状态 | Zustand | 主题、侧栏展开 |
| 表单 | React Hook Form | 创建任务表单 |
| URL 状态 | nuqs | 搜索条件 |
这个分层让新同事接手时能很快判断”这个状态该放哪”,基本不需要讨论。TanStack Query 管服务端缓存天然支持过期失效和重试,Zustand 只存主题、侧栏这类本地偏好,表单状态留在表单组件里,搜索条件同步到 URL 方便分享链接。
五、服务端优先策略
App Router 的 RSC 直接改变了写页面的方式。列表数据用 Server Component 拉取,首屏不带 loading 状态,浏览器拿到的 HTML 里就有数据,SEO 和首屏速度都好,也不用自己设计一套数据加载态的骨架。
- 列表数据用 Server Component 拉取,首屏不带 loading 状态;
- 交互密集的页面用
"use client"; - 表单提交用 Server Actions 减少 API 样板。
仓库选择页的服务端组件长这样,数据逻辑都在组件外,组件本身只剩数据流:
// app/dashboard/repositories/page.tsx
import { auth } from "@/lib/auth";
import { listInstallations } from "@/lib/github";
export default async function Page() {
const session = await auth();
const installations = await listInstallations(session!.user.id);
return <RepoList initial={installations} />;
}
这里 auth() 和 listInstallations 都做了缓存,列表页刷新也不会重复打 GitHub API。踩过的坑是 “use client” 滥用:有一版文件树整页客户端渲染,首屏白屏一秒多,后来只把树组件标成客户端,其它保持服务端,速度立刻回来。
六、API 设计
REST 风格,统一前缀 /api,错误码用 HTTP 状态 + 自定义错误体。这个统一约定是前后端联调阶段踩了几次坑才固化下来的——前端总是拿到一堆字符串错误,没法判断该不该重试。统一错误结构如下:
type ApiError = {
code: string;
message: string;
traceId: string;
};
把 traceId 加进错误体是被线上问题逼出来的——用户报错时只说”又失败了”,没有 traceId 根本查不到日志。现在前端捕获错误后把 traceId 显示出来,后端按 traceId 捞链路日志,定位快很多。WebSocket 暂未使用,任务进度走轮询,详情见姊妹篇。
七、可访问性
无障碍不是加分项,是默认项。做文件树时专门逐项核对过键盘操作,Tab 导航、方向键展开折叠、Enter 勾选,都按原生 tree 的 ARIA 模式实现,而不是只让鼠标能用。
- 全部交互元素键盘可达;
- 焦点环清晰;
- 颜色对比满足 WCAG AA;
- 模态使用 Radix UI,自带焦点陷阱。
Radix UI 的模态自带焦点陷阱,比手写弹窗省了很多心力。颜色对比这块靠 Tailwind 的语义色板兜底,自定义色值统一走主题变量,避免随手写一个接近背景色的文字。
八、国际化
文案用 next-intl,默认中文,提供英文 fallback。有一个坑:一开始直接拿翻译工具的输出当 UI 文案,结果界面中英夹杂,术语和界面文案混在一起,用户看设置页都不知道按钮是干什么的。
- 文案用
next-intl; - 默认中文,提供英文 fallback;
- 不把翻译工具的输出直接用于 UI 文案,避免术语冲突。
后来明确约定:UI 文案单独维护一份,翻译工具的译文只出现在任务结果里。这样界面语言稳定,也方便以后加别的语言时统一管理。
九、构建与性能
性能优化更多是”选对默认”。路由级代码分割、图片走 next/image、字体走 next/font 自我托管、Tailwind JIT 减小 CSS 体积,这些都是框架自带能力,把默认用对就有基础分。
- 路由级代码分割;
- 图片走
next/image; - 字体走
next/font自我托管; - Tailwind JIT 减小 CSS 体积;
- Lighthouse 性能 90+。
Lighthouse 90+ 是在做了资源预加载、减少客户端水合之后才稳定下来的。有一版把 markdown 渲染库在客户端全量引入,包体积涨到 400 多 KB,改成服务端渲染加动态加载后掉到 200 以内,首屏明显变快。
十、测试
测试策略按成本分档:逻辑核心用单元测试,关键用户路径用 e2e,视觉回归按需。文件树那种纯函数逻辑是最值得写单测的,边界条件多、容易回归;钩子逻辑反而难测,交给 e2e 兜底更划算。
- 单元:Vitest + Testing Library;
- e2e:Playwright 覆盖登录、创建任务、查看进度;
- 视觉回归:Chromatic(按需)。
十一、CI
CI 就是质量的看门人。每次 PR 跑 lint、typecheck、test、build,Preview 部署用 Vercel 自动,合并保护要求 e2e 必须过。代价是每个 PR 要等几分钟,但换来的是 main 分支永远可部署,出问题也能定位到具体一次合并。
- 每次 PR:lint、typecheck、test、build;
- Preview 部署:Vercel 自动;
- 合并保护:必须通过 e2e。
十二、可维护性
开源项目最怕个人风格浓。强类型 + 路径别名 @/*、业务组件按 feature 分目录、共享类型收敛到 packages/types、ESLint + Prettier + import order 统一,都是为了降低”该放哪、该叫啥”的决策成本。
- 强类型 + 路径别名
@/*; - 业务组件按 feature 分目录;
- 共享类型在
packages/types; - ESLint + Prettier + import order 统一。
这些约定写进 CONTRIBUTING 后,外部 PR 的代码风格基本一致,review 时间省了一大半。
十三、给同类项目的建议
如果让我重来一遍,会更早确定下面四条原则,而不是踩完坑再总结。它们不依赖具体框架,换技术栈也适用。
- 默认服务端组件,按需水合;
- 状态分层清晰,避免 Redux 滥用;
- 表单校验放 Zod,前后共用;
- 关键页 e2e 覆盖,其它靠单元测试。
技术栈本身不复杂,难的是把每层边界划清楚。这一套跑下来,前端从选型到稳定上线,返工最多的不是框架功能,而是状态归属和渲染边界这两件事,提前定好规则能省下大量迭代时间。
常见问题(FAQ)
Q1:为什么不用 Redux?
状态主要来自服务端,TanStack Query 更合适,Zustand 处理 UI 状态。
Q2:App Router 与 Pages Router 怎么选?
新项目选 App Router,老项目谨慎迁移。
Q3:样式方案用 CSS-in-JS 怎么样?
Next.js App Router 对 CSS-in-JS 支持在收敛,Tailwind 更稳。