草庐IT

vue-element-plus-admin整合后端实战——实现系统登录、缓存用户数据、实现动态路由

学海无涯,行者无疆 2024-05-02 原文

目标

整合vue-element-plus-admin前端框架,作为开发平台的前端。

准备工作

前端选用vue-element-plus-admin,地址 https://gitee.com/kailong110120130/vue-element-plus-admin
首先clone项目,然后整合到开发平台中去。这是一个独立的前端的项目,而我将其放到后端项目根目录下,即建一个huayuan-web的目录,将vue-element-plus-admin目录下的内容放进去,相当于将前端项目视为整个工程项目的一个模块。
为什么要这么做呢?原因也简单,从架构上而言,前后端是分离的,不过当前这个平台前后端都是我在做,因此开发模式并不是前后端分别开发,通过mock数据和联调再整合到一块去,而是对于一个功能,例如组织机构管理,往往是后端和前端是一块做的。这样从开发上,从Git单次提交上,都是对于一个功能的完整处理。

既然是将前端项目视为整个工程的一个模块,是一个git仓库统一管理,那么前端项目下就不应该还存在.git目录了。如果直接删除,运行pnpm install会报错,原因是使用了husky,而husky是依赖git 才能安装。
经过几次尝试,做了以下处理。先clone,然后执行pnpm install,确保前端项目能运转起来。然后执行 pnpm unistall husky,既卸载掉husky,然后再删除掉前端项目根目录下的.git目录,这样既保证了前端项目能正常运转,又将其纳入了整个工程。

调用后端服务

完成了基本的源码下载和整合到项目工程,接下来考虑的就是怎么实现前端调用后端服务。
前端使用默认的localhost:4000,后端服务的地址是localhost:8080,首先解决前后端联通性问题。
首先调整的是vite.config.ts中的server节点下的proxy设置,具体如下:

server: {
      port: 4000,
      proxy: {
        //  系统管理模块
        '/system': {
          target: env.VITE_BASE_URL,
          changeOrigin: true         
        }        
      }
}

即把路径以/system起始的请求转发到后端,其中env.VITE_BASE_URL是在local.env中定义:

# 环境
NODE_ENV=development

# 请求路径
VITE_BASE_URL='http://localhost:8080'

# 接口前缀
VITE_API_BASEPATH=/my-api

# 打包路径
VITE_BASE_PATH=/

# 标题
VITE_APP_TITLE=ElementAdmin

系统登录

前后端联通后,首先实现的功能,肯定是登录。
结果看了下官方文档,只有安装、目录结构和功能组件的大概介绍,并没有如何跟后端整合的介绍。百度搜了下,结果都是基vue-element-admin的,也就是vue2.0+Element UI 的框架。看来新技术与框架只能自己来开荒了,通过源码阅读与摸索来实现。
前端框架能独立运行,输入账号密码后完成登录,进入系统首页,实际上使用的是mock数据,登录方法位于mock/user/index.ts中。

import { config } from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'

const { result_code } = config

const timeout = 1000

const List: {
  username: string
  password: string
  role: string
  roleId: string
  permissions: string | string[]
}[] = [
    {
      username: 'admin',
      password: 'admin',
      role: 'admin',
      roleId: '1',
      permissions: ['*.*.*']
    },
    {
      username: 'test',
      password: 'test',
      role: 'test',
      roleId: '2',
      permissions: ['example:dialog:create', 'example:dialog:delete']
    }
  ]

