【Ruoyi 解密 - 09. 前端探秘2】------ 接口路径及联调实战指南

在当下竞争激烈的金融科技领域,你们公司正在全力打造一款创新理财 APP,后端基于 Ruoyi 框架,前端采用 Vue3。

开发团队各司其职,后端像精密仪器的锻造师,用 Ruoyi 构建接口;前端似界面设计师,以 Vue3 雕琢交互。

然而,如同任何宏大工程都会遭遇波折,当后端与前端的工作进入联调阶段,一系列棘手的问题如暗礁般浮出水面。譬如:

接口路径的拼接,仿佛陷入了复杂的迷宫,总在不经意间偏离正轨;

跨域问题恰似一道无形的高墙,横亘在前后端之间,阻碍着数据的顺畅流通;

Token 机制原本是守护系统安全的卫士,此刻却似偶尔闹情绪的哨兵,无端宣告过期,阻断用户的正常访问;

参数传递环节更是状况百出,参数不匹配的状况就像打乱的密码锁,让前后端信息交互陷入混乱。

这些问题犹如悬在团队头顶的达摩克利斯之剑,若不能妥善解决,不仅会延误项目进度,更可能影响产品上线后的用户体验,给公司带来不可估量的损失。接下来,让我们一同深入这个生产实际场景,看看开发团队如何凭借深厚的技术功底和敏锐的洞察力,突破重重困境,成功完成前后端联调,为这款金融 APP 的顺利问世保驾护航。

1. 接口路径相关

基础小白可能会问的一个问题:当看到

js 复制代码
// 1. 查询用户列表(对应后端 /system/user/list 接口)
export function listUser(query) {
  return request({
    url: '/system/user/list', // 接口路径(会拼接基础地址)
    method: 'get',            // 请求方法
    params: query             // GET 请求参数(自动拼在 URL 后)
  });
}

注释中的url: '/system/user/list', // 接口路径(会拼接基础地址),在ruoyi框架中url是如何拼接的呢?

在 RuoYi-Vue3 框架中,接口 URL 的拼接逻辑由封装的 request 工具统一处理,核心是基础地址(baseURL)+ 接口路径(url) 的组合,具体流程如下:

1. 基础地址(baseURL)的来源

