主题
03-03 · 前端 · React + Vite
React + Vite 是公司前端默认栈。 选它的原因:生态最大、AI 模型对它最熟、shadcn/ui 提供高质量可复制组件、TanStack 全家桶解决了路由 / 数据 / 表单的核心问题。
一、为什么是 React + Vite
| 候选 | 评价 |
|---|---|
| React 19 + Vite(推荐) | 极简极快、AI 协同最佳、shadcn 生态强 |
| Next.js 15 | 适合中大型 SSR / SEO 项目,作为 Vite 的"升级方案" |
| Vue 3 + Vite | 生态够用,但公司团队历史负担少,不切 |
| SvelteKit | 体验好但社区相对小,AI 协同 vs React 略差 |
| Nuxt 3 | 同 Vue,团队不主选 |
公司硬约定:
- 小到中型工具 / SPA / 内部后台:React 19 + Vite
- 中到大型、有 SEO 需求、有 SSR 需求:Next.js 15 + App Router
- 新项目禁用 Create React App(已弃维护)和 Webpack 5 + Babel(落后)
二、版本与依赖锁定(公司默认)
| 库 | 版本约束 | 用途 |
|---|---|---|
| Node.js | >=20 LTS(推荐 22 LTS) | 运行时 |
| pnpm | >=9 | 包管理(替代 npm / yarn) |
| vite | >=6 | 构建工具 |
| react / react-dom | >=19 | 框架 |
| typescript | >=5.5 | 必须 strict |
| tailwindcss | >=4 | 样式 |
| shadcn/ui | latest | 组件(copy 到项目里) |
| @tanstack/react-router | >=1.x | 路由 |
| @tanstack/react-query | >=5 | 数据请求 |
| react-hook-form | >=7 | 表单 |
| zod | >=3 | schema / 校验 |
| lucide-react | latest | 图标 |
| framer-motion | >=11 | 动效 |
| zustand | >=4 | 客户端状态(小) |
| vitest + @testing-library/react | latest | 测试 |
| eslint + prettier | latest | lint / 格式化 |
不要装:
axios(用 fetch + TanStack Query)、moment(用 dayjs / date-fns)、react-router-dom(用 TanStack Router)、redux(用 zustand 或 TanStack Query 缓存)
三、项目结构(公司标准)
my-web/
├── package.json
├── pnpm-lock.yaml ← 锁文件,必须 commit
├── vite.config.ts
├── tsconfig.json
├── tailwind.config.ts
├── components.json ← shadcn 配置
├── README.md
├── AGENTS.md / CLAUDE.md
├── DESIGN.md ← 设计系统(详见 04)
├── .env.example
├── public/
└── src/
├── main.tsx ← 应用入口
├── app.tsx ← Provider / 路由根
├── routes/ ← TanStack Router 路由
│ ├── __root.tsx
│ ├── index.tsx
│ └── leads.tsx
├── components/
│ ├── ui/ ← shadcn 组件(自动生成)
│ └── domain/ ← 业务组件
├── features/ ← 按业务域分目录
│ └── leads/
│ ├── api.ts ← TanStack Query hooks
│ ├── schemas.ts ← zod schema
│ └── lead-table.tsx
├── lib/ ← 工具函数 / fetch 封装
├── hooks/ ← 通用 hooks
├── stores/ ← zustand stores
└── styles/
└── globals.css三个分层原则:
- components/ui:shadcn 给的纯展示组件,不写业务逻辑
- components/domain:跨 feature 的复合组件
- features/:按业务域聚合(API + schema + 组件)
四、最小可运行项目(5 分钟)
4.1 安装
bash
# 装 pnpm(如还没装)
npm install -g pnpm
# 起项目
pnpm create vite@latest my-web -- --template react-ts
cd my-web
pnpm install4.2 装 Tailwind v4
bash
pnpm add -D tailwindcss @tailwindcss/vitevite.config.ts:
ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "node:path";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: { "@": path.resolve(__dirname, "src") },
},
});src/styles/globals.css:
css
@import "tailwindcss";
@theme {
--color-primary-500: #2b5cff;
--font-sans: "Inter", "PingFang SC", system-ui;
}src/main.tsx:
tsx
import "@/styles/globals.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./app";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);4.3 装 shadcn/ui
bash
pnpm dlx shadcn@latest init -d
pnpm dlx shadcn@latest add button card input formshadcn 不是传统 npm 包,它是 把组件代码"复制"到你 src/components/ui/ 里。这样你能 100% 自己改。配合 DESIGN.md 用,简直完美。
4.4 装数据 / 路由
bash
pnpm add @tanstack/react-router @tanstack/react-query \
react-hook-form zod lucide-react
pnpm add -D @tanstack/router-plugin4.5 跑起来
bash
pnpm dev # 默认 http://localhost:5173五、核心模式
5.1 数据请求:TanStack Query
src/lib/http.ts:
ts
const BASE = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
headers: { "Content-Type": "application/json", ...(init?.headers || {}) },
...init,
});
if (!res.ok) {
const detail = await res.text().catch(() => "");
throw new Error(`${res.status} ${res.statusText}: ${detail}`);
}
return res.json() as Promise<T>;
}src/features/leads/api.ts:
ts
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { z } from "zod";
import { apiFetch } from "@/lib/http";
export const Lead = z.object({
id: z.number(),
company_name: z.string(),
industry: z.string().nullable(),
score: z.number(),
created_at: z.string(),
});
export type Lead = z.infer<typeof Lead>;
export function useLeads() {
return useQuery({
queryKey: ["leads"],
queryFn: async () => {
const data = await apiFetch<Lead[]>("/api/v1/leads");
return z.array(Lead).parse(data);
},
staleTime: 30_000,
});
}
export function useCreateLead() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: Omit<Lead, "id" | "created_at">) =>
apiFetch<Lead>("/api/v1/leads", {
method: "POST",
body: JSON.stringify(input),
}),
onSuccess: () => qc.invalidateQueries({ queryKey: ["leads"] }),
});
}5.2 表单:react-hook-form + zod
tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useCreateLead } from "@/features/leads/api";
const Schema = z.object({
company_name: z.string().min(2).max(200),
industry: z.string().optional(),
score: z.coerce.number().min(0).max(100).default(0),
});
type FormVals = z.infer<typeof Schema>;
export function CreateLeadForm() {
const create = useCreateLead();
const { register, handleSubmit, formState: { errors } } = useForm<FormVals>({
resolver: zodResolver(Schema),
});
const onSubmit = handleSubmit(async (v) => {
await create.mutateAsync(v);
});
return (
<form onSubmit={onSubmit} className="space-y-3">
<Input placeholder="公司名" {...register("company_name")} />
{errors.company_name && <p className="text-red-500">{errors.company_name.message}</p>}
<Input placeholder="行业" {...register("industry")} />
<Input type="number" placeholder="评分" {...register("score")} />
<Button type="submit" disabled={create.isPending}>
{create.isPending ? "保存中…" : "保存"}
</Button>
</form>
);
}5.3 状态:zustand(仅在确实需要时)
优先:服务端状态 → TanStack Query 缓存搞定;URL 状态 → 路由 search params;表单 → react-hook-form。 真正需要 zustand 的场景:跨页面/跨组件的客户端状态(侧边栏开关、主题、临时编辑草稿)。
ts
import { create } from "zustand";
type UIStore = {
sidebarOpen: boolean;
toggleSidebar: () => void;
};
export const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));六、配置(环境变量)
.env(本地,不 commit):
ini
VITE_API_BASE_URL=http://localhost:8000Vite 约定:所有
VITE_前缀 的变量才会被前端读到。绝不要把后端 secret(如 OPENAI_API_KEY)放到 VITE_*。
七、测试
vitest.config.ts:
ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
globals: true,
},
});跑测试:
bash
pnpm vitest -- --run八、Makefile 片段(与后端 Makefile 配合)
makefile
web-install:
pnpm install
web-dev:
pnpm dev
web-build:
pnpm build
web-test:
pnpm vitest --run
web-lint:
pnpm eslint . && pnpm prettier --check .九、AI 协同最佳实践
9.1 在 CLAUDE.md / AGENTS.md 写清栈
markdown
## 前端栈
- React 19 + Vite + TypeScript(strict)
- Tailwind v4 + shadcn/ui
- 数据:TanStack Query(不要用 axios / SWR)
- 路由:TanStack Router(不要用 react-router-dom)
- 表单:react-hook-form + zod
- 状态:能不用就不用;非要用就 zustand
## 严禁
- any 类型(除非显式注释为何允许)
- 直接 fetch(用 lib/http.ts 封装)
- 在 components/ui 里写业务
## 设计
- 严格遵守 ./DESIGN.md
- 颜色 / 间距 / 圆角必须用 token9.2 公司专属 Skill
yaml
---
name: qdy-react
description: 当用户在 React + Vite 项目里要求"加一个页面"、"加一个表单"、"对接 API"时启用
---
# QDY 前端标准做法
## 加一个新页面
1. 在 src/routes/ 创建对应路由
2. 在 src/features/{domain}/ 写 api.ts + schema.ts
3. 写组件,组件内用 useXxx() hook 拉数据
4. UI 严格用 shadcn/ui + Tailwind tokens
5. 表单用 react-hook-form + zod
## 禁止
- 用 axios
- 用 react-router-dom
- 在组件里手写 fetch十、未来股东 · 第一周必练
- [ ] 用 Vite + React 19 + Tailwind v4 起一个项目
- [ ] 装好 shadcn/ui,至少加一个 Button + Input
- [ ] 装好 TanStack Query,写一个 useXxx hook 拉后端数据
- [ ] 用 react-hook-form + zod 写一个能提交的表单
- [ ] 配
.env+VITE_API_BASE_URL对接 01-后端-FastAPI 的本地后端
十一、参考资料
- 📚 Vite 官方
- 📚 React 19 docs
- 📚 shadcn/ui
- 📚 TanStack Router
- 📚 TanStack Query
- 📚 Tailwind v4
- 📚 zod
- 📚 vite-react-tanstack-tailwind-shadcn-starter