export default [
  // 列表接口
  {
    url: '/user/list',
    method: 'get',
    response: ({ query }) => {
      const { username, pageIndex, pageSize } = query

      const mockList = List.filter((item) => {
        if (username && item.username.indexOf(username) < 0) return false
        return true
      })
      const pageList = mockList.filter(
        (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
      )

      return {
        code: result_code,
        data: {
          total: mockList.length,
          list: pageList
        }
      }
    }
  },
  // 登录接口
  {
    url: '/user/login',
    method: 'post',
    timeout,
    response: ({ body }) => {
      const data = body
      let hasUser = false
      for (const user of List) {
        if (user.username === data.username && user.password === data.password) {
          hasUser = true
          return {
            code: result_code,
            data: user
          }
        }
      }
      if (!hasUser) {
        return {
          code: '500',
          message: '账号或密码错误'
        }
      }
    }
  },
  // 退出接口
  {
    url: '/user/loginOut',
    method: 'get',
    timeout,
    response: () => {
      return {
        code: result_code,
        data: null
      }
    }
  }
] as MockMethod[]

可以看到,逻辑比较简单,无非是比对下预先设置的账号密码,如一致则直接构造一个admin用户返回。

接下来,我来改造下,直接调用后端服务。
系统后端使用SpringSecurity框架,配置的登录路径是/system/user/login。
修改api/login/index.ts中的loginApi即可

export const loginApi = (data: UserType) => {
  return request.post({
    url: '/system/user/login?username=' + data.username + '&password=' + data.password,
    data
  })
}

上面把账号密码通过url参数的方式传入后端,实际是SpringSecurity的限制。SpringSecurity内置的过滤器,不从post请求的body里取数据,所以这地方做了点小处理。
完成上述调整后,使用浏览器调试功能,可以看到真正向后端发起请求了,并且后端返回了登录成功后的数据。

缓存用户数据

vue-element-plus-admin框架对用户信息做了定义,与我的设计差异较大,这地方也做了比较大的改造。
用户信息如下:

import { store } from '../index'
import { defineStore } from 'pinia'
import { useCache } from '@/hooks/web/useCache'
import { USER_KEY } from '@/constant/common'
const { wsCache } = useCache()

interface UserState {
  account: string
  name: string
  forceChangePassword: string
  id: string
  token: string
  buttonPermission: string[]
  menuPermission: string[]
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    account: '',
    name: '',
    forceChangePassword: '',
    id: '',
    token: '',
    buttonPermission: [],
    menuPermission: []
  }),
  getters: {
    getAccount(): string {
      return this.account
    }
  },
  actions: {
    async setUserAction(user) {
      this.account = user.account
      this.name = user.name
      this.forceChangePassword = user.forceChangePassword
      this.id = user.id
      this.token = user.token
      this.buttonPermission = user.buttonPermission
      this.menuPermission = user.menuPermission
      wsCache.set(USER_KEY, user)
    },
    async clear() {
      wsCache.clear()
      this.resetState()
    },
    resetState() {
      this.account = ''
      this.name = ''
      this.forceChangePassword = ''
      this.id = ''
      this.token = ''
      this.buttonPermission = []
      this.menuPermission = []
    }
  }
})

export const useUserStoreWithOut = () => {
  return useUserStore(store)
}

包括标识、账号、姓名、是否强制修改密码、令牌、菜单权限数组和按钮权限数组这几个关键字段。
在用户登录成功后,将后端返回的用户信息缓存到浏览器SessionStorage中。