基础地址是接口的统一前缀(如 http://localhost:8080/dev-api),由环境配置文件定义,位置在项目根目录的 .env 系列文件中:

  • 开发环境:.env.development
  • 生产环境:.env.production
  • 测试环境:.env.test

以开发环境为例,配置内容如下:

ini 复制代码
# 开发环境接口基础地址
VITE_APP_BASE_API = '/dev-api'

注意:VITE_ 前缀是 Vite 构建工具的要求,用于在前端代码中访问环境变量。

2. 接口路径(url)的拼接规则

src/utils/request.js 中,axios 实例会读取环境变量中的基础地址,并与接口定义的 url 拼接:

核心代码(request.js):

javascript 复制代码
import axios from 'axios';

// 创建 axios 实例
const service = axios.create({
  // 从环境变量中读取基础地址
  baseURL: import.meta.env.VITE_APP_BASE_API, 
  // 超时时间
  timeout: 5000
});

export default service;

拼接示例:

listUser 接口为例:

  • 接口定义的 url'/system/user/list'
  • 环境变量 VITE_APP_BASE_API'/dev-api'

最终拼接后的完整请求地址为:

复制代码
baseURL + url = '/dev-api/system/user/list'

3. 实际请求地址的转换(解决跨域)

前端开发时,/dev-api 通常是一个代理标识 ,并非真实后端地址。RuoYi 会通过 Vite 配置代理,将包含 /dev-api 的请求转发到真实后端服务器,避免跨域问题。

代理配置(vite.config.js):

javascript 复制代码
export default defineConfig({
  server: {
    // 开发环境代理配置
    proxy: {
      // 匹配以 '/dev-api' 开头的请求
      '/dev-api': {
        // 真实后端接口地址(例如 Spring Boot 后端服务)
        target: 'http://localhost:8080', 
        // 允许跨域
        changeOrigin: true,
        // 替换路径:去掉 '/dev-api' 前缀
        rewrite: (path) => path.replace(/^\/dev-api/, '')
      }
    }
  }
});

最终真实请求地址:

经过代理转发后,实际向后端发送的请求地址为:

复制代码
http://localhost:8080/system/user/list

总结:URL 拼接完整流程

  1. 定义接口路径 :在 api 模块中定义相对路径(如 /system/user/list);
  2. 读取基础地址request 工具从环境变量获取 VITE_APP_BASE_API(如 /dev-api);
  3. 拼接临时地址baseURL + url 得到临时地址(如 /dev-api/system/user/list);
  4. 代理转发 :通过 Vite 代理配置,将临时地址转发到真实后端地址(如 http://localhost:8080/system/user/list)。

这种设计的优势是:

  • 环境隔离:开发/生产环境的基础地址可通过配置文件灵活切换;
  • 跨域解决:通过代理避免前端直接请求后端的跨域问题;
  • 路径统一管理:接口路径无需硬编码完整地址,便于维护。

实战环节

2.Vue3 + Ruoyi 前后端联调实战指南:从功能拆解到问题解决

在 Ruoyi-Vue3 项目开发中,"前后端联调"是衔接前端页面与后端接口的核心环节,尤其在用户管理、角色管理等核心模块中,需明确联调流程、对齐参数规范,并高效解决跨域、Token 过期等常见问题。本文以 用户管理模块角色管理模块 为例,拆解完整联调流程,并针对性解决联调痛点。

一、联调前置准备:明确"3个约定"

联调前需与后端开发者对齐基础规范,避免后期因"约定不一致"反复修改:

约定类型 核心内容 Ruoyi 适配建议
接口地址约定 后端接口前缀(如 /system)、请求方法(GET/POST/DELETE) 前端统一在 src/api/system/ 目录下按模块封装(如 user.js 对应用户接口,role.js 对应角色接口)
参数格式约定 - GET 请求:参数拼在 URL 后(?pageNum=1&pageSize=10) - POST 请求:参数放在请求体(JSON 格式) - 分页参数:统一用 pageNum(当前页)、pageSize(每页条数) 前端用 reactive 定义 queryParams 存储分页+筛选参数,调用接口时直接传递
响应格式约定 后端统一返回 JSON 格式,包含 code(状态码)、msg(提示信息)、data(业务数据): json{ "code": 200, "msg": "success", "data": { "rows": [], "total": 100 } } 前端 request.js 已封装响应拦截器,自动处理 code 判定(如 code!==200 则抛错)

二、实战1:用户管理模块联调(核心流程拆解)

用户管理模块是 Ruoyi 最基础的模块,包含"查询用户列表、新增用户、编辑用户、删除用户"4个核心接口,联调流程具有通用性。

步骤1:前端封装接口(API 模块)

src/api/system/user.js 中,按后端接口定义封装函数(需与后端对齐 URL、请求方法、参数名):

javascript 复制代码
import request from '@/utils/request';

// 1. 查询用户列表(GET 请求,参数:分页+筛选条件)
export function listUser(query) {
  return request({
    url: '/system/user/list', // 后端接口路径
    method: 'get',
    params: query // GET 参数(自动拼 URL)
  });
}

// 2. 新增用户(POST 请求,参数:用户信息)
export function addUser(data) {
  return request({
    url: '/system/user',
    method: 'post',
    data: data // POST 参数(放请求体)
  });
}

// 3. 编辑用户(PUT 请求,参数:用户ID+更新信息)
export function updateUser(data) {
  return request({
    url: '/system/user',
    method: 'put',
    data: data
  });
}

// 4. 删除用户(DELETE 请求,参数:用户ID)
export function delUser(userId) {
  return request({
    url: '/system/user/' + userId, // 路径传参
    method: 'delete'
  });
}

步骤2:前端页面调用接口(组合式 API)

在用户列表页面 src/views/system/user/index.vue 中,用组合式 API 调用接口,实现"数据渲染+交互逻辑":

vue 复制代码
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { listUser, delUser } from '@/api/system/user'; // 导入接口

// 1. 响应式数据定义(与后端参数对齐)
const loading = ref(false); // 加载状态
const total = ref(0); // 总条数
const queryParams = reactive({ // 分页+筛选参数
  pageNum: 1,
  pageSize: 10,
  username: '', // 筛选条件:用户名
  status: ''    // 筛选条件:用户状态
});
const tableData = reactive([]); // 表格数据

// 2. 页面初始化加载列表(onMounted 钩子)
onMounted(() => {
  getList();
});

// 3. 查询用户列表(核心方法)
const getList = () => {
  loading.value = true;
  listUser(queryParams).then(res => {
    tableData.length = 0; // 清空旧数据
    tableData.push(...res.data.rows); // 赋值新数据(res.data 对应后端 data 字段)
    total.value = res.data.total; // 赋值总条数
  }).catch(err => {
    ElMessage.error('查询失败:' + err.message); // 错误提示
  }).finally(() => {
    loading.value = false; // 关闭加载
  });
};

// 4. 删除用户(调用删除接口)
const handleDelete = (row) => {
  ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
    type: 'warning'
  }).then(() => {
    delUser(row.userId).then(() => {
      ElMessage.success('删除成功');
      getList(); // 重新查询列表
    });
  });
};
</script>

