vue+koa+mongoDB社区博客之后台管理系统

862 阅读7分钟

前言

之前我发过一篇关于使用vue2.6+AntdForVue+koa2+mongoose编写的社区博客项目(前台项目的帖子),今天我又继续带来了该社区项目的后台管理系统了。

QQ图片20220709151723.gif

简介

vue iview ui 

该项目是基于vue2.6全家桶 + iview4.X 编写的一个后台管理系统,核心重点是动态路由菜单,可以说是一个简单RBAC模型的后台管理系统吧,非常适合于想了解学习动态路由加载jym,废话不多说,上家伙~~~

在线预览

在线预览后台

测试账号: test@qq.com 密码123456

功能列表

  • 登录
  • 用户管理
  • 评论管理
  • 标签管理
  • 角色管理
  • 菜单管理
  • 文章管理
  • 动态路由
  • 数据权限

SRC重点目录结构介绍

├── assets
├── components # 公共组件
    ├── tables   # 表格组件封装
    ├── SearchForm   # 搜索组件封装
    ├── SeachFormItem   # 搜索组件单组件
├── api # 各模块请求的API
├── config # 项目的参数配置
├── plugins # 插件目录   
├── router  # 路由
├── store   # vuex
├── libs   # 工具类
├── locale  # 国际化配置
    ├── const   # 各模块的变量
└── views   
    ├── Comment # 评论模块
    └── Content # 文章模块
    ├── Menu   # 菜单模块
    ├── Login   # 登录模块
    ├── Role   # 角色模块
    ├── User   # 用户模块

项目重点-动态路由加载

首先动态路由加载的实现大家应该都知道要借助于一个API 那就是addRoutes或者是addRoute,由于在vue3中addRoutes已经被废弃了,所有我在项目里使用的是addRoute这个API了,虽然说我们这项目是个vue2.6的项目,但不影响。

接下来开始实现动态路由,动态 动态 那很明显来说就是说这些路由都是来自于后端返回给前端,由前端拿到数据后调用addRoute把路由加载出来,那么我们就可以想到在路由导航守卫进行作法了

router.beforeEach((to, from, next) => {
  iView.LoadingBar.start()
  const token = getToken()
  if (!token && to.name !== LOGIN_PAGE_NAME) {
    // 未登录且要跳转的页面不是登录页
    next({
      name: LOGIN_PAGE_NAME // 跳转到登录页
    })
  } else if (!token && to.name === LOGIN_PAGE_NAME) {
    // 未登陆且要跳转的页面是登录页
    next() // 跳转
  } else if (token && to.name === LOGIN_PAGE_NAME) {
    // 已登录且要跳转的页面是登录页
    next({
      name: homeName // 跳转到homeName页
    })
  } else {
    const hasPermission = store.state.app.permission && store.state.app.permission.length > 0
    if (hasPermission) {
      next()
    } else {
      getPermission(next, to, from)
    }
  }
})

由上面代码 不难看出,首先我把登录页过滤掉,然后再判断是否已经获取过路由数据,如果已经获取过直接跳转,否则就会执行获取路由数据的方法getPermission

那么getPermission做了什么呢?

function getPermission(next, to, from) {
  getMenuRouter().then((response) => {
    if (response.data && response.data.length > 0) {
      store.dispatch('user/getUserInfo').then(async user => {
        const accessRoutes = await store.dispatch('app/generateRoutes', response.data)
        accessRoutes.forEach(item => {
          router.addRoute(item.name, item)
        })
        next({ ...to, replace: true })
      }).catch(() => {
        setToken('')
        next({
          name: 'login'
        })
      })
    } else {
      // 权限为空,跳到404,并清除掉token
      next({ path: '/404' })
      localStorage.removeItem('permissionList')
      setToken('')
      // router.push('/login')
    }
  }).catch(_ => {
    iView.LoadingBar.finish()
  })
}

其实就是去请求后台接口把路由数据返回来并且调用router.addRoute(item.name, item)加载到我们的路由里。

然后我们再来看看store.dispatch('app/generateRoutes', response.data)这里generateRoutes方法做了什么

generateRoutes({ commit }, permissions) {
  return new Promise(resolve => {
    let accessedRoutes
    if (permissions && permissions.length > 0) {
      accessedRoutes = filterAsyncRoutes(permissions)
    }
    // 解决刷新页面后会调到404页面的BUG
    accessedRoutes.push({ path: '*', redirect: '/401', meta: {
      hideInMenu: true
    }})
    const resultRoutes = accessedRoutes.concat(routers)
    commit('SET_PERMISSION', resultRoutes || [])
    resolve(accessedRoutes || [])
  })
}
// 处理component字段引入组件
const filterAsyncRoutes = (routeTable) => {
  routeTable.forEach(item => {
    item['component'] = loadView(item.component)
    if (item.children && item.children.length > 0) {
      filterAsyncRoutes(item.children)
    } else {
      delete item.children
    }
  })
  return routeTable
}
const loadView = (view) => {
  if (process.env.NODE_ENV === 'development') {
    return (resolve) => require([`@/${view}`], resolve)
  } else {
    // 使用 import 实现生产环境的路由懒加载
    return () => import(`@/${view}`)
  }
}

由上面代码可以看出generateRoutes方法是用于最终生成一份vue-router所需要的一个数组对象格式,如下面这种,相信大家学过vue的都很熟悉

{
    path: '/',
    name: '_home',
    redirect: '/home',
    component: Main,
    meta: {
      hideInMenu: true,
      notCache: true
    },
    children: [
      {
        path: '/home',
        name: 'home',
        meta: {
          hideInMenu: true,
          title: '首页',
          notCache: true,
          icon: 'md-home'
        },
        component: () => import('@/view/single-page/home')
      }
    ]
  }

