前言
后端篇一阶段内容已经完成得差不多了,前端篇还没正式开始,这段时间会先转前端篇,就着之前搭建的前端脚手架搭建继续添加后续的内容。本文重点讲一下前端登录与路由模块化思路,顺便会讲一下elementui-admin脚后架的自定义图标的使用。
登录
做为后台管理系统,登录模块是不可缺少的。这里简单说明一下登录模块需要做的事及注意事项。
用户状态
- 未登录
未登录用户,访问所有页面,都会重定向到登录页。
- 已登录
已登录的用户只能看到自己拥有的权限的菜单、按钮,并可对其进行对应操作。
用户状态存储
前后端分离后,已登录的用户需要在本地存储会话信息,常见的方式是使用Cookies存储token。本框架这里也是使用Cookies。当然,除了使用Cookies外,还可以使用h5中的window.localStorage等。
用户状态清除
已登录的用户,如下情况下会清除用户登录状态
- 用户主动退出;
- 接口返回token过期状态码
出现如上两种情况,会调用代码清除本地会话信息,并重定向到登录页重新登录。
登录接口文档
请求地址:
{{api_base_url}}/sys/login
数据类型:
application/json
请求示例:
{
"password": "123456",
"userName": "admin"
}
响应示例:
{
"code": 0, // 返回状态码0,成功
"msg": "登录成功", // 消息描述
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtbGRvbmciLCJleHAiOjE1OTI0OTIyMDEsInVzZXJOYW1lIjoiYWRtaW4iLCJpYXQiOjE1OTI0ODUwMDEsInVzZXJJZCI6MX0.HldmyCcL2EV8rtIeIiYsei963Cb3qIDHJRMOYo0iXkU", // 临时令牌,登录后,其他接口都需要携带该参数
"userId": 1, // 用户id
"userName": "admin", // 用户名
"realName": "蒙立东", // 用户姓名
"avatar": "", // 用户头像
"accessList": [], // 权限标识
"menuList": [] // 菜单集合
}
}
登录模块涉及到改动的文件
src/views/login.vue
登录页面,登录表单,代码片段。
handleLogin() {
// 在这里做表单校验
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
// 校验通过,调用action->user/login,对应src/store/modules/user.js的action.login方法
this.$store.dispatch('user/login', this.loginForm).then(() => {
this.$router.push({ path: this.redirect || '/' })
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
src/store/modules/user.js
vue状态管理,代码片段
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: ''
}
}
const state = getDefaultState()
const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
}
const actions = {
// user login
login({ commit }, userInfo) {
// 这个就是登录那边调用的方法 this.$store.dispatch('user/login', this.loginForm)
const { username, password } = userInfo
return new Promise((resolve, reject) => {
// 这里调用src/api/user.js的login
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
// 设置token
commit('SET_TOKEN', data.token)
// 设置cookies,这里调用的是src/utils/auth.js文件的setToken方法
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户个人信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
// 这里后端返回信息进行修改
const { userName, avatar } = data
commit('SET_NAME', userName)
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// 用户登录系统
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},
src/utils/auth.js
cookies相关操作
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
// 获取 token
export function getToken() {
return Cookies.get(TokenKey)
}
// 设置 token
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
// 删除 token
export function removeToken() {
return Cookies.remove(TokenKey)
}
src/api/user.js
用户登录、退出、获取个人信息接口服务,根据后端接口情况进行修改。
import request from '@/utils/request'
// 登录
export function login(data) {
return request({
url: '/sys/login',
method: 'post',
data: {
// 这里需要简单的修改一下入参username->userName
userName: data.username,
password: data.password
}
})
}
// 获取用户个人信息
export function getInfo(token) {
return request({
url: '/sys/user/info',
method: 'post'
})
}
// 登录系统
export function logout() {
return request({
url: '/sys/logout',
method: 'post'
})
}
src/utils/request.js
http请求工具类,在这里做全局的请求头、请求参数、返回数据处理
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// 存在token,就放到请求头中
// 这里修改一下请求头与后端一致,X-Token->Auth-Token
config.headers['Auth-Token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
// 这里做全局的返回数据处理
response => {
const res = response.data
// 如果状态码不为0,则异常,.
if (res.code !== 0) {
Message({
message: res.msg || '服务器异常',
type: 'error',
duration: 5 * 1000
})
// 这里的状态码可根据后端状态码进行修改
if (res.code === 401) {
// to re-login
MessageBox.confirm('您已经退出了,将离开该页面,确定退出?', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
路由模块化
脚手架把大多数功能都做了,但是对于路由的模块化,还有功能模块的目录分层还是没有做的,这需要我们根据自己项目的情况进行划分,下面是我自己的分层方案,这里只涉及到路由和页面,下一篇讲到具体的CURD样例时才会有完整的分层。
目录结构
├── src/router
├── cms.router.js 内容管理路由
├── index.js 路由主入口
├── oms.router.js 订单管理路由
├── pms.router.js 商品管理路由
└── sys.router.js 系统管理路由
└── views/modules
├── cms 内容管理模块
├── article
└── index.vue
├── category
└── index.vue
├── model
└── index.vue
└── modelFiled
└── index.vue
├── oms 订单管理模块
├── order
└── index.vue
└── orderSetting
└── index.vue
├── pms 商品管理模块
├── brand
└── index.vue
├── product
└── index.vue
└── productCategory
└── index.vue
├── sys 系统管理模块
├── dict
└── index.vue
├── dictItem
└── index.vue
├── menu
└── index.vue
├── role
└── index.vue
└── user
└── index.vue
上面那么多个模块并不是说要立马实现,只是想讲解一下路由模块化与页面模块化。
样例
src/router/sys.router.js
import Layout from '@/layout'
export default [
{
path: '/sys', // 这里的模块化标识与后台菜单管理的权限标识一致
name: 'sys',
meta: {
icon: 'sys',
title: '系统设置',
access: ['admin', 'sys'],
notCache: true,
showAlways: true
},
component: Layout,
children: [
{
path: '/sys/menu/index',
name: 'sys:menu:index', // 与菜单管理权限标识一致
meta: {
icon: '',
title: '菜单管理',
access: ['admin', 'sys:menu:index'],
notCache: true
},
component: (resolve) => {
import('@/views/modules/sys/menu/index.vue').then(m => {
resolve(m)
})
}
},
{
path: '/sys/user/index',
name: 'sys:user:index',
meta: {
icon: '',
title: '用户管理',
access: ['admin', 'sys:user:index'],
notCache: true
},
component: (resolve) => {
import('@/views/modules/sys/user/index.vue').then(m => {
resolve(m)
})
}
},
{
path: '/sys/role/index',
name: 'sys:role:index',
meta: {
icon: '',
title: '角色管理',
access: ['admin', 'sys:role:index'],
notCache: true
},
component: (resolve) => {
import('@/views/modules/sys/role/index.vue').then(m => {
resolve(m)
})
}
},
{
path: '/sys/dict/index',
name: 'sys:dict:index',
meta: {
icon: '',
title: '字典管理',
access: ['admin', 'sys:dict:index'],
notCache: true
},
component: (resolve) => {
import('@/views/modules/sys/dict/index.vue').then(m => {
resolve(m)
})
}
}
]
}
]
src/router/index.js
路由管理主入口,这里使用了webpack_require动态加载路由文件
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
*/
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
hidden: true,
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
}
]
/**
* webpack_require动态加载路由文件
*/
const routersFiles = require.context('./', true, /\.js$/)
const routerList = []
const routers = routersFiles.keys().reduce((modules, routerPath) => {
// set './app.js' => 'app'
const routerName = routerPath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = routersFiles(routerPath)
if (routerName !== 'index') {
routerList.push(...value.default)
}
return routers
}, {})
/**
* 异步路由,需要动态router.addRoutes(accessRoutes)
*/
export const asyncRoutes = [
... routerList,
// 404 page 一定要放在这最后,要不然异步加载,刷新页面没找到就直接跑去constantRoutes的404了
{ path: '*', redirect: {
name: 'm404'
}, hidden: true }
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: [
... constantRoutes,
// 因为还没做到权限,这里先直接放在这,后面做到权限时,再修改
... asyncRoutes
]
})
const router = createRouter()
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
路由拦截器
路由拦截器与请求拦截器差不多,在该脚手架中,主要放在
src/permission.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
// 进入页面前拦截
router.beforeEach(async(to, from, next) => {
// 进度条开始
NProgress.start()
// 重设页面标题
document.title = getPageTitle(to.meta.title)
// 获取token,判断是否已经登录
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 如果已经登录且是登录页,则重定向到首页
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
// 判断用户信息是否存在,如果已经存在,则可以进入页面
if (hasGetUserInfo) {
next()
} else {
try {
// 用户信息不存在,需要去获取
await store.dispatch('user/getInfo')
// 获取成功,则进入页面
next()
} catch (error) {
// 获取用户信息失败,则删除会话信息并跳转到登录页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 没有token*/
if (whiteList.indexOf(to.path) !== -1) {
// 是白名单的页面,则可以进入页面
next()
} else {
// 非白名单,则跳转到登录页
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
// 进入页面后
router.afterEach(() => {
// 进度条完成
NProgress.done()
})
关于自定义图标
左侧菜单使用的自定义图标使用的是svg文件定义的,这个可以去阿里的矢量图标库去获取。
图标存放目录
src/icons/svg/xxxx.svg
效果图
小结
本文只是简单的介绍了登录模块及路由模块化分层,基本的分层思想还是借鉴于后端。权限这块因为涉及内容太多,所以并没有在该篇文章中展开,后续会专门出一篇来详细说明前后端分离后的权限管理。
项目源码地址
- 后端
- 前端