首页 / 知识库 / 0基础入门-阅读资料 / 0基础-后台入门

第六章 前后端联调 —— 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:5173http://localhost:8000是(端口不同)
http://localhost:5173http://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(或其他端口)。

第三步:在浏览器中测试

  1. 打开前端页面
  2. 在输入框输入”买牛奶”,点击添加
  3. 列表中出现”买牛奶”
  4. 点击复选框,标记为完成
  5. 点击删除按钮,移除待办

如何确认数据真的存到了数据库

  • 方法一:关闭后端服务,重新启动,刷新前端页面,数据还在
  • 方法二:用 DB Browser for SQLite 打开 database.db 文件查看

本章小结

完成项说明
CORS 跨域后端加中间件,允许前端跨域访问
axios GETaxios.get(url) 获取数据
axios POSTaxios.post(url, data) 新增数据
axios PUTaxios.put(url, data) 修改数据
axios DELETEaxios.delete(url) 删除数据
完整联调Vue 组件 + FastAPI 接口 + SQLite 数据库

到这里,你已经掌握了一个完整的前后端应用的开发流程:

Vue 前端  ←→  FastAPI 后端  ←→  SQLite 数据库

下一章,我们讲讲开发中常见的坑和调试技巧。