324 lines
7.7 KiB
Markdown
324 lines
7.7 KiB
Markdown
# API 路径配置说明
|
||
|
||
## 📋 概述
|
||
|
||
本项目使用统一的 API 路径配置机制,通过环境变量和辅助函数确保开发环境和生产环境的 API 调用一致性。
|
||
|
||
---
|
||
|
||
## 🔧 配置方式
|
||
|
||
### 1. **环境变量配置**
|
||
|
||
#### **开发环境** (`.env`)
|
||
```env
|
||
REACT_APP_API_BASE_URL=http://localhost:5001
|
||
```
|
||
|
||
#### **生产环境** (`.env.production`)
|
||
```env
|
||
REACT_APP_API_BASE_URL=https://d1kt.cn/api
|
||
```
|
||
|
||
### 2. **代码中的使用**
|
||
|
||
所有 API 调用统一使用 `api` 对象(来自 `src/api/apiClient.ts`):
|
||
|
||
```typescript
|
||
import { api } from './api/apiClient';
|
||
|
||
// ✅ 正确用法
|
||
const users = await api.get<User[]>('/admin/users');
|
||
|
||
// ❌ 不要直接使用 apiClient
|
||
// const response = await apiClient.get('/admin/users');
|
||
```
|
||
|
||
---
|
||
|
||
## 🌐 URL 构造流程
|
||
|
||
### **开发环境**
|
||
|
||
```
|
||
1. 代码调用:api.get('/admin/users')
|
||
↓
|
||
2. getFullApiPath('/admin/users')
|
||
→ '/api/admin/users'
|
||
↓
|
||
3. axios.get(API_BASE_URL + '/api/admin/users')
|
||
→ axios.get('http://localhost:5001/api/admin/users')
|
||
↓
|
||
4. 最终请求:http://localhost:5001/api/admin/users
|
||
↓
|
||
5. 后端接收:/api/admin/users ✅
|
||
```
|
||
|
||
### **生产环境**
|
||
|
||
```
|
||
1. 代码调用:api.get('/admin/users')
|
||
↓
|
||
2. getFullApiPath('/admin/users')
|
||
→ '/api/admin/users'
|
||
↓
|
||
3. axios.get(API_BASE_URL + '/api/admin/users')
|
||
→ axios.get('https://d1kt.cn/api/api/admin/users')
|
||
↓
|
||
4. 浏览器请求:https://d1kt.cn/api/api/admin/users
|
||
↓
|
||
5. Nginx 处理:
|
||
location /api/ {
|
||
proxy_pass http://localhost:5001/;
|
||
}
|
||
→ 去掉第一个 /api,转发到后端
|
||
↓
|
||
6. 后端接收:http://localhost:5001/api/admin/users ✅
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 关键点说明
|
||
|
||
### ✅ **为什么生产环境需要两个 `/api`?**
|
||
|
||
1. **第一个 `/api`**:Nginx 反向代理的路径匹配
|
||
```nginx
|
||
location /api/ {
|
||
proxy_pass http://localhost:5001/;
|
||
}
|
||
```
|
||
- 浏览器请求 `https://d1kt.cn/api/xxx`
|
||
- Nginx 匹配到 `/api/` 后,去掉这部分,转发到后端
|
||
- 后端收到 `http://localhost:5001/xxx`
|
||
|
||
2. **第二个 `/api`**:后端路由的前缀
|
||
- 后端所有路由都以 `/api` 开头
|
||
- 例如:`app.use('/api/admin', adminRoutes)`
|
||
- 所以需要请求 `/api/admin/users`
|
||
|
||
3. **组合结果**:
|
||
```
|
||
浏览器 → https://d1kt.cn/api/api/admin/users
|
||
Nginx → 转发 http://localhost:5001/api/admin/users
|
||
后端 → 路由匹配 /api/admin/users ✅
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 统一的调用方式
|
||
|
||
### **所有页面都使用相同的逻辑**
|
||
|
||
| 功能模块 | API 调用示例 | 最终 URL(开发) | 最终 URL(生产) |
|
||
|---------|------------|---------------|---------------|
|
||
| 打字练习 | `api.get('/practice-records/statistics')` | `http://localhost:5001/api/practice-records/statistics` | `https://d1kt.cn/api/api/practice-records/statistics` |
|
||
| 用户管理 | `api.get('/admin/users')` | `http://localhost:5001/api/admin/users` | `https://d1kt.cn/api/api/admin/users` |
|
||
| 词汇学习 | `api.get('/vocabulary/word-sets')` | `http://localhost:5001/api/vocabulary/word-sets` | `https://d1kt.cn/api/api/vocabulary/word-sets` |
|
||
| 扫雷游戏 | `api.get('/minesweeper/leaderboard/beginner')` | `http://localhost:5001/api/minesweeper/leaderboard/beginner` | `https://d1kt.cn/api/api/minesweeper/leaderboard/beginner` |
|
||
|
||
---
|
||
|
||
## 🚀 部署步骤
|
||
|
||
### **生产环境部署**
|
||
|
||
1. **在服务器上创建 `.env.production` 文件**
|
||
```bash
|
||
cd /path/to/typing_practiceweb
|
||
cp .env.production.example .env.production
|
||
nano .env.production
|
||
```
|
||
|
||
2. **配置生产环境变量**
|
||
```env
|
||
REACT_APP_API_BASE_URL=https://d1kt.cn/api
|
||
```
|
||
|
||
3. **构建前端**
|
||
```bash
|
||
npm run build
|
||
```
|
||
这会自动读取 `.env.production` 文件
|
||
|
||
4. **确认 Nginx 配置**
|
||
```nginx
|
||
location /api/ {
|
||
proxy_pass http://localhost:5001/;
|
||
}
|
||
```
|
||
|
||
5. **重启服务**
|
||
```bash
|
||
sudo nginx -s reload
|
||
pm2 restart typing-backend
|
||
```
|
||
|
||
---
|
||
|
||
## ⚠️ 常见错误
|
||
|
||
### ❌ **错误配置示例**
|
||
|
||
```env
|
||
# ❌ 生产环境配置为空字符串
|
||
REACT_APP_API_BASE_URL=
|
||
|
||
# 结果:请求变成 /api/admin/users(相对路径)
|
||
# 问题:Nginx 无法正确代理
|
||
```
|
||
|
||
```env
|
||
# ❌ 开发环境包含 /api
|
||
REACT_APP_API_BASE_URL=http://localhost:5001/api
|
||
|
||
# 结果:http://localhost:5001/api/api/admin/users
|
||
# 问题:多了一层 /api,后端无法匹配路由
|
||
```
|
||
|
||
### ✅ **正确配置**
|
||
|
||
```env
|
||
# ✅ 开发环境
|
||
REACT_APP_API_BASE_URL=http://localhost:5001
|
||
|
||
# ✅ 生产环境
|
||
REACT_APP_API_BASE_URL=https://d1kt.cn/api
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 验证方法
|
||
|
||
### **开发环境验证**
|
||
```bash
|
||
# 1. 启动服务
|
||
npm run start
|
||
|
||
# 2. 浏览器访问
|
||
http://localhost:3001/admin
|
||
|
||
# 3. 打开 Network 面板,查看请求
|
||
# 应该看到:http://localhost:5001/api/admin/users ✅
|
||
```
|
||
|
||
### **生产环境验证**
|
||
```bash
|
||
# 1. 构建并部署
|
||
npm run build
|
||
pm2 restart typing-backend
|
||
|
||
# 2. 浏览器访问
|
||
https://d1kt.cn/admin
|
||
|
||
# 3. 打开 Network 面板,查看请求
|
||
# 应该看到:https://d1kt.cn/api/api/admin/users ✅
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 总结
|
||
|
||
- ✅ **统一使用 `api` 对象**进行所有 API 调用
|
||
- ✅ **通过环境变量**控制不同环境的基础 URL
|
||
- ✅ **`getFullApiPath` 函数**自动添加 `/api` 前缀
|
||
- ✅ **生产环境需要两个 `/api`**:第一个给 Nginx,第二个给后端路由
|
||
- ✅ **开发环境和生产环境使用完全相同的代码逻辑**
|
||
|
||
---
|
||
|
||
## 🌐 WebSocket 连接配置
|
||
|
||
### **WebSocket 路径配置**
|
||
|
||
扫雷游戏的 WebSocket 连接需要正确处理路径前缀,以匹配 Nginx 代理配置:
|
||
|
||
#### **客户端配置**
|
||
- `REACT_APP_API_BASE_URL`: API 基础地址(如 `/api` 或 `https://d1kt.cn/api`)
|
||
- `REACT_APP_CLIENT_URL`: 客户端域名(如 `https://d1kt.cn`)
|
||
|
||
#### **服务器端配置**
|
||
- `REACT_APP_API_BASE_URL`: 与客户端相同的环境变量,用于确定 WebSocket 路径前缀
|
||
|
||
### **配置示例**
|
||
|
||
#### **开发环境** (`.env`)
|
||
```env
|
||
REACT_APP_API_BASE_URL=http://localhost:5001
|
||
REACT_APP_CLIENT_URL=http://localhost:3001
|
||
```
|
||
|
||
#### **生产环境** (`.env.production`)
|
||
```env
|
||
REACT_APP_API_BASE_URL=/api
|
||
REACT_APP_CLIENT_URL=https://d1kt.cn
|
||
```
|
||
|
||
或使用完整 URL:
|
||
```env
|
||
REACT_APP_API_BASE_URL=https://d1kt.cn/api
|
||
REACT_APP_CLIENT_URL=https://d1kt.cn
|
||
```
|
||
|
||
### **WebSocket 连接流程**
|
||
|
||
```
|
||
开发环境:
|
||
1. 客户端连接: ws://localhost:5001/socket.io
|
||
2. 服务器监听: /socket.io
|
||
|
||
生产环境:
|
||
1. 客户端连接: wss://d1kt.cn/api/socket.io
|
||
2. Nginx 代理: location /api/ { proxy_pass http://localhost:5001/; }
|
||
3. 服务器监听: /socket.io
|
||
```
|
||
|
||
### **Nginx WebSocket 代理配置**
|
||
|
||
确保 Nginx 配置包含 WebSocket 代理头:
|
||
|
||
```nginx
|
||
location /api/ {
|
||
proxy_pass http://localhost:5001/;
|
||
proxy_http_version 1.1;
|
||
|
||
# WebSocket 代理头
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header Host $host;
|
||
|
||
# 超时设置
|
||
proxy_read_timeout 86400;
|
||
proxy_send_timeout 86400;
|
||
}
|
||
```
|
||
|
||
### **故障排查**
|
||
|
||
#### **WebSocket 连接失败**
|
||
1. **检查 Nginx 配置**:确认包含 `Upgrade` 和 `Connection` 头
|
||
2. **检查服务器日志**:查看 Socket.IO 服务器是否启动
|
||
3. **验证环境变量**:确保 `REACT_APP_API_BASE_URL` 正确设置
|
||
|
||
#### **路径不匹配**
|
||
- 客户端日志显示连接地址:`wss://d1kt.cn/api/socket.io`
|
||
- 服务器日志显示连接日志:`用户连接: [socket-id]`
|
||
|
||
#### **生产环境验证命令**
|
||
```bash
|
||
# 测试 HTTP 握手
|
||
curl "https://d1kt.cn/api/socket.io/?EIO=4&transport=polling"
|
||
|
||
# 预期响应:
|
||
# 0{"sid":"xxx","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}
|
||
```
|
||
|
||
### **已完成的修复**
|
||
1. ✅ 客户端 WebSocket 路径配置 (`getWebSocketPath()`)
|
||
2. ✅ 服务器端 Socket.IO 路径配置 (`getSocketIoPath()`)
|
||
3. ✅ Nginx WebSocket 代理配置示例
|
||
|
||
---
|
||
|
||
*最后更新:2025-10-30*
|