// 登录
const signIn = async () => {
  const formRef = unref(elFormRef)
  await formRef?.validate(async (isValid) => {
    if (isValid) {
      loading.value = true
      const { getFormData } = methods
      const formData = await getFormData<UserType>()

      try {
        const res = await loginApi(formData)

        if (res) {
          // 保存用户信息
          userStore.setUserAction(res.data)

          // 是否使用动态路由}
      } finally {
        loading.value = false
      }
    }
  })

实现动态路由

接下来就是最复杂的一块功能改造了,即实现动态路由,根据后端返回的菜单权限,动态构造出前端路由来。
在vue-elment-ui框架里,这块功能实际是没有的,当初我自己费了不少劲最终实现了。
在vue-element-plus-admin框架中里,这块功能有了支持,预留了三种模式:
1.静态路由:也就是默认的前端独立运行模式看到的效果,所有菜单固化,预先配置好。
2.前端控制:只初始化通用的路由至路由表中。对于动态路由,在前端固定写死对应的角色。用户登录后,通过角色去遍历动态路由表,获取该角色可以访问的路由表,生成动态路由表,再通过 router.addRoutes 添加到路由实例。
3.后端控制:通过接口动态生成路由表,且遵循一定的数据结构返回。前端根据需要处理该数据为可识别的结构,再通过 router.addRoutes 添加到路由实例。
上面三种模式,第一种明显不可用,第二种勉强可用,但缺点也很明显,灵活性不够,如果服务端改动角色,前端也需要跟着改动,并且排序什么的都需要前端控制。第三种才是我们真正想要的,后端调整权限,前端无需修改,自动动态获取,处理后形成系统菜单。

虽然前端框架预留了口子,但是调整起来仍然比较复杂,下面具体说说。
首先得改一个全局变量,将store/modules/app.ts 中的dynamicRouter 设置为 true,即启用动态路由,框架在多处会首先判断该配置的取值,进行不同的处理。
其次,是修改store/modules/permission.ts 中的generateRoutes方法。

generateRoutes(
      type: 'admin' | 'test' | 'none',
      routers?: AppCustomRouteRecordRaw[] | string[]
    ): Promise<unknown> {
      return new Promise<void>((resolve) => {
        // TODO:前后端动态路由临时添加固定路由,待去除
        let routerMap: AppRouteRecordRaw[] = asyncRouterMap
        if (type === 'admin') {
          // 后端过滤菜单
          routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[]).concat(routerMap)
        } else if (type === 'test') {
          // 模拟前端过滤菜单
          routerMap = generateRoutesFn1(cloneDeep(asyncRouterMap), routers as string[])
        } else {
          // 直接读取静态路由表
          routerMap = cloneDeep(asyncRouterMap)
        }
        // 动态路由,404一定要放到最后面
        this.addRouters = routerMap.concat([
          {
            path: '/:path(.*)*',
            redirect: '/404',
            name: '404Page',
            meta: {
              hidden: true,
              breadcrumb: false
            }
          }
        ])
        // 渲染菜单的所有路由
        this.routers = cloneDeep(constantRouterMap).concat(routerMap)
        resolve()
      })
    },

这个方法有两个参数,第一个是指定模式,admin代表模式三,从后端接口拿到动态路由数据,第二个参数就是后端返回的路由数据。
再次,是将后端返回的路由数据,进行转换处理,成为前端需要的数据结构,需要调整/utils/routerHelper.ts中的
generateRoutesFn2方法。

// 后端控制路由生成
export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
  const res: AppRouteRecordRaw[] = []

  for (const route of routes) {
    const data: AppRouteRecordRaw = {
      path: route.path,
      name: route.name,
      redirect: route.redirect,
      meta: route.meta
    }
    if (route.component) {
      const comModule =
        modules[`../modules/${route.component}.vue`] || modules[`../modules/${route.component}.tsx`]

      const component = route.component as string

      if (!comModule && !component.includes('#')) {
        console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`)
      } else {
        // 动态加载路由文件
        data.component =
          component === '#' ? Layout : component.includes('##') ? getParentLayout() : comModule
      }
    }
    // recursive child routes
    if (route.children) {
      data.children = generateRoutesFn2(route.children)
    }
    res.push(data as AppRouteRecordRaw)
  }
  return res
}

数据处理和转换,跟后端返回的数据结构有关系,特别是动态引入组件部分,需根据自己的情况进行适配调整。

完成上述操作后,动态路由就实现了,回到登录环节,实现加载动态路由,然后进入系统,默认加载第一个能找到的路由。

// 登录
const signIn = async () => {
  const formRef = unref(elFormRef)
  await formRef?.validate(async (isValid) => {
    if (isValid) {
      loading.value = true
      const { getFormData } = methods
      const formData = await getFormData<UserType>()


      try {
        const res = await loginApi(formData)


        if (res) {
          // 保存用户信息
          userStore.setUserAction(res.data)


          // 是否使用动态路由
          if (appStore.getDynamicRouter) {
            const routers = res.data.menuPermission || []
            await permissionStore.generateRoutes('admin', routers).catch(() => {})


            permissionStore.getAddRouters.forEach((route) => {
              addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
            })
            permissionStore.setIsAddRouters(true)
            push({ path: redirect.value || permissionStore.addRouters[0].path })
          } else {
            await permissionStore.generateRoutes('none').catch(() => {})
            permissionStore.getAddRouters.forEach((route) => {
              addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
            })
            permissionStore.setIsAddRouters(true)


            push({ path: redirect.value || permissionStore.addRouters[0].path })
          }
        }
      } finally {
        loading.value = false
      }
    }
  })
}

总结

今天主要介绍了如何对vue-element-plus-admin改造,实现系统登录、缓存用户数据以及动态路由。完成上述操作后,基本实现了前后端的打通工作。

开发平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT

有关vue-element-plus-admin整合后端实战——实现系统登录、缓存用户数据、实现动态路由的更多相关文章

  1. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  2. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. ruby - rails 3 redirect_to 将参数传递给命名路由 - 2

    我没有找到太多关于如何执行此操作的信息,尽管有很多关于如何使用像这样的redirect_to将参数传递给重定向的建议:action=>'something',:controller=>'something'在我的应用程序中,我在路由文件中有以下内容match'profile'=>'User#show'我的表演Action是这样的defshow@user=User.find(params[:user])@title=@user.first_nameend重定向发生在同一个用户Controller中,就像这样defregister@title="Registration"@user=Use

  5. ruby-on-rails - Rails 3,嵌套资源,没有路由匹配 [PUT] - 2

    我真的为这个而疯狂。我一直在搜索答案并尝试我找到的所有内容,包括相关问题和stackoverflow上的答案,但仍然无法正常工作。我正在使用嵌套资源,但无法使表单正常工作。我总是遇到错误,例如没有路线匹配[PUT]"/galleries/1/photos"表格在这里:/galleries/1/photos/1/edit路线.rbresources:galleriesdoresources:photosendresources:galleriesresources:photos照片Controller.rbdefnew@gallery=Gallery.find(params[:galle

  6. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  7. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  8. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  9. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  10. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

随机推荐