1. 为什么是 FastAPI + React:这个组合到底解决了什么问题
先说一个我自己的真实场景。去年我要给团队内部做一个数据看板工具,需求不复杂:前端展示几个图表,后端对接数据库返回聚合结果,用户登录后各看各的报表。按理说随便找个模板引擎渲染一下也够用,但后来需求越加越多:要支持不同角色的权限控制、要对接第三方登录、要给外部系统开放接口,原本"渲染页面"的那点逻辑完全不够用了。最后我决定彻底切到前后端分离架构,后端选了 FastAPI,前端选了 React。跑通之后我才真正体会到,这个组合解决的不是"某个页面怎么写"的问题,而是整个团队协作方式和项目维护成本的问题。
前后端分离的核心思想,简单说就是后端只用 JSON 说话,前端只管页面渲染。后端不再关心用户浏览器里长什么样,它只负责把数据算好、返回出去;前端也不再拼模板字符串,它专注做交互和展示。这样一来,后端接口可以被 Web 页面、小程序、App、自动化脚本任意调用,前端也可以用最快的框架去构建交互体验,两边只要把 API 约定好,就能并行开发互不阻塞。FastAPI 是这个分工里后端侧非常顺手的一个选择,React 则是前端侧生态成熟度最高的选项之一,两者拼在一起,就是"现代全栈"的一种标准打开方式。
这套组合到底适合谁?我觉得有三类人最值得看:一是已经会 Python 但想补前端能力的开发者,FastAPI 几乎零学习门槛,你只需要知道 Python 语法就能写接口;二是团队里本来用 Flask/Django 写模板页面的朋友,想升级到前后端分离但不知道从哪里下手;三是正在做课程设计、毕设或者个人作品集的同学,前后端分离项目的完整度在面试和展示中都更有说服力。如果你属于其中之一,这篇文章应该能帮你省掉不少弯路。
2. 开工前的技术选型与环境准备:别在第一步翻车
2.1 为什么是 FastAPI 而不是 Flask 或 Django
很多人问我,用现成的 Flask 或者 Django 不好吗,为什么要换 FastAPI?我的回答是:如果项目很小、逻辑简单,Flask 完全没问题;如果项目有复杂的 Admin 后台和 ORM 体系,Django 也很成熟。但 FastAPI 的定位刚好卡在中间——它既有 Flask 的轻量灵活,又具备 Django 没有的现代特性。
| 对比维度 | FastAPI | Flask | Django |
|---|---|---|---|
| 性能 | 基于 Starlette 和 Uvicorn,异步原生 | 同步为主,高并发场景偏弱 | 同步为主,重框架开销大 |
| 数据校验 | Pydantic 自动校验,定义即校验 | 手写校验逻辑 | 依赖 Django Form/DRF Serializer |
| API 文档 | 自动生成 Swagger 和 ReDoc | 需额外接入 flasgger | 需额外配置 drf-yasg |
| 类型提示 | 原生支持,写接口像写函数签名 | 基本不涉及 | 部分支持 |
| 学习成本 | 低,通读文档一天就能上手 | 极低 | 高,自带体系多 |
我最看重的是 FastAPI 的类型提示驱动的开发方式。你定义一个 Pydantic 模型,就等于同时定义了请求体结构、响应体结构、数据校验规则和 API 文档,一份代码四份用途。这在前后端分离的项目里非常关键,因为前后端需要靠接口文档对齐,FastAPI 自动生成的 Swagger 页面直接扔给前端同事,他连 Postman 都不用配,就能看到每个字段的类型、是否必填、示例值。我在若干项目里试验下来,这种"接口即文档"的模式省掉了大量口头沟通和对字段的扯皮。
2.2 用 uv 管理 Python 虚拟环境,绕开安装怪坑
环境准备这件事,看着不起眼,却是我见过翻车最多的地方。很多人一上来就在全局环境里pip install fastapi,然后过几天被各种包版本冲突折腾到崩溃。我的习惯是用虚拟环境隔离每个项目的依赖,工具方面现在强烈推荐 uv,它的速度比 pip 快一个量级,还自带 Python 版本管理。
用 uv 创建一个 FastAPI 项目的虚拟环境,几步就完成了:
# 安装 uv(macOS / Linux) curl -LsSf https://astral.sh/uv/install.sh | sh # 初始化项目目录并创建虚拟环境 mkdir my-project cd my-project uv venv .venv # 激活虚拟环境(Windows 用 .venv\Scripts\activate) source .venv/bin/activate # 安装 FastAPI 和 Uvicorn uv pip install fastapi "uvicorn[standard]"有个来自真实使用场景的细节:很多人安装 fastapi 失败或者装完启动报错,十有八九是因为 Python 版本不对。FastAPI 要求 Python 3.8 及以上,但我在 PyCharm 里就遇到过选了老版本解释器导致 Pydantic 装不上、代码里一堆红波浪线的情况。解决方式很简单:在 PyCharm 的 Settings -> Project -> Python Interpreter 里,确认解释器路径指向刚才用 uv 创建的.venv,而不是系统自带的 Python。还要留意一下这个虚拟环境里 pip 指向的是不是项目目录,如果指错了,装的东西全进全局环境,项目里照样 import 不到。
2.3 React 前端脚手架:用 Vite 而不是 CRA
前端侧我踩过一个比较疼的坑。以前建 React 项目我习惯用 create-react-app,但这个脚手架越来越慢,依赖安装动辄几分钟,构建也要等半天。后来新项目我全部切到 Vite,同样是 React,Vite 的冷启动几乎是秒开,热更新也快一个档次。创建方式很简单:
npm create vite@latest frontend -- --template react-ts cd frontend npm install npm run dev这里建议直接选react-ts模板。虽然 TS 会多写一点类型代码,但前后端分离项目里,前端接口层的 TypeScript 类型可以直接对照后端的 Pydantic 模型手动定义,能提前暴露很多字段不匹配的问题,而不是等运行到页面上才报错。好的全栈项目,前后端之间应该有"类型默契",TypeScript 就是前端侧守住这条线的工具。
2.4 项目目录结构:别把前后端揉成一坨
目录组织直接决定项目维护体验。我踩过一次把前后端代码放在同一个目录下、dependencies 互相引用的坑之后,现在统一采用这样的结构:
my-project/ ├── backend/ │ ├── app/ │ │ ├── __init__.py │ │ ├── main.py # FastAPI 入口 │ │ ├── config.py # 配置读取 │ │ ├── models.py # Pydantic / ORM 模型 │ │ ├── routers/ # 按业务模块拆分的路由 │ │ ├── schemas.py # 请求响应数据结构 │ │ └── database.py # 数据库连接 │ ├── .env │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── components/ # 通用组件 │ │ ├── pages/ # 页面级组件 │ │ ├── stores/ # 全局状态 │ │ └── App.tsx │ ├── package.json │ └── vite.config.ts └── README.md前后端分属两个独立目录,各自拥有独立的依赖和启动方式,只在接口层面做约定。这样做的最大好处是:后端可以单独测试、单独部署,前端也可以单独跑起来用 mock 数据开发,谁都不需要等谁。
3. 后端先行:用 FastAPI 搭建可扩展的 API 骨架
3.1 配置文件:别再硬编码连接串了
我在真实项目里见过太多人把数据库地址、密钥、第三方接口的 token 直接写在代码里,一旦换环境就要改代码重新部署,非常痛苦。FastAPI 项目里最标准的做法是用pydantic-settings读取配置文件。
# backend/app/config.py from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") app_name: str = "FastAPI React Demo" debug: bool = False database_url: str = "sqlite:///./test.db" secret_key: str = "change-me" cors_origins: list[str] = ["http://localhost:5173"] settings = Settings()然后在.env文件里写实际值:
APP_NAME="My Project" DEBUG=true DATABASE_URL=postgresql://user:password@localhost:5432/mydb SECRET_KEY=your-secret-key CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173"]在 main.py 里直接from app.config import settings就能全局使用,代码里不出现任何环境相关的硬编码。这个习惯越早养成越好,后面部署到 Linux 服务器时只需要在服务器上配一份.env,代码不用动任何一行。我第一次在服务器上部署时因为没做配置分离,被迫临时改了代码里的数据库地址,重新构建之后才发现SECRET_KEY也写死在代码里,简直想抽自己。
3.2 定义数据模型:一份代码,四份用途
接下来定义一个简单的业务模型,用待办事项(Todo)项目做例子。FastAPI 配合 Pydantic,接口的定义非常直观:
# backend/app/schemas.py from pydantic import BaseModel class TodoCreate(BaseModel): title: str description: str | None = None class Todo(TodoCreate): id: int completed: bool = False class Config: from_attributes = True# backend/app/main.py from fastapi import FastAPI from app.config import settings from app.schemas import Todo, TodoCreate app = FastAPI(title=settings.app_name) # 用一个内存列表模拟数据库,实际开发换成 SQLAlchemy 连真实数据库 fake_db = [] current_id = 0 @app.get("/api/todos", response_model=list[Todo]) def list_todos(): return fake_db @app.post("/api/todos", response_model=Todo, status_code=201) def create_todo(payload: TodoCreate): global current_id current_id += 1 todo = Todo(id=current_id, **payload.model_dump()) fake_db.append(todo) return todo启动服务:
uvicorn app.main:app --reload --port 8000启动之后打开http://localhost:8000/docs,你会看到 Swagger 页面已经把两个接口列出来了,还能直接在页面上测试请求。这就是前面说的自动生成 API 文档的威力,前端同事只要能打开这个地址,就能搞清楚接口怎么调。
我在实际项目里习惯把路由按业务模块拆分到routers/目录下,比如routers/todos.py、routers/auth.py,然后通过app.include_router()挂载。项目一旦超过两三个模块,全部写在 main.py 里就没法维护了,拆分的时机不要等,从一开始就按模块建文件,后面扩展起来会舒服很多。
3.3 数据库会话与依赖注入:连接别随手关
真实项目肯定不能用内存列表,至少要接一个 SQLite 或 PostgreSQL。FastAPI 搭配 SQLAlchemy 是常见组合。这里我不展开完整 ORM 配置,重点说一个非常容易被忽略的设计:数据库会话的创建和关闭应该通过 FastAPI 的依赖注入来处理。
# backend/app/database.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session DATABASE_URL = "sqlite:///./todos.db" engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close()然后在路由函数里这样用:
@router.get("/api/todos", response_model=list[Todo]) def list_todos(db: Session = Depends(get_db)): return db.query(TodoModel).all()这段代码的精妙之处在于:你不需要在每个接口里手动db.close(),FastAPI 的依赖注入系统会在请求结束后自动执行get_db中finally块里的清理逻辑。这样做不仅避免连接泄漏,也让代码非常干净。我第一次写时不懂这个模式,每个函数里都复制粘贴一遍获取会话和关闭会话的代码,改起来头大,后来统一改成依赖注入,整体代码量少了一半。
4. 前后端联调的拦路虎:CORS 跨域问题从报错到解决
4.1 跨域是怎么发生的,为什么浏览器要拦你
前后端分离后遇到的第一个拦路虎,几乎百分之百是 CORS。你在前端npm run dev启动在http://localhost:5173,后端跑在http://localhost:8000,浏览器打开页面后,JavaScript 去请求后端的接口,控制台就会冒出一片红,报错信息大概长这样:
Access to fetch at 'http://localhost:8000/api/todos' from origin 'http://localhost:5173' has been blocked by CORS policy这个报错其实是浏览器的安全机制在起作用,它的逻辑是:不同源(协议、域名、端口任一不同)之间的请求,默认是不被信任的。浏览器本质上是帮用户把关,防止某个网站偷偷去请求其他网站的数据。但前后端分离的开发模式下,前端和后端天然就是不同源的,就会撞上这堵墙。
4.2 CORSMiddleware 的基本配置
FastAPI 解决跨域有专门的中间件,代码量非常少:
# backend/app/main.py from fastapi.middleware.cors import CORSMiddleware from app.config import settings app = FastAPI(title=settings.app_name) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )几个参数说明一下:
allow_origins:允许哪些源的请求。开发环境写["http://localhost:5173"],生产环境要改成实际部署的域名。allow_methods:允许哪些 HTTP 方法。["*"]表示全部,一般就这么设。allow_headers:允许哪些请求头。["*"]表示全部,如果前端要传Authorization头,通常设["*"]即可。allow_credentials:是否允许携带 Cookie。如果前端需要带 Cookie 请求,必须设为True。
4.3 我在 CORS 上踩过的三个真实坑
坑一:allow_origins写错了还浑然不知。有一次前端用http://127.0.0.1:5173访问,但我在后端配置里只写了http://localhost:5173,结果一路排查了半天才想起来 Origin 里有127.0.0.1和localhost的区别。建议干脆两个都写上,或者在生产环境用一个域名就只留一个。
坑二:allow_credentials=True时allow_origins不能是*。浏览器对携带凭证的跨域请求要求 Origin 必须精确匹配,不能使用通配符。很多教程里写allow_origins=["*"],你照着配了,结果前端加了credentials: 'include'之后依然报错。所以生产环境要老老实实写具体的 Origin 列表。
坑三:预检请求(OPTIONS)被前端当成报错。当你的请求使用了非简单请求头(比如Authorization)或自定义方法时,浏览器会先发一个OPTIONS请求去探测服务端允不允许。有段时间我的前端同事看到 Network 面板里有OPTIONS /api/todos返回 405,就以为接口挂了,其实只要 CORSMiddleware 配置正确,这个预检请求是会被正常处理的,前端真正关心的还是后面那个实际请求。后来我在文档里明确告诉同事:看到OPTIONS成功返回 200,说明预检通过,不用管它。
解决 CORS 还有一个偷懒方案:在 Vite 的配置文件里设置代理,让前端开发服务器把/api开头的请求转发到后端,这样浏览器看来请求始终是同源的,CORS 问题直接消失:
// vite.config.ts export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, }, }, }, });如果你用了代理,后端其实可以不配 CORSMiddleware。但这个方案只对开发环境有效,生产环境还是要靠后端正确配置跨域,或者用同一个域名部署前后端。我在团队里的习惯是:开发环境用 Vite 代理避开跨域,生产环境用 Nginx 做同域部署,后端 CORS 作为兜底配置保留,三层下来基本不会出问题。
5. 前端对接:Axios 封装、状态管理与接口层的设计
5.1 封装请求层:别在每个组件里裸写 fetch
前端对接后端,最忌讳的就是每个页面组件里直接fetch请求,随便写写 URL,不看错误,不处理加载状态。项目大了之后,接口地址散落各处,改一个 baseURL 要全局搜索,非常崩溃。我的做法是封装一个统一的请求模块,以 Axios 为例:
// frontend/src/api/client.ts import axios from 'axios'; const client = axios.create({ baseURL: '/api', timeout: 10000, }); // 请求拦截器:自动带上 token client.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器:统一处理错误 client.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { // token 过期,跳转登录页 window.location.href = '/login'; } return Promise.reject(error); } ); export default client;这样每个业务模块的请求函数就非常干净:
// frontend/src/api/todos.ts import client from './client'; import type { Todo, TodoCreate } from '../types/todo'; export const getTodos = () => client.get<Todo[]>('/todos'); export const createTodo = (data: TodoCreate) => client.post<Todo>('/todos', data);baseURL: '/api'配合 Vite 代理,就是我在上一节提到的开发方案。代码里所有请求都写在api/目录下,前端同事看这个目录就能知道项目一共调了哪些接口,后端接口变动时也能快速定位需要改动的位置。
5.2 用 Zustand 管理前端全局状态
React 项目里全局状态管理,很多人第一反应是 Redux。但我的体验是:Redux 的样板代码太多,对一个轻量全栈项目纯属负担。我更推荐 Zustand,它的 API 设计极其简洁,用起来几乎没有学习成本。
// frontend/src/stores/useAuthStore.ts import { create } from 'zustand'; interface AuthState { user: { username: string } | null; token: string | null; login: (token: string, user: { username: string }) => void; logout: () => void; } export const useAuthStore = create<AuthState>((set) => ({ user: null, token: null, login: (token, user) => set({ token, user }), logout: () => set({ token: null, user: null }), }));在组件里使用:
import { useAuthStore } from '../stores/useAuthStore'; function Header() { const user = useAuthStore((state) => state.user); const logout = useAuthStore((state) => state.logout); return ( <div> {user ? ( <span>{user.username} <button onClick={logout}>退出</button></span> ) : ( <span>未登录</span> )} </div> ); }我用 Zustand 最大的感受是,它没有把 React 的状态管理复杂化。不需要 Provider 包裹,不需要高阶组件,一个create函数就完事,特别适合中小型全栈项目。
5.3 服务端请求状态:用 TanStack Query 管起来
除了客户端自己的状态,前后端分离项目里还有一类"服务端状态"——比如列表数据、详情数据,这些是从后端读取的。早期我习惯把这些数据也放进全局 store 里管理,后来发现这带来无尽的同步问题:另一个组件改了数据,列表不知道要不要更新,缓存怎么失效等等。
TanStack Query(以前叫 React Query)专门解决这个问题。它帮你处理请求的加载态、错误态、缓存和自动重新拉取,写起来也很直接:
// frontend/src/features/TodoList.tsx import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getTodos, createTodo } from '../api/todos'; function TodoList() { const queryClient = useQueryClient(); const { data: todos, isLoading } = useQuery({ queryKey: ['todos'], queryFn: getTodos, }); const createMutation = useMutation({ mutationFn: createTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); if (isLoading) return <div>加载中...</div>; return ( <div> {todos?.map((todo) => <div key={todo.id}>{todo.title}</div>)} <button onClick={() => createMutation.mutate({ title: '新任务' })} > 添加 </button> </div> ); }看到这段代码的妙处了吗:invalidateQueries会在创建成功后自动让getTodos重新拉取,不需要手动刷新页面或管理数据更新逻辑。在真实项目里,这种模式省掉了大量"改完数据就刷新列表"的低级代码。
6. 联调、调试与部署:从本地跑通到线上可用
6.1 本地联调的标准流程
前后端联调听起来很复杂,其实只要双方都启动在本地,按固定流程走就行。我的习惯是:
- 后端启动
uvicorn app.main:app --reload --port 8000,确认 Swagger 可访问。 - 前端启动
npm run dev,确认页面能打开。 - 先用 Vite 代理配置好
/api转发,浏览器里打开页面,按 F12 看 Network 面板,逐个接口验证。 - 后端加日志或断点,前端在 DevTools 的 Console 和 Network 里对照请求参数和响应体。
- 遇到字段对不上,直接在 Swagger 页面看后端实际返回;遇到类型错误,用 TypeScript 的报错信息对照 Pydantic 模型。
这里有一个非常实用的调试技巧:看接口问题优先级永远先看 Network 面板,而不是直接去看代码。Network 里能看到请求是否发出、请求头是什么、响应体是什么、状态码是多少,信息比 IDE 里 print 出来的还全。有几次同事说"接口返回有问题",我一打开 Network 就发现请求根本没发出去,是前端拦截器在 header 里莫名其妙加了个空 token,压根跟后端无关。这个经验让我养成了一个习惯:先确认去和后端的数据通路是好的,再谈代码逻辑。
6.2 构建前端产物并用 Nginx 部署
本地联调通过之后,就到了上线环节。前后端分离项目部署的方式很多,我比较推荐用 Nginx 同时托管前端静态文件和反代后端接口,这样最终用户只需要访问一个域名,浏览器不会遇到跨域问题。
先构建前端:
cd frontend npm run build构建完成后会在frontend/dist目录产出静态文件(HTML、JS、CSS)。然后配置 Nginx:
server { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/my-project/frontend/dist; index index.html; # 所有 /api 开头的请求转发到 FastAPI location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 前端路由 history 模式回退 location / { try_files $uri $uri/ /index.html; } }这里有几个坑我反复踩过。第一个是try_files $uri $uri/ /index.html这一行,如果前端使用了 React Router 的 BrowserRouter(即 history 模式),必须加上这一行,否则用户直接访问https://your-domain.com/dashboard这个路径时,Nginx 会返回 404,因为服务器上确实没有dashboard这个物理文件。加了这行之后,所有没有对应物理文件的路径都会回退到index.html,由 React 路由接管。
第二个坑是proxy_pass后面的路径拼接。location /api/配合proxy_pass http://127.0.0.1:8000/api/,会把请求原样转发;如果proxy_pass写成了http://127.0.0.1:8000(后面没有/api/),Nginx 会把整个/api/xxx路径拼到后端地址后面,CPU 想半天才知道转发错了。我自己因为这个细节排查过一个晚上,最后 curl 后端接口发现直接访问 8000 端口是通的,走 Nginx 就 404,才意识到是路径配置的问题。
6.3 后端进程管理:别只用裸的 uvicorn
生产环境用uvicorn app.main:app --host 0.0.0.0 --port 8000直接跑虽然能工作,但终端一关进程就没了,服务器重启也不会自动拉起。我的做法是配合 systemd 把 FastAPI 服务设为系统服务。
# /etc/systemd/system/my-fastapi.service [Unit] Description=FastAPI Application After=network.target [Service] User=www-data WorkingDirectory=/var/www/my-project/backend ExecStart=/var/www/my-project/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restart=always RestartSec=3 EnvironmentFile=/var/www/my-project/backend/.env [Install] WantedBy=multi-user.target然后执行:
sudo systemctl daemon-reload sudo systemctl enable my-fastapi.service sudo systemctl start my-fastapi.service这样进程崩溃了会自动重启,服务器重启了服务会自己拉起来。EnvironmentFile指定了环境变量文件,保证.env里的配置能被正确加载。注意ExecStart里我用了虚拟环境中的 uvicorn 绝对路径,这样做是为了避开系统里装的其他 Python 包的影响。
还有一个小建议:生产环境可以给 uvicorn 加几个 workers 提升并发处理能力,比如--workers 4。但要注意,如果你的代码里有依赖内存存储的数据(比如我那节示例里的fake_db),多 worker 会导致数据不共享,每个 worker 各存一份。生产环境千万别用内存存储,数据库一定要独立出来,这是多进程部署的基本前提。
6.4 上线后容易忽略的几个细节
部署完成后,我吃过几次亏之后总结了一套上线自检清单,每一条都是用时间换来的教训:
- 环境变量要单独管理。
.env文件不要提交到 Git 仓库,尤其是包含密钥、数据库密码、第三方 token 的。在服务器上单独创建并给予正确的文件权限。 - SECRET_KEY 要换掉默认值。很多人上线后忘了改默认密钥,被人拿到 JWT 签名的 key 之后可以直接伪造 token。我的习惯是一部署就生成一个新的随机串。
- 看日志是最快的排障入口。前端报错先看 Network,后端报错先看 journalctl。systemd 服务的日志可以通过
journalctl -u my-fastapi.service -f实时查看。 - 数据库备份要提前想好。PostgreSQL 可以用
pg_dump做定时备份,SQLite 直接复制文件也行,但前提是你记得有这个事。我在一次误删数据后就把备份脚本写进了 crontab,隔天跑一次。 - HTTPS 不能忽略。如果部署在公网,建议尽早用 certbot 配好证书,浏览器地址栏不带锁的网站在现在的用户眼里基本等于"不安全"三个字。
我在实际部署中还发现一个比较隐蔽的问题:前端项目里如果用了路由的basename或者接口的baseURL,一旦部署路径不是根路径(比如部署在https://domain.com/my-app/),就要额外考虑静态资源引用地址和 API 路径前缀的问题。最好的办法是部署前就在前端代码里用环境变量控制路径,而不是硬编码/。
另外,当部署的服务器上想再塞进第二个前端项目时,Nginx 配置就需要在server块里多写几个location,或者干脆用不同的 server_name 区分。我早年在一个服务器上同时部署了三个 Web 项目,全是靠 server_name 分开的,Nginx 配置里互相不干扰,管理起来也清楚。多个项目共用一个服务器时,建议每个项目一个 server 块,端口都用 80,域名或子域名区分,别把它们串在同一个 location 规则里,否则改一处影响全局,会非常痛苦。
7. 写在最后:一个小技巧和一句真心话
所有东西跑通之后,我发现一个让全栈开发体验提升不少的小习惯:把后端的 Swagger 地址和前端的本地开发地址固定在 README 里,每次新同事加入项目或者隔一段时间重新打开这个项目,照着 README 两分钟内就能把所有服务跑起来,不用到处问"这个项目怎么启动"。别小看这件事,项目搁置三周之后,你自己也会忘掉的。
还有一个我经常用的调试技巧:如果前端页面迟迟看不到数据,别急着翻代码,可以直接在浏览器地址栏里输一遍后端接口地址(比如http://localhost:8000/api/todos),看直接访问返回什么。这一步能立刻区分问题是出在前端还是后端——如果后端能返回 JSON,说明接口是通的,问题一定在前端的请求封装或代理配置上;如果连直接访问都报错,那就安心去改后端代码吧。这个排查思路无数次帮我快速定位问题,比一头扎进代码里盲猜高效太多。
FastAPI 和 React 的组合,在前后端分离的大趋势下已经是一套相当成熟的生产级方案。它没有很玄乎的设计,没有复杂的魔法,每一样技术都能看透、学透、用透。我写这篇东西,不是想告诉你"就该这么干",而是把我在真实项目中撞过的墙、绕过的路、总结出来的做法原原本本晒出来。如果你正在做一个全新项目,或者正在考虑从传统模板渲染切到前后端分离架构,希望这套实践路线能帮你少走一段我在黑暗中走过的路。全栈的路从来不短,但每踩过一个坑,前面的路就会亮一点。