<template>
  <!-- 筛选表单 -->
  <el-form :model="queryParams" inline @submit.prevent="getList">
    <el-form-item label="用户名">
      <el-input v-model="queryParams.username" placeholder="请输入" clearable />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="getList">查询</el-button>
    </el-form-item>
  </el-form>

  <!-- 用户表格 -->
  <el-table :data="tableData" v-loading="loading">
    <el-table-column label="用户名" prop="username" />
    <el-table-column label="状态" prop="status">
      <template #default="scope">{{ scope.row.status === '0' ? '启用' : '禁用' }}</template>
    </el-table-column>
    <el-table-column label="操作">
      <template #default="scope">
        <el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>

  <!-- 分页组件 -->
  <el-pagination
    :current-page="queryParams.pageNum"
    :page-size="queryParams.pageSize"
    :total="total"
    @current-change="(val) => { queryParams.pageNum = val; getList(); }"
  />
</template>

步骤3:后端接口适配(联调对齐)

后端需基于 Ruoyi 后端框架(Spring Boot)实现对应接口,确保与前端约定一致:

  1. 查询用户列表 :接收 pageNumpageSizeusername 等参数,返回 rows(列表数据)和 total(总条数);
  2. 删除用户 :接收路径参数 userId,执行删除逻辑后返回 code:200
  3. 关键注解:用 @GetMapping/@PostMapping/@DeleteMapping 标注请求方法,用 @RequestParam 接收 GET 参数,@RequestBody 接收 POST 请求体。

步骤4:联调测试(验证功能)

  1. 启动服务 :前端 npm run dev,后端启动 Spring Boot 服务;
  2. 抓包验证 :打开浏览器 F12 → Network 面板,查看接口请求:
    • 检查 请求地址 :是否拼接正确(如 http://localhost:8080/dev-api/system/user/list);
    • 检查 请求参数:GET 参数是否在 URL 后,POST 参数是否在 Request Body 中;
    • 检查 响应数据 :是否符合 code+msg+data 格式,data.rows 是否为数组;
  3. 功能验证:测试"查询、新增、删除"是否正常(如删除后表格数据刷新,提示"删除成功")。

三、实战2:角色管理模块联调(重点:参数嵌套场景)

角色管理模块涉及"角色绑定权限"的嵌套参数(如 roleId + menuIds 数组),需处理"前端参数格式"与"后端接收方式"的对齐问题。

核心场景:角色绑定权限(前端传数组参数)

  1. 前端接口封装src/api/system/role.js):
