421 lines
8.8 KiB
Markdown
421 lines
8.8 KiB
Markdown
# React SPA Template
|
||
|
||
这是一个用于后台系统或通用 SPA 项目的前端基础脚手架。它已经接好了以下底座能力:
|
||
|
||
- `Vite + React 19 + TypeScript`
|
||
- `TanStack Router` 文件路由
|
||
- `TanStack Query + ky` 请求与缓存
|
||
- `Zustand` 会话状态基座
|
||
- `Tailwind CSS` 原子化样式
|
||
- `i18next` 多语言
|
||
- `Head / Metadata` 动态标题与元信息
|
||
- `Biome + Husky + commitlint` 代码规范与提交规范
|
||
|
||
这个模板现在已经被“净化”为一个空白 scaffold。
|
||
你拿到它之后,应该在这个基础上替换成自己的页面、接口、登录逻辑和测试,而不是继续沿用示例业务。
|
||
|
||
## 1. 快速开始
|
||
|
||
```bash
|
||
pnpm install
|
||
pnpm dev
|
||
```
|
||
|
||
常用命令:
|
||
|
||
```bash
|
||
pnpm dev
|
||
pnpm build
|
||
pnpm lint
|
||
pnpm lint:fix
|
||
pnpm generate-routes
|
||
pnpm commit
|
||
```
|
||
|
||
## 2. 目录结构
|
||
|
||
核心目录说明:
|
||
|
||
- `src/routes`
|
||
TanStack Router 文件路由目录。你新增页面,优先在这里建文件。
|
||
- `src/lib/api`
|
||
通用请求层。`api-client.ts` 是所有接口调用的基础入口。
|
||
- `src/lib/auth`
|
||
会话初始化、401 清理、refresh token、受保护路由 helper。
|
||
- `src/lib/head`
|
||
页面标题、description、Open Graph、Twitter metadata。
|
||
- `src/lib/query`
|
||
TanStack Query 全局默认配置。
|
||
- `src/store`
|
||
全局状态。目前主要是认证状态。
|
||
- `src/styles.css`
|
||
全局 Tailwind 入口。当前项目只保留这一份样式文件。
|
||
- `src/locales`
|
||
多语言文案。
|
||
|
||
## 3. 新项目落地后先改哪里
|
||
|
||
如果你要基于这个模板开一个新项目,建议按这个顺序改:
|
||
|
||
### 第一步:改首页
|
||
|
||
先改这个文件:
|
||
|
||
- `src/routes/$lang/index.tsx`
|
||
|
||
这是当前默认首页。通常你会:
|
||
|
||
- 改成你的项目欢迎页
|
||
- 或直接改成 dashboard / 工作台入口
|
||
- 或拆成自己的页面布局
|
||
|
||
同时对应修改多语言文案:
|
||
|
||
- `src/locales/zh-CN/common.ts`
|
||
- `src/locales/en-US/common.ts`
|
||
|
||
### 第二步:改接口基础地址
|
||
|
||
先看这些文件:
|
||
|
||
- `.env.development`
|
||
- `.env.production`
|
||
- `src/vite-env.d.ts`
|
||
|
||
至少确认这些变量:
|
||
|
||
- `VITE_APP_ENV`
|
||
- `VITE_API_BASE_URL`
|
||
- `VITE_ENABLE_QUERY_DEVTOOLS`
|
||
- `VITE_ENABLE_REQUEST_LOG`
|
||
|
||
### 第三步:改全局样式入口
|
||
|
||
当前模板已经切到 Tailwind CSS,并移除了旧的 `App.css` / `index.css`。
|
||
|
||
你主要会改:
|
||
|
||
- `src/styles.css`
|
||
|
||
这里通常只放:
|
||
|
||
- `@import "tailwindcss";`
|
||
- 少量全局 base 样式
|
||
- 你的主题变量
|
||
|
||
### 第四步:接你的业务模块
|
||
|
||
建议按“功能模块”建目录,而不是把所有请求、类型和 hooks 混在一起。
|
||
|
||
例如:
|
||
|
||
```text
|
||
src/features/user/api/user-api.ts
|
||
src/features/user/hooks/use-current-user.ts
|
||
src/features/user/types/user.ts
|
||
src/routes/$lang/users/index.tsx
|
||
```
|
||
|
||
### 第五步:接你的登录体系
|
||
|
||
优先改这些文件:
|
||
|
||
- `src/store/auth-store.ts`
|
||
- `src/lib/auth/auth-session.ts`
|
||
- `src/lib/api/api-client.ts`
|
||
|
||
## 4. Head / Metadata 怎么用
|
||
|
||
模板已经提供了一个通用 hook:
|
||
|
||
- `src/lib/head/document-metadata.ts`
|
||
|
||
在页面组件里直接调用:
|
||
|
||
```tsx
|
||
import { useDocumentMetadata } from '@/lib/head/document-metadata'
|
||
|
||
function UserPage() {
|
||
useDocumentMetadata({
|
||
title: '用户管理',
|
||
description: '用户管理页面',
|
||
})
|
||
|
||
return <div>User Page</div>
|
||
}
|
||
```
|
||
|
||
它会自动更新:
|
||
|
||
- `document.title`
|
||
- `meta[name="description"]`
|
||
- `meta[name="robots"]`
|
||
- `og:title`
|
||
- `og:description`
|
||
- `twitter:title`
|
||
- `twitter:description`
|
||
|
||
适用场景:
|
||
|
||
- 列表页标题
|
||
- 详情页标题
|
||
- 登录页 / 404 页
|
||
- 分享卡片基础描述
|
||
|
||
## 5. 登录怎么接
|
||
|
||
这个模板的认证层不是“现成登录系统”,而是“认证骨架”。
|
||
|
||
它已经有:
|
||
|
||
- `accessToken`
|
||
- `refreshToken`
|
||
- `currentUser`
|
||
- `status`
|
||
- `401` 后清会话
|
||
- refresh token 的可插拔入口
|
||
|
||
核心文件:
|
||
|
||
- `src/store/auth-store.ts`
|
||
- `src/lib/auth/auth-session.ts`
|
||
- `src/lib/api/api-client.ts`
|
||
|
||
### 5.1 登录成功后怎么写入会话
|
||
|
||
假设你的登录接口返回:
|
||
|
||
```ts
|
||
{
|
||
accessToken: string
|
||
refreshToken: string
|
||
user: {
|
||
id: string
|
||
name: string
|
||
}
|
||
}
|
||
```
|
||
|
||
那么登录成功后可以这样写:
|
||
|
||
```ts
|
||
import { useAuthStore } from '@/store/auth-store'
|
||
|
||
useAuthStore.getState().startSession({
|
||
accessToken: response.accessToken,
|
||
refreshToken: response.refreshToken,
|
||
currentUser: response.user,
|
||
})
|
||
```
|
||
|
||
### 5.2 刷新 token 怎么接
|
||
|
||
模板里预留了 refresh 的注册入口:
|
||
|
||
```ts
|
||
import { registerRefreshSessionHandler } from '@/lib/auth/auth-session'
|
||
|
||
registerRefreshSessionHandler(async (refreshToken) => {
|
||
const response = await fetch('/auth/refresh', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ refreshToken }),
|
||
}).then((result) => result.json())
|
||
|
||
return {
|
||
accessToken: response.accessToken,
|
||
refreshToken: response.refreshToken,
|
||
currentUser: response.user ?? null,
|
||
}
|
||
})
|
||
```
|
||
|
||
当请求返回 `401` 时,请求层会:
|
||
|
||
1. 尝试执行这个 refresh handler
|
||
2. 如果 refresh 成功,则自动重试原请求一次
|
||
3. 如果 refresh 失败,则清空本地会话
|
||
|
||
### 5.3 当前用户初始化怎么接
|
||
|
||
如果你的项目在刷新页面后,需要根据已有 token 主动请求一次“当前用户信息”,可以注册:
|
||
|
||
```ts
|
||
import { registerCurrentUserInitializer } from '@/lib/auth/auth-session'
|
||
|
||
registerCurrentUserInitializer(async () => {
|
||
const response = await fetch('/auth/me').then((result) => result.json())
|
||
|
||
return response.user
|
||
})
|
||
```
|
||
|
||
这样在应用启动时,模板会在已有 token 且尚未拿到 `currentUser` 的情况下,自动初始化一次用户信息。
|
||
|
||
## 6. 怎么加受保护路由
|
||
|
||
模板已经提供了路由保护 helper:
|
||
|
||
- `src/lib/auth/require-auth.ts`
|
||
|
||
用法是,在路由的 `beforeLoad` 里调用它。
|
||
|
||
示例:
|
||
|
||
```tsx
|
||
import { createFileRoute } from '@tanstack/react-router'
|
||
|
||
import { requireAuthenticatedSession } from '@/lib/auth/require-auth'
|
||
|
||
export const Route = createFileRoute('/$lang/dashboard')({
|
||
beforeLoad: async () => {
|
||
await requireAuthenticatedSession()
|
||
},
|
||
component: DashboardPage,
|
||
})
|
||
|
||
function DashboardPage() {
|
||
return <div>Dashboard</div>
|
||
}
|
||
```
|
||
|
||
它的行为是:
|
||
|
||
- 如果当前已经登录,正常进入页面
|
||
- 如果当前未登录,重定向到 `/$lang`
|
||
|
||
如果你想把未登录用户导向专门的登录页,可以直接修改:
|
||
|
||
- `src/lib/auth/require-auth.ts`
|
||
|
||
把重定向目标从 `/$lang` 改成你的登录页路由,例如 `/$lang/login`。
|
||
|
||
## 7. 怎么写第一个接口模块
|
||
|
||
推荐模式是:
|
||
|
||
1. 在 `src/features/.../types` 定义类型
|
||
2. 在 `src/features/.../api` 写请求函数
|
||
3. 在 `src/features/.../hooks` 写 query / mutation hook
|
||
4. 在 `src/routes/...` 页面里消费
|
||
|
||
示例:
|
||
|
||
```ts
|
||
// src/features/user/api/user-api.ts
|
||
import { api } from '@/lib/api/api-client'
|
||
|
||
export interface CurrentUser {
|
||
id: string
|
||
name: string
|
||
}
|
||
|
||
export function getCurrentUser() {
|
||
return api.get<CurrentUser>('users/me').then((response) => response.data)
|
||
}
|
||
```
|
||
|
||
```ts
|
||
// src/features/user/hooks/use-current-user.ts
|
||
import { useQuery } from '@tanstack/react-query'
|
||
|
||
import { getCurrentUser } from '@/features/user/api/user-api'
|
||
import type { ApiError } from '@/lib/api/api-error'
|
||
|
||
export function useCurrentUserQuery() {
|
||
return useQuery<Awaited<ReturnType<typeof getCurrentUser>>, ApiError>({
|
||
queryKey: ['current-user'],
|
||
queryFn: getCurrentUser,
|
||
})
|
||
}
|
||
```
|
||
|
||
## 8. 路由相关说明
|
||
|
||
这个模板使用 TanStack Router 文件路由。
|
||
|
||
路由目录:
|
||
|
||
- `src/routes`
|
||
|
||
生成文件:
|
||
|
||
- `src/routeTree.gen.ts`
|
||
|
||
你通常不需要手改 `src/routeTree.gen.ts`。
|
||
新增、删除、修改路由文件后,执行:
|
||
|
||
```bash
|
||
pnpm generate-routes
|
||
```
|
||
|
||
`pnpm dev` 和 `pnpm build` 也都会自动先生成一次路由树。
|
||
|
||
## 9. 代码规范和提交流程
|
||
|
||
## 9.1 Tailwind 说明
|
||
|
||
当前模板使用的是官方 Vite 接法:
|
||
|
||
- `tailwindcss`
|
||
- `@tailwindcss/vite`
|
||
|
||
核心文件:
|
||
|
||
- `vite.config.ts`
|
||
- `src/styles.css`
|
||
|
||
新增页面时,优先直接写 Tailwind utility class,而不是再恢复成多个分散的 CSS 文件。
|
||
|
||
格式化与 lint:
|
||
|
||
```bash
|
||
pnpm lint
|
||
pnpm lint:fix
|
||
pnpm format
|
||
```
|
||
|
||
提交规范:
|
||
|
||
```bash
|
||
pnpm commit
|
||
```
|
||
|
||
仓库已经包含:
|
||
|
||
- `Biome`
|
||
- `Husky`
|
||
- `lint-staged`
|
||
- `commitlint`
|
||
- `Commitizen`
|
||
|
||
## 10. 你后续最可能改动的文件
|
||
|
||
如果你刚接手这个模板,优先关注这几个文件:
|
||
|
||
- `src/routes/$lang/index.tsx`
|
||
- `src/routes/$lang/route.tsx`
|
||
- `src/lib/api/api-client.ts`
|
||
- `src/lib/auth/auth-session.ts`
|
||
- `src/lib/auth/require-auth.ts`
|
||
- `src/store/auth-store.ts`
|
||
- `src/lib/head/document-metadata.ts`
|
||
- `src/locales/zh-CN/common.ts`
|
||
- `src/locales/en-US/common.ts`
|
||
|
||
## 11. 当前状态
|
||
|
||
当前模板已经验证通过:
|
||
|
||
- `pnpm lint`
|
||
- `pnpm build`
|
||
|
||
如果你继续往前整理,下一步通常就是:
|
||
|
||
1. 接入真实登录接口
|
||
2. 增加 `login`、`dashboard`、`403` 等实际页面
|
||
3. 按业务模块拆分 `features`
|
||
4. 按项目实际需要补充测试方案
|