Vue 3 客户端开发 — 前后端分离项目实战

之前的课程完成了 FastAPI 后端服务器的搭建——邮件发送、验证码验证、MVC 分层架构。今天进入客户端(前端)开发领域——学习 Vue 3 框架,掌握脚手架搭建、路由配置、组件开发、Axios 前后端交互,将后端接口与前端页面打通,实现完整的前后端分离项目


一、前后端分离架构概述

1. 什么是前后端分离?

在之前的课程中,我们的客户端页面(HTML)直接放在服务器的 templates 文件夹下,由 FastAPI 通过 Jinja2 模板引擎渲染——这是一体式项目模式。

前后端分离则是另一种架构:

┌─────────────────────┐          ┌─────────────────────┐
│   前端 (Vue 3)       │  HTTP    │   后端 (FastAPI)      │
│   localhost:8080    │ ◄──────► │   localhost:8001     │
│                     │  JSON    │                      │
│  • 页面展示          │          │  • 接口定义 (Controller) │
│  • 用户交互          │          │  • 业务逻辑 (Service)    │
│  • 路由跳转          │          │  • 数据库操作 (Dao)      │
│  • 发送 HTTP 请求    │          │  • LLM 调用 (AI)       │
└─────────────────────┘          └─────────────────────┘

关键区别:

对比维度一体式项目前后端分离
前端运行方式服务器渲染 HTML,浏览器直接访问服务器路径独立 Vite 开发服务器,占用独立端口
前端端口无(复用服务器端口 8001)8080(独立端口)
后端端口80018001
页面访问http://localhost:8001/users/goAhttp://localhost:8080/testOne
前后端通信服务器内部跳转前端通过 Axios 发 HTTP 请求调用后端接口
开发体验改前端需刷新页面热更新(HMR),保存即刷新
2. 技术选型
层级技术作用
核心框架Vue 3构建用户界面的渐进式框架
构建工具Vite下一代前端构建工具,替代 Vue CLI
路由Vue Router 4管理客户端页面跳转(SPA 单页应用)
HTTP 请求Axios向后端发送请求、接收响应
包管理器npm安装和管理前端依赖

二、Vue 3 项目搭建

1. 环境准备

Vue 3 项目开发需要 Node.js 环境(v16.0 以上)。

node -v    # 查看 Node.js 版本
npm -v     # 查看 npm 版本
2. 使用 Vite 创建项目
# 切换磁盘路径到项目保存路径下
# 执行创建命令,my-vue-app 为项目名称(英文小写)
npm create vite@latest my-vue-app -- --template vue

# 进入项目目录
cd my-vue-app

# 安装依赖
npm install
3. 项目结构介绍
my-vue-app/
├── node_modules/       # 依赖包(npm install 后生成)
├── public/             # 静态资源(不经过编译,直接复制到输出目录)
├── components/         # 组件目录(页面级别的 .vue 文件)
├── src/                # 源代码目录
│   ├── assets/         # 需要编译的资源(图片、CSS 等)
│   ├── router/         # 路由配置目录
│   │   └── index.js    # 路由定义文件
│   ├── App.vue         # 根组件
│   ├── main.js         # 入口文件(全局配置)
│   └── style.css       # 全局样式
├── index.html          # 入口 HTML 文件
├── package.json        # 项目配置与依赖声明
└── vite.config.js      # Vite 配置文件

核心文件说明:

  • index.html:浏览器访问的第一个页面,<div id="app"> 是 Vue 挂载点,<script type="module" src="/src/main.js"> 加载入口 JS
  • main.js:入口 JS 文件,创建 Vue 应用实例、注册路由、配置 Axios 等全局设置
  • App.vue:根组件,所有页面的外层容器,<router-view/> 负责切换显示不同路由下的页面
  • vite.config.js:Vite 构建工具配置(端口、插件等)

三、项目配置

1. 配置启动端口与自动打开浏览器