javascript 复制代码
// 角色绑定权限(POST 请求,参数:roleId + menuIds 数组)
export function assignRoleMenu(data) {
  return request({
    url: '/system/role/assignMenu',
    method: 'post',
    data: data // data 格式:{ roleId: 1, menuIds: [1,2,3] }
  });
}
  1. 前端页面调用
vue 复制代码
<script setup>
import { reactive } from 'vue';
import { assignRoleMenu } from '@/api/system/role';

// 嵌套参数:roleId + menuIds 数组
const form = reactive({
  roleId: 1, // 当前角色ID
  menuIds: [] // 选中的菜单ID数组(如 [1,2,3])
});

// 提交绑定权限
const handleSubmit = () => {
  assignRoleMenu(form).then(() => {
    ElMessage.success('权限绑定成功');
  });
};
</script>
  1. 后端接口接收 (关键:用 @RequestBody 接收嵌套对象):
java 复制代码
@RestController
@RequestMapping("/system/role")
public class SysRoleController {

  // 角色绑定权限接口
  @PostMapping("/assignMenu")
  public AjaxResult assignRoleMenu(@RequestBody SysRoleMenuDTO roleMenuDTO) {
    // roleMenuDTO 包含 roleId(Long 类型)和 menuIds(Long[] 数组)
    return AjaxResult.success(roleService.assignRoleMenu(roleMenuDTO));
  }
}

// 数据传输对象(DTO):与前端参数名完全对齐
@Data
public class SysRoleMenuDTO {
  private Long roleId;
  private Long[] menuIds; // 接收前端 menuIds 数组
}

四、联调常见问题与解决方案(实战痛点)

联调中最容易遇到 跨域、Token 过期、参数不匹配、接口报错 四类问题,以下是针对性解决方案:

问题1:跨域错误(Access to XMLHttpRequest at ... from origin ... has been blocked)

原因:

浏览器的"同源策略"限制------前端服务(如 http://localhost:80)与后端服务(如 http://localhost:8080)的"协议、域名、端口"不同,直接请求会被拦截。

解决方案(2种方式,优先选方式1):
  1. 前端 Vite 代理(推荐,开发环境用)

    vite.config.js 中配置代理,将前端 dev-api 前缀的请求转发到后端地址:

    javascript 复制代码
    export default defineConfig({
      server: {
        proxy: {
          '/dev-api': { // 前端请求前缀
            target: 'http://localhost:8080', // 后端真实地址
            changeOrigin: true, // 允许跨域
            rewrite: (path) => path.replace(/^\/dev-api/, '') // 去掉前缀(关键)
          }
        }
      }
    });

    配置后,前端请求 http://localhost:80/dev-api/system/user/list 会被转发为 http://localhost:8080/system/user/list,避免跨域。

  2. 后端 CORS 配置(生产环境用)

    后端通过 Spring 配置允许跨域,在 Ruoyi 后端 config/CorsConfig.java 中添加:

    java 复制代码
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 所有接口
                .allowedOrigins("*") // 允许所有前端地址(生产环境需指定具体域名)
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的请求方法
                .allowedHeaders("*") // 允许的请求头
                .maxAge(3600); // 预检请求有效期
      }
    }

问题2:Token 过期/未登录(响应 code:401,msg:"未登录或登录已过期")

原因:
  • 前端未传 Token(登录后未存储 Token 到 Pinia);
  • Token 已过期(后端 JWT 过期时间通常为 2 小时)。
解决方案:
  1. 确保登录后存储 Token

    Ruoyi 登录页面(src/views/login/index.vue)已封装逻辑,登录成功后会将 Token 存储到 Pinia 的 userStore

    javascript 复制代码
    // 登录成功后存储 Token
    const userStore = useUserStore();
    userStore.setToken(res.data.token); // 调用 Pinia 方法存储
  2. 确保请求自动携带 Token

    Ruoyi request.js 已封装请求拦截器,自动从 Pinia 取 Token 并添加到请求头:

    javascript 复制代码
    // 请求拦截器
    service.interceptors.request.use(config => {
      const userStore = useUserStore();
      if (userStore.token) {
        // 添加 Token 到 Authorization 头(后端需按此头解析)
        config.headers['Authorization'] = 'Bearer ' + userStore.token;
      }
      return config;
    });
  3. Token 过期处理

    request.js 响应拦截器中添加"Token 过期跳转登录页"逻辑:

    javascript 复制代码
    // 响应拦截器
    service.interceptors.response.use(
      response => { ... },
      error => {
        // 若响应码为 401,说明 Token 过期
        if (error.response && error.response.status === 401) {
          const userStore = useUserStore();
          userStore.logout(); // 清空 Token
          window.location.href = '/login'; // 跳转到登录页
        }
        return Promise.reject(error);
      }
    );

