首页
/ RuoYi前后端分离改造:Vue+ElementUI集成方案

RuoYi前后端分离改造:Vue+ElementUI集成方案

2026-02-04 04:11:14作者:咎岭娴Homer

一、改造背景与痛点分析

在传统的Web开发模式中,RuoYi框架采用SpringBoot+Thymeleaf的服务端渲染架构,这种架构在开发效率和用户体验上存在诸多痛点:

  1. 开发效率低下:前后端代码耦合紧密,前端开发依赖后端环境,无法实现并行开发
  2. 用户体验受限:页面刷新频繁,交互体验差,无法满足现代Web应用的响应式需求
  3. 扩展性不足:服务端渲染难以支持多端适配,移动端开发需要额外适配
  4. 维护成本高:前后端代码混杂,后期维护和功能迭代困难

本文将详细介绍如何将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:
登录后查看全文
热门项目推荐
相关项目推荐