filterAsyncRoutes方法则是为了处理component字段的,因为我们的路由数据从后端拿回来后是一个路径字符,所以我们需要将它转为组件懒加载方法引入组件。

换句话说就是要把component字段从字符串变成component: () => import('@/view/single-page/home'),而loadView方法就是做转换处理的。

这里我踩了一下坑当时候做的时候,因为一开始我是直接filterAsyncRoutes方法里item['component'] = () => import(url)这样处理,这样处理后,编译时候会报错的,后来一顿面向搜索引擎搜索解决方案说是要向loadView方法那样处理才行(这好像是因为webpack的原因)具体的 大家可以深究下,可以在下面评论分享一下。

至此为此的话动态路由的重点就是这么多了。

项目重点-数据权限拦截(后端koa2中间件)

我们都知道koa框架一个强大原因之一是因为它的中间件,因为它可以帮我们处理很多东西,下面就是koa的洋葱模型

image.png

所以我们的数据权限肯定也是在中间里做处理了,而这块的逻辑都在我之前发的社区前台帖子里有一个后台项目的源码 大家可以去下载下来看看(前台项目的帖子)

打开后台项目后找到src/common/authMiddleWare.js就是数据权限拦截的中间件了

接下来 我们看看里面到底干了些什么

export default async(ctx, next) => {
  const headers = ctx.headers.authorization
  if (headers) { // 携带了token
    try {
      // 检查当前id是不是属于超级管理员
      var tokenInfo = getTokenInfo(ctx)
      if (tokenInfo.userId) {
        const adminList = JSON.parse(await getValue('adminList'))
        if (adminList.indexOf(tokenInfo.userId) !== -1) { // 如果是超管账号直接放行,下面就没必要再验证任何东西了
          ctx.isAdmin = true
          await next()
          return
        } else {
          ctx.isAdmin = false
        }
      } else {
        ctx.throw(401)
      }
    } catch (error) {
      error.status = 401
      ctx.throw(error)
    }
  }
  // 过滤掉公共不需要数据校验的接口
  const publicPath = config.AUTH_WHILE_lIST
  if (publicPath.some(path => path.test(ctx.url))) {
    await next()
    return
  }
  const isPermission = await getResourceById(tokenInfo.userId, ctx.url)
  if (isPermission) {
    await next()
  } else {
    ctx.throw(501)
  }
}

从上面代码来看 我首先对是判断请求的来源是否携带了token,如果携带了token那么我就会校验当前的token是否有效并且是否属于白名单(或者说管理员名单)const adminList = JSON.parse(await getValue('adminList')) 如果是白名单成员直接就next往下走了,否则标记其为非白名单成员(用于后面部分接口做判断处理,如获取菜单,是超管直接就获取全部菜单数据)。

处理完token判断之后就是过滤掉一些不需要做数据校验的请求路径

const publicPath = config.AUTH_WHILE_lIST
    if (publicPath.some(path => path.test(ctx.url))) {
        await next()
    return
}

最后就是判断其是否有权限访问了,没有权限直接抛501异常。 而getResourceById方法主要是做了什么呢?

// 根据用户ID获取出用户ID具有的资源权限数组并且判断是否具有权限
const getResourceById = async (userId, url) => {
  // 生成整个菜单树
  const menu = await Menus.find({})
  const menuToJson = menu.map(item => item.toJSON())
  const menuTree = getMenuTree(menuToJson, null)
  // 查找用户对应的角色权限
  const userRole = (await User.findById(userId, 'role')).role
  // 用户拥有的所有权限ID
  let userMenuRole = []
  for (const role of userRole) {
    const roleRecords = await Roles.findOne({ code: role })
    userMenuRole = [...userMenuRole, ...roleRecords.menus]
  }
  // 去掉重复的权限id-因为有可能一个账号多个角色
  userMenuRole = [...new Set(userMenuRole)]
  const menuResourcePath = getOperations(menuTree, 'path', userMenuRole)
  if (menuResourcePath.indexOf(url) !== -1) {
    return true
  } else {
    return false
  }
}

// 根据用户权限数组取出资源权限的path
const getOperations = (treeData, property, userRole) => {
  let result = []
  treeData.forEach(item => {
    if (item.operations && item.operations.length > 0) {
      item.operations.forEach(ope => {
        if (userRole.indexOf(ope._id + '') !== -1) {
          result.push(ope[property])
        }
      })
    }
    if (item.children && item.children.length > 0) {
      result = result.concat(getOperations(item.children, property, userRole))
    }
  })
  return result
}

其实就是取出用户对应的角色,并且根据角色找到对应的权限,生成一个权限数组,然后根据用户当前需要访问的路径去生成出来的权限数据找存在该路径,存在则证明有权限,否则就是没权限了。

项目运行方法

把项目从仓库中克隆下来,然后使用npm i安装依赖,安装完成之后使用npm run dev 即可(前提是需要先把后端项目运行起来,这个的话看我之前发的帖子就有说如何部署运用后端项目帖子

项目展示

image.png

image.png

image.png

image.png

image.png

最后

至此为止差不多就完事了,其实这项目比较有趣就是我上面提到的动态路由以及数据权限部分了,其他部分基本都是一些简单管理页面以及功能而已。如果该文章对大家有帮助的话就帮忙点赞+转发+关注吧~谢谢~~~

源码附上

后台管理前端源码

后端项目API源码

前台项目的帖子

记得给个Star

QQ图片20220709170507.gif