问题3:参数不匹配(后端报"参数缺失"或"类型转换错误")

常见场景与解决:
场景 原因 解决方案
后端报"Required Integer parameter 'pageNum' is not present" 前端 GET 请求参数名与后端 @RequestParam 不一致(如前端写 pageNum,后端写 pageNo 1. 前端修改 queryParams 中的参数名,与后端对齐; 2. 后端在 @RequestParam 中指定别名,如 @RequestParam("pageNum") Integer pageNo
后端报"JSON parse error: Cannot deserialize value of type java.lang.Long from String" 前端传的参数类型与后端不一致(如前端传字符串 userId: "1",后端接收 Long userId 1. 前端确保参数类型正确(如 userId: 1,不用引号); 2. 后端用 @JsonFormat 兼容类型,如 @JsonFormat(shape = JsonFormat.Shape.STRING) private Long userId
后端接收数组参数为 null(如 menuIds 数组) 前端传参格式错误(如传 menuIds: "1,2,3" 字符串,后端接收 Long[] menuIds 前端确保传数组格式:menuIds: [1,2,3],且后端用 @RequestBody 接收

问题4:接口报错(后端报 code:500,前端提示"服务器内部错误")

排查步骤:
  1. 查看后端日志:优先看后端控制台输出,定位错误原因(如 SQL 语法错误、空指针异常);
  2. 抓包看请求参数:在浏览器 Network 面板查看"Request Payload",确认参数是否正确传递;
  3. 用 Postman 测试 :直接用 Postman 调用后端接口(如 http://localhost:8080/system/user/list),若 Postman 也报错,说明是后端接口问题;若 Postman 正常,说明是前端传参或请求方式问题。

五、联调总结:高效协作的"3个技巧"

  1. 优先用接口文档对齐 :提前用 Swagger(Ruoyi 后端默认集成)生成接口文档(访问 http://localhost:8080/doc.html),前端按文档封装接口,避免口头约定;
  2. 善用抓包工具:浏览器 F12 Network 面板是联调"神器",能快速定位"请求地址、参数、响应"是否正常;
  3. 先定位问题归属:遇到错误时,先判断是"前端问题"(如参数没传对、请求没发出去)还是"后端问题",甚至可以详细展开。
相关推荐
JackieDYH6 分钟前
vue3中reactive和ref如何使用和区别
前端·javascript·vue.js
ZZHow10241 小时前
React前端开发_Day4
前端·笔记·react.js·前端框架·web
前端开发爱好者1 小时前
弃用 html2canvas!快 93 倍的截图神器
前端·javascript·vue.js
ss2731 小时前
手写MyBatis第39弹:深入MyBatis BatchExecutor实现原理与最佳实践
前端·javascript·html
leon_teacher2 小时前
HarmonyOS权限管理应用
android·服务器·前端·javascript·华为·harmonyos
lumi.2 小时前
HarmonyOS image组件深度解析:多场景应用与性能优化指南(2.4详细解析,完整见uniapp官网)
前端·javascript·小程序·uni-app·html·css3
OEC小胖胖2 小时前
动态UI的秘诀:React中的条件渲染
前端·react.js·ui·前端框架·web
dreams_dream3 小时前
django注册app时两种方式比较
前端·python·django
liangshanbo12155 小时前
Speculation Rules API
前端·javascript·html
石国旺5 小时前
前端javascript在线生成excel,word模板-通用场景(免费)
前端·javascript·excel