第六章 前后端联调 —— Vue 调用后台接口
本章目标:解决跨域问题,用 Vue 前端调用 FastAPI 后台接口,实现完整的前后端数据流转。
6.1 跨域问题(CORS)
当你用 Vue 调用后台接口时,你很可能会遇到这个错误:
Access to XMLHttpRequest at 'http://127.0.0.1:8000/todos'
from origin 'http://localhost:5173' has been blocked by CORS policy
什么是跨域
浏览器有一个安全策略:如果前端和后端的地址(域名或端口)不一样,浏览器默认会拦截请求。
| 前端地址 | 后端地址 | 是否跨域 |
|---|---|---|
http://localhost:5173 | http://localhost:8000 | 是(端口不同) |
http://localhost:5173 | http://localhost:5173 | 否(完全相同) |
开发时前端通常跑在 5173 端口,后端跑在 8000 端口,端口不同就是跨域。
解决方法:后端添加 CORS 中间件
在 main.py 中加几行代码即可:
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
这段代码的意思是:允许任何来源的前端访问后端接口。
| 参数 | 含义 |
|---|---|
allow_origins=["*"] | 允许所有来源(开发阶段用 *,上线时应该限制为具体域名) |
allow_methods=["*"] | 允许所有 HTTP 方法(GET、POST、PUT、DELETE) |
allow_headers=["*"] | 允许所有请求头 |
加上这段代码后重启服务,跨域问题就解决了。
更新后的完整 main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import Session, select
from database import create_db_and_tables, engine
from models import Todo, TodoCreate, TodoUpdate
@asynccontextmanager
async def lifespan(app: FastAPI):
create_db_and_tables()
yield
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def index():
return {"message": "Todo 后台服务已启动"}
@app.post("/todos", status_code=201)
def create_todo(todo: TodoCreate):
with Session(engine) as session:
db_todo = Todo.model_validate(todo)
session.add(db_todo)
session.commit()
session.refresh(db_todo)
return db_todo
@app.get("/todos")
def get_todos():
with Session(engine) as session:
todos = session.exec(select(Todo)).all()
return todos
@app.get("/todos/{todo_id}")
def get_todo(todo_id: int):
with Session(engine) as session:
todo = session.get(Todo, todo_id)
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
return todo
@app.put("/todos/{todo_id}")
def update_todo(todo_id: int, todo_update: TodoUpdate):
with Session(engine) as session:
todo = session.get(Todo, todo_id)
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
todo_data = todo_update.model_dump(exclude_unset=True)
todo.sqlmodel_update(todo_data)
session.add(todo)
session.commit()
session.refresh(todo)
return todo
@app.delete("/todos/{todo_id}")
def delete_todo(todo_id: int):
with Session(engine) as session:
todo = session.get(Todo, todo_id)
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
session.delete(todo)
session.commit()
return {"message": "删除成功"}
6.2 Vue 中使用 axios 调用接口
安装 axios
在你的 Vue 项目中运行:
npm install axios
基本用法
axios 是一个 HTTP 请求库,用法非常简单:
import axios from 'axios'
const API = 'http://127.0.0.1:8000'
GET 请求 —— 获取待办列表
async function fetchTodos() {
const response = await axios.get(`${API}/todos`)
console.log(response.data) // [{ id: 1, title: "买牛奶", done: false }, ...]
}
POST 请求 —— 新增待办
async function addTodo(title) {
const response = await axios.post(`${API}/todos`, {
title: title,
done: false
})
console.log(response.data) // { id: 2, title: "写作业", done: false }
}
PUT 请求 —— 修改待办
async function updateTodo(id, done) {
const response = await axios.put(`${API}/todos/${id}`, {
done: done
})
console.log(response.data) // { id: 1, title: "买牛奶", done: true }
}
DELETE 请求 —— 删除待办
async function deleteTodo(id) {
await axios.delete(`${API}/todos/${id}`)
console.log('删除成功')
}
6.3 实战:Todo 待办应用完整联调
下面给出一个完整的 Vue 组件示例,实现了 Todo 应用的全部功能。
TodoApp.vue 完整代码
<template>
<div class="todo-app">
<h1>待办事项</h1>
<!-- 添加新待办 -->
<div class="add-todo">
<input
v-model="newTitle"
placeholder="输入新的待办事项..."
@keyup.enter="addTodo"
/>
<button @click="addTodo">添加</button>
</div>
<!-- 待办列表 -->
<ul class="todo-list">
<li v-for="todo in todos" :key="todo.id" :class="{ done: todo.done }">
<input
type="checkbox"
:checked="todo.done"
@change="toggleTodo(todo)"
/>
<span>{{ todo.title }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<!-- 空状态 -->
<p v-if="todos.length === 0" class="empty">暂无待办事项,添加一个吧!</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const API = 'http://127.0.0.1:8000'
const todos = ref([])
const newTitle = ref('')
// 页面加载时获取所有待办
onMounted(async () => {
await fetchTodos()
})
// 获取所有待办
async function fetchTodos() {
const res = await axios.get(`${API}/todos`)
todos.value = res.data
}
// 添加待办
async function addTodo() {
if (!newTitle.value.trim()) return
await axios.post(`${API}/todos`, { title: newTitle.value })
newTitle.value = ''
await fetchTodos()
}
// 切换完成状态
async function toggleTodo(todo) {
await axios.put(`${API}/todos/${todo.id}`, { done: !todo.done })
await fetchTodos()
}
// 删除待办
async function removeTodo(id) {
await axios.delete(`${API}/todos/${id}`)
await fetchTodos()
}
</script>
<style scoped>
.todo-app {
max-width: 500px;
margin: 40px auto;
font-family: sans-serif;
}
.add-todo {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.add-todo input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.add-todo button {
padding: 8px 20px;
background: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.todo-list li.done span {
text-decoration: line-through;
color: #999;
}
.todo-list li button {
margin-left: auto;
padding: 4px 12px;
background: #ff5252;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.empty {
text-align: center;
color: #999;
margin-top: 40px;
}
</style>
6.4 联调流程图
把前后端联调的完整数据流画出来:
Vue 前端 (localhost:5173) FastAPI 后端 (localhost:8000) SQLite
┌─────────────────────┐ ┌─────────────────────┐ ┌──────────┐
│ │ │ │ │ │
│ 用户点击"添加" │── POST ──────▶│ create_todo() │── INSERT ──▶│ todo 表 │
│ │ │ │ │ │
│ 页面显示列表 │◀── JSON ──────│ get_todos() │◀── SELECT ──│ │
│ │ │ │ │ │
│ 用户点击"完成" │── PUT ───────▶│ update_todo() │── UPDATE ──▶│ │
│ │ │ │ │ │
│ 用户点击"删除" │── DELETE ────▶│ delete_todo() │── DELETE ──▶│ │
│ │ │ │ │ │
└─────────────────────┘ └─────────────────────┘ └──────────┘
每次操作都是:前端发请求 → 后端处理 → 操作数据库 → 返回结果 → 前端更新页面。
6.5 联调步骤总结
第一步:启动后端
cd todo_backend
uvicorn main:app --reload
确认后端运行在 http://127.0.0.1:8000。
第二步:启动前端
cd todo_frontend
npm run dev
确认前端运行在 http://localhost:5173(或其他端口)。
第三步:在浏览器中测试
- 打开前端页面
- 在输入框输入”买牛奶”,点击添加
- 列表中出现”买牛奶”
- 点击复选框,标记为完成
- 点击删除按钮,移除待办
如何确认数据真的存到了数据库
- 方法一:关闭后端服务,重新启动,刷新前端页面,数据还在
- 方法二:用 DB Browser for SQLite 打开
database.db文件查看
本章小结
| 完成项 | 说明 |
|---|---|
| CORS 跨域 | 后端加中间件,允许前端跨域访问 |
| axios GET | axios.get(url) 获取数据 |
| axios POST | axios.post(url, data) 新增数据 |
| axios PUT | axios.put(url, data) 修改数据 |
| axios DELETE | axios.delete(url) 删除数据 |
| 完整联调 | Vue 组件 + FastAPI 接口 + SQLite 数据库 |
到这里,你已经掌握了一个完整的前后端应用的开发流程:
Vue 前端 ←→ FastAPI 后端 ←→ SQLite 数据库
下一章,我们讲讲开发中常见的坑和调试技巧。