跳转至

前后端联调总览

后端你已经会写了,这一章把 Vue 前端接上去。核心是 Axios + 拦截器——这是你前端天天写的东西,只不过现在后端也是你自己写的。

联调全景

flowchart LR
    subgraph 前端["前端(Vue 5173)"]
        V[页面调用 api]
        V --> I["Axios 请求拦截器\n塞 Authorization: Bearer token"]
    end
    subgraph 后端["后端(Spring 8080)"]
        J["JwtInterceptor\n校验 token,取 userId"]
        J --> C[Controller → Service]
    end
    I -->|"/api 经 Vite 代理/Nginx 反代"| J
    C -->|"统一响应 {code,message,data}"| R["Axios 响应拦截器\n拆 code、处理 401"]
    R --> V

Axios 封装(核心)

一个 axios 实例 + 两个拦截器,搞定所有对接细节:

import axios from 'axios'
import { useUserStore } from '../stores/user'

// 第四篇 ch32:axios 实例 + 拦截器(这是前后端对接的核心)
// 对标后端的 JwtInterceptor:前端在"发请求前"塞 token,后端在"进 Controller 前"校验。
const request = axios.create({
  baseURL: '/api',        // 走 Vite 代理 / Nginx 反代,统一加前缀
  timeout: 10000,
})

// 请求拦截器:每个请求自动带上 token
request.interceptors.request.use((config) => {
  const userStore = useUserStore()
  if (userStore.token) {
    config.headers.Authorization = `Bearer ${userStore.token}`
  }
  return config
})

// 响应拦截器:统一拆 code、处理 401
// 关键:后端全局异常处理器把所有异常都返回 HTTP 200,业务码放在 body.code 里。
// 所以"token 失效"是 HTTP 200 + body.code===401,必须在这里按 code 识别(不会触发下面 HTTP 401 分支)。
request.interceptors.response.use(
  (response) => {
    const res = response.data            // 后端统一返回 { code, message, data }
    if (res.code === 200) {
      return res.data                     // 直接把 data 拆出来,业务代码更干净
    }
    if (res.code === 401) {
      // token 失效:清登录态,跳登录页(业务码 401,不是 HTTP 401)
      useUserStore().logout()
      window.location.href = '/login'
    }
    alert(res.message || '请求失败')
    return Promise.reject(new Error(res.message || '请求失败'))
  },
  (error) => {
    // 网络层 / HTTP 非 2xx(后端目前不会走到这里,保留作防御)
    if (error.response?.status === 401) {
      useUserStore().logout()
      window.location.href = '/login'
    }
    return Promise.reject(error)
  },
)

export default request

两件事:

  1. 请求拦截器:每个请求自动带上 Authorization: Bearer <token>(token 从 Pinia store 拿)。
  2. 响应拦截器:统一拆后端的 {code, message, data}code===200 就把 data 直接返回;遇到业务码 401(token 失效)自动登出跳转。

为什么 401 要在响应拦截器里判断,而不是 axios 的错误回调?

后端全局异常处理器把所有异常(包括 401 鉴权失败)都返回 HTTP 200,真正的状态码放在 body 的 code 字段里。所以"token 失效"是 HTTP 200 + body.code===401,会走进成功回调——必须在成功回调里按 code 判断,axios 的错误回调(HTTP 非 2xx)根本不会触发。

这样业务代码只管调接口、拿数据,完全不用碰 token 和错误处理

接口定义

import request from './request'

// 第四篇 ch32:认证相关接口
// 登录成功后端返回 { token, user: { id, username } }(已被 request 拦截器拆出 data 层)。
export interface LoginResult {
  token: string
  user: { id: number; username: string }
}

export function login(username: string, password: string): Promise<LoginResult> {
  // request 拦截器把 res.data 透出,但 axios 的 TS 类型仍是 AxiosResponse,故用断言还原真实返回类型
  return request.post('/auth/login', { username, password }) as unknown as Promise<LoginResult>
}

export function register(username: string, password: string): Promise<void> {
  return request.post('/auth/register', { username, password }) as unknown as Promise<void>
}
import request from './request'

// 第四篇 ch33:任务 CRUD 接口
export interface TaskPage {
  records: Task[]
  total: number
  current: number
  size: number
}

export interface Task {
  id: number
  title: string
  description?: string
  status: number
}

export function getTasks(params: { current?: number; size?: number; status?: number }): Promise<TaskPage> {
  // request 拦截器把 res.data 透出,但 axios 的 TS 类型仍是 AxiosResponse,故用断言还原真实返回类型
  return request.get('/tasks', { params }) as unknown as Promise<TaskPage>
}

export function createTask(data: { title: string; description?: string; status?: number }): Promise<Task> {
  return request.post('/tasks', data) as unknown as Promise<Task>
}

export function updateTask(id: number, data: { title: string; description?: string; status?: number }): Promise<Task> {
  return request.put(`/tasks/${id}`, data) as unknown as Promise<Task>
}

export function deleteTask(id: number): Promise<void> {
  return request.delete(`/tasks/${id}`) as unknown as Promise<void>
}

开发期跨域:Vite 代理

前端 5173 调后端 8080 是跨域。开发期最省事的是用 Vite 代理(不用动后端 CORS):

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

// 第四篇 ch32:开发期用 Vite 代理,把 /api/* 转发到后端 8080,避开跨域。
// 生产环境改由 Nginx 反代(见 ch35),前端代码不用改。
export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
})

/api/* 被转发到 localhost:8080 并去掉 /api 前缀。所以前端 request.get('/tasks') 实际打到后端 http://localhost:8080/tasks

token 的完整生命周期

sequenceDiagram
    participant U as 用户
    participant F as 前端
    participant B as 后端
    U->>F: 输入账密
    F->>B: POST /auth/login
    B-->>F: { token, user }
    F->>F: 存 localStorage + Pinia
    Note over F,B: 之后每个请求
    F->>B: GET /tasks (带 token)
    B->>B: 拦截器校验 token
    B-->>F: 任务列表

:octicons-arrow-left-16: 上一章:接口测试 | 下一章:Vue 3 实现前端