vite.config.js 中添加 server 配置:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  // 开发服务器配置
  server: {
    host: 'localhost',   // 服务器地址
    port: 8080,          // 启动端口(默认 5173,改为 8080)
    open: true,          // 启动后自动打开浏览器
  },
})

关键点:

  • port: 8080 把默认端口 5173 改为 8080,与后端 8001 区分
  • open: true 启动项目时自动用默认浏览器打开 http://localhost:8080
  • 要注意项目网页启动后不用了要ctrl+c及时关掉,不然下一次启动后8080端口会因为网页还存在(手动叉掉网页也没用)被占,自动调去8081端口,导致前后端跨域链接出现问题。
  • host: 'localhost' 限定只在本机访问
2. 启动项目
npm run dev

启动后终端显示:

VITE v5.x.x  ready in xxx ms
➜  Local:   http://localhost:8080/

热更新(HMR): 修改代码后按 Ctrl+S 保存,页面内容自动更新,无需手动刷新浏览器,大多数情况下不需要重启项目

3. package.json — 依赖管理
{
  "name": "my-vue-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",           // 启动开发服务器
    "build": "vite build",   // 构建生产版本
    "preview": "vite preview" // 预览生产构建
  },
  "dependencies": {
    "axios": "^1.19.0",       // HTTP 请求库
    "vue": "^3.4.0",          // Vue 3 核心
    "vue-router": "^4.6.4"    // 路由管理
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",  // Vite 的 Vue 插件
    "vite": "^5.0.0"                 // 构建工具
  }
}

