RuoYi前后端分离改造:Vue+ElementUI集成方案
2026-02-04 04:11:14作者:咎岭娴Homer
一、改造背景与痛点分析
在传统的Web开发模式中,RuoYi框架采用SpringBoot+Thymeleaf的服务端渲染架构,这种架构在开发效率和用户体验上存在诸多痛点:
- 开发效率低下:前后端代码耦合紧密,前端开发依赖后端环境,无法实现并行开发
- 用户体验受限:页面刷新频繁,交互体验差,无法满足现代Web应用的响应式需求
- 扩展性不足:服务端渲染难以支持多端适配,移动端开发需要额外适配
- 维护成本高:前后端代码混杂,后期维护和功能迭代困难
本文将详细介绍如何将RuoYi框架改造为前后端分离架构,采用Vue+ElementUI作为前端技术栈,实现前后端解耦,提升开发效率和用户体验。
二、技术栈选型
2.1 后端技术栈
| 技术 | 版本 | 说明 |
|---|---|---|
| SpringBoot | 2.5.15 | 应用开发框架 |
| Shiro | 1.13.0 | 安全框架 |
| MyBatis | 3.5.9 | ORM框架 |
| Swagger | 3.0.0 | API文档生成工具 |
| JWT | 0.11.5 | 身份认证令牌 |
| Maven | 3.6.3 | 项目构建工具 |
2.2 前端技术栈
| 技术 | 版本 | 说明 |
|---|---|---|
| Vue | 3.2.37 | JavaScript框架 |
| Vue Router | 4.0.16 | 路由管理 |
| Vuex | 4.0.2 | 状态管理 |
| Element Plus | 2.2.17 | UI组件库 |
| Axios | 0.27.2 | HTTP客户端 |
| Vite | 3.0.7 | 前端构建工具 |
三、改造总体架构
flowchart TD
A[客户端] -->|HTTP/HTTPS| B[负载均衡]
B --> C[前端应用(Vue+ElementUI)]
C -->|RESTful API| D[后端服务(SpringBoot)]
D --> E[数据库]
D --> F[缓存]
D --> G[消息队列]
3.1 架构说明
- 客户端层:用户使用的浏览器或移动设备
- 前端应用层:基于Vue+ElementUI构建的单页应用(SPA)
- 后端服务层:提供RESTful API的SpringBoot应用
- 数据持久层:数据库、缓存等存储服务
四、后端改造步骤
4.1 添加依赖
修改pom.xml文件,添加JWT和CORS支持相关依赖:
<!-- JWT支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- CORS支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
4.2 配置CORS支持
创建CORS配置类,解决跨域问题:
package com.ruoyi.framework.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许的源
config.addAllowedOriginPattern("*");
// 允许的请求头
config.addAllowedHeader("*");
// 允许的请求方法
config.addAllowedMethod("*");
// 允许携带Cookie
config.setAllowCredentials(true);
// 有效时长
config.setMaxAge(3600L);
// 配置路径
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
4.3 集成JWT认证
4.3.1 创建JWT工具类
package com.ruoyi.common.utils.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private long expire;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 生成JWT令牌
*/
public String generateToken(String username) {
Date now = new Date();
Date expiration = new Date(now.getTime() + expire * 1000);
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析JWT令牌
*/
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*/
public String getUsernameFromToken(String token) {
return parseToken(token).get("username", String.class);
}
/**
* 验证令牌是否过期
*/
public boolean isTokenExpired(String token) {
Date expiration = parseToken(token).getExpiration();
return expiration.before(new Date());
}
}
4.3.2 修改Shiro配置
修改ShiroConfig.java,替换传统的Session认证为JWT认证:
// 省略其他代码...
/**
* 安全管理器
*/
@Bean
public SecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(userRealm);
// 禁用记住我功能
securityManager.setRememberMeManager(null);
// 注入缓存管理器
securityManager.setCacheManager(getEhCacheManager());
// 禁用默认Session管理
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* JWT过滤器
*/
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter();
}
/**
* Shiro过滤器配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 匿名访问不鉴权注解列表
List<String> permitAllUrl = SpringUtils.getBean(PermitAllUrlProperties.class).getUrls();
if (StringUtils.isNotEmpty(permitAllUrl)) {
permitAllUrl.forEach(url -> filterChainDefinitionMap.put(url, "anon"));
}
// 添加JWT过滤器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", jwtFilter());
// ... 其他过滤器
// 所有请求需要认证
filterChainDefinitionMap.put("/**", "jwt,kickout,onlineSession,syncOnlineSession,csrfValidateFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
4.4 改造控制器为RESTful风格
将传统的@Controller改造为@RestController,统一返回AjaxResult对象:
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController {
@Autowired
private ISysUserService sysUserService;
/**
* 获取用户列表
*/
@RequiresPermissions("system:user:list")
@PostMapping("/list")
public TableDataInfo list(SysUser user) {
startPage();
List<SysUser> list = sysUserService.selectSysUserList(user);
return getDataTable(list);
}
/**
* 获取用户详情
*/
@RequiresPermissions("system:user:query")
@GetMapping(value = "/{userId}")
public AjaxResult getInfo(@PathVariable Long userId) {
return success(sysUserService.selectSysUserById(userId));
}
/**
* 新增用户
*/
@RequiresPermissions("system:user:add")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysUser user) {
if (UserConstants.NOT_UNIQUE.equals(sysUserService.checkUserNameUnique(user.getUserName()))) {
return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
} else if (StringUtils.isNotEmpty(user.getPhonenumber())
&& UserConstants.NOT_UNIQUE.equals(sysUserService.checkPhoneUnique(user))) {
return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
} else if (StringUtils.isNotEmpty(user.getEmail())
&& UserConstants.NOT_UNIQUE.equals(sysUserService.checkEmailUnique(user))) {
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setCreateBy(getUsername());
return toAjax(sysUserService.insertSysUser(user));
}
// 其他方法...
}
五、前端项目搭建
5.1 创建Vue项目
# 使用npm创建Vue项目
npm create vite@latest ruoyi-ui -- --template vue
cd ruoyi-ui
npm install
# 安装Element Plus
npm install element-plus --save
# 安装Axios
npm install axios --save
# 安装Vue Router
npm install vue-router@4 --save
# 安装Vuex
npm install vuex@4 --save
5.2 项目目录结构
ruoyi-ui/
├── public/
├── src/
│ ├── api/ # API请求
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── styles/ # 全局样式
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 应用入口
│ └── main.js # 入口文件
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
├── package.json # 项目依赖
└── vite.config.js # Vite配置
5.3 配置Axios
创建src/utils/request.js:
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import store from '@/store';
import { getToken } from '@/utils/auth';
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000
});
// 请求拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['Authorization'] = 'Bearer ' + getToken();
}
return config;
},
error => {
console.error('request error:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data;
// 状态码不是20000的情况
if (res.code !== 200) {
ElMessage({
message: res.msg || 'Error',
type: 'error',
duration: 5 * 1000
});
// token过期或没有权限
if (res.code === 401) {
ElMessageBox.confirm('您的登录已过期,请重新登录', '确认', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload();
});
});
}
return Promise.reject(new Error(res.msg || 'Error'));
} else {
return res;
}
},
error => {
console.error('response error:', error);
ElMessage({
message: error.message,
type: 'error',
duration: 5 * 1000
});
return Promise.reject(error);
}
);
export default service;
5.4 配置路由
创建src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router';
import store from '@/store';
import { ElMessage } from 'element-plus';
// 白名单路由
const whiteList = ['/login', '/register'];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue')
},
{
path: '/',
name: 'layout',
component: () => import('@/layout/index.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '首页', icon: 'dashboard' }
}
]
}
]
});
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 存在token
if (store.getters.token) {
if (to.path === '/login') {
next({ path: '/' });
} else {
// 检查是否已加载用户信息
if (store.getters.name) {
next();
} else {
try {
// 获取用户信息
await store.dispatch('user/getInfo');
// 动态生成路由
const accessRoutes = await store.dispatch('permission/generateRoutes', store.getters.roles);
accessRoutes.forEach(route => {
router.addRoute(route);
});
next({ ...to, replace: true });
} catch (error) {
// 清除token并重新登录
await store.dispatch('user/resetToken');
ElMessage.error(error || 'Has Error');
next(`/login?redirect=${to.path}`);
}
}
}
} else {
// 不存在token
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
next(`/login?redirect=${to.path}`);
}
}
});
export default router;
5.5 实现登录功能
创建src/views/login/index.vue:
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<div class="title-container">
<h3 class="title">RuoYi管理系统</h3>
</div>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
prefix-icon="User"
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
autocomplete="off"
show-password
/>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
placeholder="请输入验证码"
prefix-icon="Verification"
autocomplete="off"
style="width: 63%"
/>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" class="remember-me">记住我</el-checkbox>
<el-form-item>
<el-button
:loading="loading"
size="default"
type="primary"
style="width: 100%"
@click="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { getCodeImg } from '@/api/login';
import { ElMessage } from 'element-plus';
const store = useStore();
const router = useRouter();
const loginForm = reactive({
username: '',
password: '',
code: '',
uuid: '',
rememberMe: false
});
const loginRules = reactive({
username: [{ required: true, trigger: 'blur', message: '请输入用户名' }],
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
code: [{ required:
登录后查看全文
热门项目推荐
相关项目推荐
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0133- DDeepSeek-V4-ProDeepSeek-V4-Pro(总参数 1.6 万亿,激活 49B)面向复杂推理和高级编程任务,在代码竞赛、数学推理、Agent 工作流等场景表现优异,性能接近国际前沿闭源模型。Python00
GLM-5.1GLM-5.1是智谱迄今最智能的旗舰模型,也是目前全球最强的开源模型。GLM-5.1大大提高了代码能力,在完成长程任务方面提升尤为显著。和此前分钟级交互的模型不同,它能够在一次任务中独立、持续工作超过8小时,期间自主规划、执行、自我进化,最终交付完整的工程级成果。Jinja00
MiniCPM-V-4.6这是 MiniCPM-V 系列有史以来效率与性能平衡最佳的模型。它以仅 1.3B 的参数规模,实现了性能与效率的双重突破,在全球同尺寸模型中登顶,全面超越了阿里 Qwen3.5-0.8B 与谷歌 Gemma4-E2B-it。Jinja00
MiniMax-M2.7MiniMax-M2.7 是我们首个深度参与自身进化过程的模型。M2.7 具备构建复杂智能体应用框架的能力,能够借助智能体团队、复杂技能以及动态工具搜索,完成高度精细的生产力任务。Python00
MusicFreeDesktop插件化、定制化、无广告的免费音乐播放器TypeScript00
热门内容推荐
最新内容推荐
项目优选
收起
暂无描述
Dockerfile
725
4.66 K
Ascend Extension for PyTorch
Python
597
749
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
425
376
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
992
984
Claude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed.
Get Started
Rust
921
133
昇腾LLM分布式训练框架
Python
160
188
暂无简介
Dart
968
246
deepin linux kernel
C
29
16
Oohos_react_native
React Native鸿蒙化仓库
C++
345
393
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
1.65 K
970