先还原一个真实"案发现场"
小张花了一周写完FastAPI接口,用Postman测试全部通过,美滋滋地去睡了个好觉。第二天起床,信心满满地启动Vue项目,npm run dev,然后......控制台一片红 :Access-Control-Allow-Origin 错误。
赶紧上网搜,装了个flask-cors?不对,我是FastAPI。手忙脚乱加上CORSMiddleware,跨域是解决了,但POST请求又报422 Unprocessable Entity------前端传的JSON格式后端不认。
你可能会问:不就是配置个代理、写个接口吗?错!前后端分离的"分离"二字,坑全藏在细节里。下面我把最终稳定运行的结构和配置贴出来,你直接复制粘贴就能跑。
📁 我的"强迫症"项目结构(治好了我的精神内耗)
以前我喜欢把所有文件堆在一个文件夹里,后来发现维护起来像在垃圾堆里找钥匙。现在的结构长这样,按功能拆分明细,但又不至于过度拆分(别学我一开始拆了20个文件夹,结果自己都找不到东西)。
fastapi_vue_project/
├── backend/ # FastAPI后端
│ ├── app/
│ │ ├── api/ # 路由层(按模块分)
│ │ │ ├── v1/
│ │ │ │ ├── users.py
│ │ │ │ └── tasks.py
│ │ ├── core/ # 配置、安全、数据库连接
│ │ │ ├── config.py
│ │ │ └── database.py
│ │ ├── models/ # SQLAlchemy模型
│ │ ├── schemas/ # Pydantic模型(请求/响应结构)
│ │ ├── services/ # 业务逻辑层
│ │ └── main.py # 入口
│ ├── requirements.txt
│ └── .env # 环境变量(别提交到git!)
├── frontend/ # Vue3前端
│ ├── src/
│ │ ├── api/ # 封装axios请求
│ │ ├── views/ # 页面
│ │ ├── router/ # 路由
│ │ └── utils/ # 工具函数
│ └── .env.development # 开发环境变量
│ └── .env.production # 生产环境变量
└── docker-compose.yml # 可选,线上部署用
👆 这个结构我用了很久,大小项目都稳得很。关键是 core/config.py 和 **frontend/.env.***这对"黄金搭档",解决了90%的环境切换问题。
🔌 通信的3个核心配置(少一个都连不上)
1️⃣ FastAPI的CORS中间件(不是加一行就完事的)
很多人复制官方示例allow_origins=["*"]就跑了,但生产环境千万别这么干 !而且你还要注意allow_credentials和allow_headers的配合。
# backend/app/core/config.py
class Settings:
BACKEND_CORS_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"] # 开发环境
# 生产环境从环境变量读取,不要写死
# backend/app/main.py
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS, # 注意!不是 "*"
allow_credentials=True,
allow_methods=["*"],
allow_headers=["Authorization", "Content-Type"],
)
这里有个坑:如果你前端用axios 带上了withCredentials: true,后端allow_origins就不能是["*"],必须指定具体域名。当初我在这里卡了4个小时,最后翻FastAPI源码才找到原因。
2️⃣ Vue的代理配置(开发神器,但别滥用)
Vite(或webpack)的proxy 配置简直是开发阶段的救星,让你彻底告别跨域烦恼。但很多人抄完配置就不管了,结果部署到生产环境又报错。
// frontend/vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8000', // FastAPI默认端口
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') // 注意这个重写规则!
}
}
}
})
注意看rewrite这一行:如果你的FastAPI路由是@app.get("/users"),前端请求/api/users,代理会把/api去掉再转发。这里写错了路径,就会404。
3️⃣ 统一的响应格式(别让前端猜你返回什么)
这是我最想吐槽的一点:很多人后端返回的数据结构每天不一样,今天{"data": {...}},明天{"result": {...}}。前端大哥没被你气死算我输。
我的习惯:统一用下面的格式
# backend/app/schemas/common.py
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional
T = TypeVar('T')
class ResponseModel(BaseModel, Generic[T]):
code: int = 200
message: str = "success"
data: Optional[T] = None
# 使用示例
@router.get("/users/{user_id}")
def get_user(user_id: int):
user = service.get_user(user_id)
return ResponseModel(data=user)
前端axios拦截器统一处理这个结构,代码量直接砍半 。是不是以为这样就完了?不,还有一个关于错误码的约定,建议至少约定401去登录、403无权限、422参数错误,别让前端去猜。
🌍 开发 vs 生产:别再手改baseURL了!
我见过最野的操作:每次部署前,手动把 axios 的 baseURL 从localhost:8000改成线上域名,然后commit,然后......忘了改回来。😱
正确姿势:用环境变量
# frontend/.env.development
VITE_API_BASE_URL = '/api' # 开发走代理
# frontend/.env.production
VITE_API_BASE_URL = 'https://your-api-domain.com'
// frontend/src/api/request.js
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
后端也一样,用python-dotenv加载 .env 文件,永远不要把密钥写死在代码里。
💣 再说三个容易翻车的点(都是真金白银换的教训)
⚠️ 第一个:路径拼接的斜杠
后端路由@app.get("/users"),前端请求/users/(多了一个斜杠),在nginx下可能301重定向,cookie丢了。
建议统一规则:路由末尾不加斜杠,前端请求也不加。
⚠️ 第二个:请求/响应拦截器里的"循环引用"
有人在拦截器里用response.data.data取数据,但刷新token的接口又走了同一个拦截器,结果死循环。
解决方案:在白名单接口的meta里加一个标记跳过拦截器。
⚠️ 第三个:FastAPI的异步陷阱
如果你用了async def,里面却调用同步的SQLAlchemy操作,会阻塞事件循环。
要么全用def,要么用asyncio.to_thread。很多人不知道,上了生产才发现接口越跑越慢。