关键点:

  • dependencies:生产环境需要的包(axios、vue、vue-router)
  • devDependencies:仅开发环境需要的包(vite、vite 插件)
  • "type": "module":项目使用 ES Module 语法(import/export
  • 安装新依赖:npm install 包名 → 自动写入 dependencies

四、客户端路由 — Vue Router【核心】

客户端路由的作用:控制如何通过 URL 路径访问不同的页面。与后端路由(FastAPI 的接口路径)不同,客户端路由管理的是页面跳转,不需要向服务器发请求。

1. 安装 Vue Router
npm install vue-router@4
2. 创建路由配置文件 src/router/index.js
// 引入路由相关函数
import { createRouter, createWebHistory } from "vue-router";

// 定义路由配置 --- 数组,每个页面是一个对象
const routes = [
    {// 一个页面的访问路径就是一个js对象,至少包含2个属性:path、component
        path: '/testOne',    // 访问路径
        component: () => import('../../components/TestOne.vue')  // 懒加载组件
    },
    {
        path: '/testTwo',
        component: () => import('../../components/TestTwo.vue')
    },
    {
        path: '/',           // 根路径,作为默认首页
        component: () => import('../../components/Login.vue')
    }
]

// 创建路由实例(History 模式)
const router = createRouter({
    history: createWebHistory(),  // 干净的 URL 路径(无 # 号)
    routes                         // 路由配置数组
})

// 导出路由实例
export default router

关键点:

概念说明
createRouter()Vue Router 4 的工厂函数,创建路由实例
createWebHistory()HTML5 History 模式,URL 为干净的路径 /testOne,而非 Hash 模式的 /#/testOne。依赖浏览器 history.pushState() API
path访问路径,和服务器的请求路径规则一致
component该路径对应的页面组件。() => import(...)懒加载写法——只有访问该路径时才加载组件,减少首屏加载时间
export default导出路由实例,供 main.js 注册

路由配置对象的核心属性:

属性类型说明
pathstring页面访问路径,如 /testOne
componentComponent路径匹配时渲染的 Vue 组件
namestring(可选)路由名称,用于编程式导航
childrenArray(可选)嵌套子路由
3. 在 main.js 中注册路由
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 导入路由
import router from './router'
app.use(router)   // 注册路由到 Vue 应用

app.mount('#app')

关键点:

  • app.use(router) 将路由实例注册到 Vue 应用中,如果不注册,路由不生效
  • 注册后,Vue 应用才能识别 <router-view/><router-link> 组件
4. 修改 App.vue — 路由占位
<template>
    <div>
        <!-- router-view 是路由切换的占位标签,匹配到的页面组件会渲染在这里 -->
        <router-view/>
    </div>
</template>

<script setup>
</script>

<style>
</style>

关键点:

  • <router-view/> 是 Vue Router 提供的内置组件,相当于一个"插槽"
  • 当访问 /testOne 时,TestOne.vue 的内容会渲染在 <router-view/> 的位置
  • 任何时候,只要创建了新页面组件,先去配置路由——这是一个标准操作流程
5. 路由访问流程
用户在浏览器输入 http://localhost:8080/testOne
                │
                ▼
    Vue Router 拦截 URL,匹配 routes 数组
                │
                ▼
    找到 path: '/testOne' → 加载 TestOne.vue 组件
                │
                ▼
    TestOne.vue 的内容渲染到 App.vue 中的 <router-view/> 位置
                │
                ▼
    用户看到 TestOne 页面

五、Vue 单文件组件(.vue)

一个 .vue 文件就是一个组件(页面),由三部分组成:

<template>
    <!-- HTML 模板 —— 页面的结构和内容 -->
</template>

<script setup>
    <!-- JavaScript 逻辑 —— 响应式数据、函数、生命周期等 -->
</script>

<style scoped>
    <!-- CSS 样式 —— scoped 表示样式只作用于当前组件 -->
</style>

三部分详解:

部分作用说明
<template>HTML 模板编写页面结构。必须有一个根元素包裹所有内容
<script setup>JS 逻辑Vue 3 的语法糖,顶层绑定(变量、函数)自动暴露给模板,不需要 return
<style scoped>CSS 样式scoped 属性让样式只作用于当前组件,不会污染其他组件

项目中的实际用法:在本次项目中,直接把 .vue 当作一个 .html 页面来写,不深入研究组件嵌套、父子传参等高级用法。


六、响应式数据 — ref

ref() 是 Vue 3 定义响应式数据的核心 API。数据变化时,页面自动更新。

TestOne.vue 实战
<template>
    <div>
        <h1>操作1 — v-bind</h1>
        <form>
            账号:<input type="text" v-bind:disabled="isDisabled">
            <br>
            密码:<input type="password">
            <br>
            <button type="button" @click="ChangeInput">点我1</button>
        </form>

        <h1>操作2 — v-show</h1>
        <div v-show="isShow" style="border:1px solid red; width: 100%;height: 30px">
            我显示了1
        </div>
        <div v-show="!isShow" style="border:1px solid red; width: 100%;height: 30px">
            我显示了2
        </div>
        <button type="button" @click="isShow = !isShow">点我2</button>
    </div>
</template>

<script setup>
// 导入响应式数据 API
import { ref } from "vue";

// ref() 定义响应式变量,初始值为 true
let isDisabled = ref(true);

function ChangeInput() {
    // JS 中通过 .value 访问和修改
    isDisabled.value = !isDisabled.value;
}

let isShow = ref(true);
</script>

关键点分析:

概念代码说明
定义响应式数据let isDisabled = ref(true)创建一个响应式引用,初始值为 true
模板中使用:disabled="isDisabled"模板中自动解包,直接用变量名,不需要 .value
JS 中修改isDisabled.value = !isDisabled.valueJS 中必须用 .value 读取和修改
绑定属性v-bind:disabled="isDisabled"动态绑定 HTML 属性,简写 :disabled
绑定事件@click="ChangeInput"@v-on: 的简写,绑定点击事件
条件显示v-show="isShow"控制元素的显示/隐藏(切换 display:none

v-bind 简写规则:

<!-- 完整写法 -->
<input v-bind:disabled="isDisabled" />

<!-- 简写(推荐) -->
<input :disabled="isDisabled" />

v-on 简写规则:

<!-- 完整写法 -->
<button v-on:click="ChangeInput">按钮</button>

<!-- 简写(推荐) -->
<button @click="ChangeInput">按钮</button>

七、表单双向绑定 — v-model

v-model 实现表单元素和数据之间的双向绑定:输入框内容变化 → 数据自动更新;数据变化 → 输入框内容自动更新。

Login.vue 中的 v-model 实战
<template>
    <div>
        <form>
            邮箱号:<input type="text" :disabled="!isCode" v-model="email">
            <br>
            验证码:<input type="text" :disabled="isCode" v-model="code">
            <br>
            <button type="button" @click="sendEmail" v-show="isCode">发送验证码</button>
            <button type="button" @click="checkCode" v-show="!isCode">验证验证码</button>
        </form>
    </div>
</template>

<script setup>
import { ref, getCurrentInstance } from "vue";

const { proxy } = getCurrentInstance();

// 控制输入框是否可用
let isCode = ref(true);

// 双向绑定的数据
let email = ref("12345678@qq.com");  // 邮箱号
let code = ref("");                      // 验证码
</script>

这个页面中的指令汇总:

指令代码示例作用
v-modelv-model="email"双向绑定:输入框 ↔ 变量,一方变另一方自动同步
:disabled:disabled="!isCode"动态控制输入框是否可编辑
v-showv-show="isCode"条件显示按钮(切换 display 属性)
@click@click="sendEmail"绑定点击事件

v-show vs v-if:

指令原理适用场景
v-show始终渲染 DOM,切换 display:none频繁切换显示/隐藏(本项目中使用)
v-if条件为假时销毁 DOM,为真时重建 DOM切换不频繁,需要节省初始渲染开销

八、Axios — 前端与后端交互【核心】

客户端通过 Axios 向后端发送 HTTP 请求,获取服务器返回的数据。这是前后端分离中最关键的环节

1. 安装 Axios
npm install axios
2. 全局配置 — main.js

main.js 中配置 Axios 的全局默认值,避免每个组件重复写配置信息:

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 注册路由
import router from './router'
app.use(router)

// ========== Axios 全局配置 ==========
import axios from 'axios'

// ① 设置基础 URL —— 所有请求自动拼接此前缀
axios.defaults.baseURL = 'http://localhost:8001/'

// ② 设置 POST/PUT 请求的 Content-Type 为 JSON
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.defaults.headers.put['Content-Type'] = 'application/json'

// ③ 挂载到 Vue 全局属性 —— 组件中用 proxy.$axios 替代原生 axios
app.config.globalProperties.$axios = axios

app.mount('#app')

配置项详解:

配置项作用
baseURL'http://localhost:8001/'后端服务器的公共路径前缀。之后发请求只需写相对路径如 users/sendEmail,实际请求 http://localhost:8001/users/sendEmail
headers.post['Content-Type']'application/json'POST 请求时自动带上 JSON 请求头
globalProperties.$axiosaxios挂载后所有组件通过 proxy.$axios 使用,不需要每个组件都 import axios
3. 在组件中使用 Axios

获取 proxy 对象:

import { ref, getCurrentInstance } from "vue";

// getCurrentInstance() 获取当前组件实例
// proxy 是 Vue 的代理对象,挂载了 main.js 中 globalProperties 的所有属性
const { proxy } = getCurrentInstance();

之后用 proxy.$axios({...}) 发送请求:

proxy.$axios({
    url: 'users/sendEmail',  // 请求地址(自动拼接 baseURL)
    method: 'get',            // 请求方式
    params: {                 // GET 请求的参数 → 拼接为 URL 查询字符串
        email: sendEmail,
    },
}).then(res => {
    // 请求成功:res.data 是后端返回的 JSON 数据
    console.log(res.data);
}).catch(err => {
    // 请求失败:网络错误、后端未启动、跨域等
    console.error(err);
});
4. GET 请求 vs POST 请求 — 参数传递对比
GET 请求POST 请求
Axios 参数写法params: { email: 'xxx' }data: { email: 'xxx', code: '1234' }
参数位置拼接在 URL 后面:?email=xxx放在请求体(Body)中
后端接收方式形参 email: strPydantic 数据类
本项目用例发送验证码验证验证码

GET 请求示例(发送验证码):

function sendEmail() {
    let sendEmail = email.value;
    proxy.$axios({
        url: 'users/sendEmail',
        method: 'get',
        params: {                    // ← GET 用 params
            email: sendEmail,        // key 必须和后端形参名一致!
        },
    }).then(res => {
        let code = res.data.code;
        let msg = res.data.msg;
        if (code === 200) {
            alert(msg || "验证码已发送,请查收邮箱");
        } else {
            alert(msg || "发送失败");
        }
    }).catch(err => {
        alert("请求失败,请确认后端服务是否已启动:" + err.message);
    });
}

POST 请求示例(验证验证码):

function checkCode() {
    let checkCode = {
        email: email.value,   // key 必须和后端 BaseModel 的属性名一致
        code: code.value,
    };
    proxy.$axios({
        url: 'users/verifyEmail',
        method: 'post',
        data: checkCode,           // ← POST 用 data
    }).then(res => {
        let code = res.data.code;
        let msg = res.data.msg;
        if (code === 200) {
            alert(msg || "验证成功!");
        } else {
            alert(msg || "验证失败");
        }
    }).catch(err => {
        alert("请求失败:" + err.message);
    });
}

关键原则:

客户端发送参数时的 key 必须和后端接口形参名 / BaseModel 属性名一致。这是前后端交互的铁律——客户端和服务器通过 key:value 交互数据,只能通过 key 找 value。

5. 统一返回格式

后端所有接口返回统一的 JSON 格式:

{
    "code": 200,        // 状态码:200 成功,500 失败
    "msg": "发送成功",    // 提示信息
    "data": null        // 数据内容
}

前端根据 res.data.code 判断请求结果并展示对应的 msg 给用户。


九、Login.vue 完整流程解析

Login.vue 是前后端交互的实战综合案例,涉及双向绑定、条件渲染、事件绑定、Axios GET/POST 请求等核心知识点。

1. 完整代码
<template>
    <div>
        <form>
            邮箱号:<input type="text" :disabled="!isCode" v-model="email">
            <br>
            验证码:<input type="text" :disabled="isCode" v-model="code">
            <br>
            <button type="button" @click="sendEmail" v-show="isCode">发送验证码</button>
            <button type="button" @click="checkCode" v-show="!isCode">验证验证码</button>
        </form>
    </div>
</template>

<script setup>
import { ref, getCurrentInstance } from "vue";

const { proxy } = getCurrentInstance();

// 控制 UI 状态
let isCode = ref(true);       // true → 可以发验证码,false → 可以验证

// 双向绑定的数据
let email = ref("12345678@qq.com");
let code = ref("");

// ========== 发送验证码 ==========
function sendEmail() {
    let sendEmail = email.value;
    proxy.$axios({
        url: 'users/sendEmail',
        method: 'get',
        params: { email: sendEmail },
    }).then(res => {
        let code = res.data.code;
        let msg = res.data.msg;
        if (code === 200) {
            isCode.value = !isCode.value;   // 切换到验证码输入状态
            alert(msg || "验证码已发送,请查收邮箱");
        } else {
            alert(msg || "发送失败,请稍后重试");
        }
    }).catch(err => {
        alert("请求失败,请确认后端服务是否已启动:" + err.message);
    });
}

// ========== 验证验证码 ==========
function checkCode() {
    let checkCode = {
        email: email.value,
        code: code.value,
    };
    proxy.$axios({
        url: 'users/verifyEmail',
        method: 'post',
        data: checkCode,
    }).then(res => {
        let code = res.data.code;
        let msg = res.data.msg;
        if (code === 200) {
            alert(msg || "验证成功!");
            isCode.value = true;   // 恢复初始状态
        } else {
            alert(msg || "验证失败");
        }
    }).catch(err => {
        alert("请求失败:" + err.message);
    });
}
</script>
2. 交互状态机

Login.vue 的 UI 有两套状态,通过 isCode 变量切换:

初始状态 (isCode = true)
    ├── 邮箱输入框:可编辑(:disabled="!isCode" → :disabled="false")
    ├── 验证码输入框:禁用(:disabled="isCode" → :disabled="true")
    ├── "发送验证码"按钮:显示(v-show="isCode")
    └── "验证验证码"按钮:隐藏(v-show="!isCode")
            │
            │ 点击"发送验证码" → 后端返回 code:200
            │
            ▼
验证状态 (isCode = false)
    ├── 邮箱输入框:禁用
    ├── 验证码输入框:可编辑
    ├── "发送验证码"按钮:隐藏
    └── "验证验证码"按钮:显示
            │
            │ 点击"验证验证码" → 后端返回 code:200
            │
            ▼
回到初始状态 (isCode = true)
3. 数据流全景
【用户操作】输入邮箱号 → 点击"发送验证码"
        │
        ▼
【前端 Login.vue】
    sendEmail() 函数
    ├── email.value 获取用户输入的邮箱号
    ├── proxy.$axios → GET http://localhost:8001/users/sendEmail?email=xxx
    └── .then(res) → 根据 res.data.code 判断结果
        │
        ▼  HTTP 请求 ──────────────────────────
        │
        ▼
【后端 stu_fastapi_te】
    UsersController.send_email(email)
        │
        ▼
    UsersService.send_email(email)
        ├── UsersDao.check_email(email) → MySQL 查询邮箱是否注册
        ├── random 生成 4 位验证码
        ├── smtplib 通过 QQ SMTP 发送邮件
        └── Redis 存储验证码(email → code,60 秒过期)
        │
        ▼  返回 JSON ──────────────────────────
        │
        ▼
【前端 Login.vue】
    .then(res) 收到响应
    ├── code === 200 → isCode 切换为 false → 显示验证码输入框
    └── code !== 200 → alert 显示错误信息

══════════════════════════════════════════════

【用户操作】输入验证码 → 点击"验证验证码"
        │
        ▼
【前端 Login.vue】
    checkCode() 函数
    ├── 封装 { email, code } 为 JS 对象
    ├── proxy.$axios → POST http://localhost:8001/users/verifyEmail
    │                    Body: {"email":"xxx","code":"1234"}
    └── .then(res) → 根据 res.data.code 判断
        │
        ▼  HTTP 请求 ──────────────────────────
        │
        ▼
【后端 stu_fastapi_te】
    UsersController.verify_email(request)
        │
        ▼
    UsersService.verify_email(email, code)
        ├── Redis 读取该 email 对应的验证码
        ├── 比对用户输入的 code 与 Redis 中的 stored_code
        └── 验证成功 → 删除 Redis 中的验证码(一次性使用)
        │
        ▼  返回 JSON ──────────────────────────
        │
        ▼
【前端 Login.vue】
    .then(res) 收到响应
    ├── code === 200 → 验证成功,isCode 恢复为 true
    └── code !== 200 → alert 显示错误信息
4. 点击按钮后 .then(res => { 里面发生了什么?
.then(res => {
    // res 是 axios 的响应对象,有6个常用属性
    //    res.status     HTTP 状态码(200、201、500 等)
    //    res.statusText 状态文本("OK","Created"等)
    //    res.headers    响应头
    //    res.config     本次请求的配置
    //    res.request    发送请求的 XMLHttpRequest 对象
    //    res.data       后端接口返回的 JSON 数据(最常用)

    // 实际代码中,只从 res.data 获取业务数据:
    let code = res.data.code;   // 业务状态码(200 成功 / 500 失败)
    let msg  = res.data.msg;    // 提示信息(如 "发送成功" / "邮箱号不存在")

    // 判定业务结果并更新前端状态
    if (code === 200) {
        // 成功 — 切换界面状态、弹出成功提示
        isCode.value = !isCode.value;
        alert(msg || "默认提示语");
    } else {
        // 失败 — 弹出后端的错误提示
        alert(msg || "默认错误提示");
    }
}).catch(err => {
    // catch 捕获的不是业务错误,而是网络/服务器异常:
    // — 后端服务没启动(ERR_CONNECTION_REFUSED)
    // — 跨域被浏览器拦截(CORS error)
    // — 网络断开、DNS 解析失败等
    alert("请求失败,请确认后端服务是否已启动:" + err.message);
})

关键点:

  • res.data 是后端接口 return {...} 返回的字典(自动 JSON 序列化)
  • .then() 处理 HTTP 请求成功的情况(HTTP 200),但业务可能失败(code: 500),需要进一步判断
  • .catch() 处理 HTTP 请求本身失败的情况(网络错误、跨域、服务器未启动等)

十、跨域配置(CORS)— 后端配合

1. 为什么需要跨域配置?

同源策略要求 A 服务访问 B 服务必须满足"三同":

  • 协议相同(http / https)
  • IP 相同(localhost / 127.0.0.1)
  • 端口相同(8080 / 8001)

前后端分离项目中,前端 localhost:8080 访问后端 localhost:8001端口不同,违背同源策略。浏览器会拦截请求,抛出 CORS 错误。

2. 解决方案 — 后端配置 CORS

main.py 中添加跨域中间件(只需后端配置,前端无需任何处理):

# main.py
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:8080"],  # 允许的前端源地址
    allow_credentials=True,                    # 允许携带 Cookie
    allow_methods=["*"],                       # 允许所有 HTTP 方法(GET/POST/PUT/DELETE)
    allow_headers=["*"],                       # 允许所有请求头
)

关键点:

  • allow_origins 指定哪些前端地址可以访问后端,生产环境应写具体域名
  • allow_methods=["*"] 允许所有请求方法
  • CORS 配置只在后端做,前端代码无需改动
  • 配置后浏览器不再拦截前端到后端的 HTTP 请求

十一、完整开发流程总结

1. 前端开发标准步骤
① 创建项目
    npm create vite@latest 项目名 -- --template vue
    cd 项目名
    npm install

② 安装依赖
    npm install vue-router@4   # 路由
    npm install axios           # HTTP 请求

③ 配置 vite.config.js
    server: { host, port: 8080, open: true }

④ 创建页面组件 (.vue)
    components/xxx.vue

⑤ 配置路由 (src/router/index.js)
    { path: '/xxx', component: () => import('...') }

⑥ 在 main.js 注册路由
    app.use(router)

⑦ 在 main.js 配置 Axios
    axios.defaults.baseURL = 'http://localhost:8001/'
    app.config.globalProperties.$axios = axios

⑧ 修改 App.vue
    <router-view/>

⑨ 开发页面逻辑
    导入 ref → 定义响应式数据 → 编写交互函数 → 使用 proxy.$axios 请求后端

⑩ 启动项目
    npm run dev
2. 前后端端口与地址对照
前端 (Vue 3)后端 (FastAPI)
端口80808001
访问地址http://localhost:8080http://localhost:8001
接口测试通过 Axios 调用Swagger UI:http://localhost:8001/docs
启动命令npm run devpython main.py
3. 前后端请求对应关系
功能前端组件Axios 调用后端接口后端 Controller
发送验证码Login.vuesendEmail()GET users/sendEmail?email=xxx@users_router.get("/sendEmail")UsersController.send_email
验证验证码Login.vuecheckCode()POST users/verifyEmail + JSON body@users_router.post("/verifyEmail")UsersController.verify_email
4. 核心概念速查表

Vue 3 模板语法:

语法完整写法作用
:attr="val"v-bind:attr="val"动态绑定 HTML 属性
@event="fn"v-on:event="fn"绑定事件处理函数
v-model="val"表单双向绑定
v-show="cond"条件显示(display:none)
{{ val }}文本插值

ref() vs reactive():

特性refreactive
适用类型基本类型、对象仅对象/数组
JS 中访问.value直接访问
模板中使用直接使用直接使用
本项目推荐✅ 占 90%+ 场景特定场景

Axios 请求参数:

请求方式参数写法参数位置后端接收
GETparams: { key: val }URL 查询字符串接口形参
POSTdata: { key: val }请求体 (JSON)BaseModel 数据类

十二、课后实践

📝 任务要求
  1. 创建前端项目:使用 Vite 创建一个新的 Vue 3 项目
  2. 安装依赖:安装 vue-routeraxios
  3. 配置端口:在 vite.config.js 中设置端口为 8080,开启自动打开浏览器
  4. 创建页面:创建至少 2 个 .vue 页面组件
  5. 配置路由:在 src/router/index.js 中配置路由,实现页面间跳转
  6. 配置 Axios:在 main.js 中全局配置 Axios(baseURL、全局挂载)
  7. 实现交互:参照 Login.vue 的模式,实现一个前后端交互功能(如注册、查询等)
  8. 启动后端:确保 FastAPI 服务器在 8001 端口运行,并已配置 CORS
🔧 准备工作检查清单
  • Node.js 已安装(v16.0 以上),node -v 验证
  • Vue 3 前端项目已创建,依赖已安装
  • 前端端口 8080,后端端口 8001
  • 后端 FastAPI 已配置 CORS(allow_origins=["http://localhost:8080"]
  • 前后端同时启动,浏览器控制台无 CORS 报错
  • 了解 res.data 的结构(codemsgdata
⚠️ 常见问题
问题可能原因解决方案
前端页面空白路由未配置或 <router-view/> 未写检查 router/index.jsApp.vue
请求报 CORS 错误后端未配置跨域main.py 添加 CORSMiddleware
proxy.$axios 报 undefinedmain.js 中未挂载添加 app.config.globalProperties.$axios = axios
后端接收不到参数前端 key 和后端形参名不一致确保 params/data 中的 key 与后端一致
npm run dev 报错依赖未安装执行 npm install
.catch 触发但后端正常跨域配置的 origin 不匹配确认 allow_origins 中的端口号与前端一致

核心要点

  • 前后端分离:前端 8080 端口(Vite) + 后端 8001 端口(FastAPI),通过 HTTP 通信,各自独立开发部署
  • Vue SFC 三件套<template>(HTML)、<script setup>(JS 逻辑)、<style scoped>(CSS 样式)
  • 客户端路由:Vue Router 管理页面跳转,<router-view/> 是切换占位符,访问不同路径显示不同组件
  • 响应式数据 ref():数据变化 → 页面自动更新;JS 中用 .value,模板中直接使用
  • 模板指令: 绑定属性、@ 绑定事件、v-model 双向绑定、v-show 条件显示
  • Axios 三部曲:① main.js 全局配置(baseURL + 挂载) → ② 组件中用 getCurrentInstance().proxy.$axios 获取 → ③ 发请求(GET 用 params,POST 用 data
  • 前端交互铁律:前端参数 key 必须和后端形参名 / BaseModel 属性名完全一致
  • CORS:同源策略限制 → 后端添加 CORSMiddleware 中间件解决,前端无需处理
  • 统一返回格式{"code": 状态码, "msg": 提示信息, "data": 数据},前端根据 code 判断业务结果

最终思考:从 day5 的 FastAPI 接口开发,到 day6 的邮件验证码后端实战,再到今天的 Vue 3 客户端开发——完成了从前到后的全栈项目闭环。Login.vue 的完整流程(用户输入 → Axios GET 发邮件 → 后端 Service/Dao/Redis 处理 → 返回 JSON → 前端 .then() 判断 → POST 验证 → 比对 Redis → 返回结果)就是前后端分离开发的标准范式。之后任何功能的开发(AI 对话、RAG 检索、图片上传等),都是这个范式的变体:前端发请求 → 后端处理 → 返回 JSON → 前端展示

更多推荐