登录功能的主要步骤
1、创建login.vue静态页面
1.1) 其中,el-form,会直接跳转到主页(确定按钮对应的方法,直接跳转到 “/dashboard”路由)
el-form组件:elementUI插件里面的一个组件,经常展示表单元素 model:用于收集表单数据 rules:表单验证规则
- rules:表单验证规则具体定义在
data(){}
loginRules里表单验证,验证用户名与密码操作
-【涉及三个参数 required: true(是否必填), trigger: ‘blur’(什么事件触发), validator: validateUsername(触发函数)】 - 这里validateUsername需要额外引入utils/validate.js里已经封装好的方法(判断是否是https?:|mailto:|tal: 开头的链接)
eg: loginRules: {
username: [{ required: true, trigger: ‘blur’, validator: validateUsername }],
password: [{ required: true, trigger: ‘blur’, validator: validatePassword }]
},
//src/views/login/index.vue/<template>
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" >
<div class="title-container">
<h3 class="title">登录</h3>
</div>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon-class="user" />
</span>
<el-input
ref="username"
v-model="loginForm.username"
placeholder="Username"
name="username"
type="text"
tabindex="1"
auto-complete="on"
/>
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="Password"
name="password"
tabindex="2"
auto-complete="on"
@keyup.enter.native="handleLogin"
/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</el-form-item>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
<div class="tips">
<span style="margin-right:20px;">username: admin</span>
<span> password: any</span>
</div>
</el-form>
</div>
</template>
-utils/validate.js如下,并记得在login/index.vue/script引入
//src/utils/validate.js
/**
* @param {string} path
* @returns {Boolean}
*/
// 判断字符串是否是https?:|mailto:|tal: 开头的
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}
//src/views/login/index.js/<script>
<script>
import { validUsername } from '@/utils/validate'
data() {
//具体定义表单提交事件函数:这里面在进行表单验证,验证用户名与密码操作
const validateUsername = (rule, value, callback) => {
if (!validUsername(value)) {
callback(new Error('Please enter the correct user name'))
} else {
callback()
}
}
const validatePassword = (rule, value, callback) => {
if (value.length < 6) {
callback(new Error('The password can not be less than 6 digits'))
} else {
callback()
}
}
return {
loginForm: {
username: 'admin',
password: '111111'
},
//这里定义具体loginRules
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
},
loading: false,
passwordType: 'password',
redirect: undefined
}
},
</script>
1.2) 上面的提交按钮绑定事件 @click.native.prevent=“handleLogin”,事件操作如下,
假如账号密码验证格式全通过
,那么发请求
派发action,先触发封装的axios请求拦截器,在utils/request.js(首次登录不会拿到token),并在之后的每一次请求里把header里带上token
,后请求后端接口user/login,(在actions里本地永久存储了token在了浏览器cookie里,详见store/user.js的actions:setToken(result.data.token);)
methods:{
handleLogin() {
//这里是在验证表单元素(用户名与密码)的是否符合规则
this.$refs.loginForm.validate(valid => {
//如果符合验证规则
if (valid) {
//按钮会有一个loading效果
this.loading = true;
//派发一个action:user/login,带着用户名与密码的载荷
this.$store.dispatch('user/login', this.loginForm).then(() => {
//登录成功进行路由的跳转
this.$router.push({ path: this.redirect || '/' });
//loading效果结束
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
1.2.1)这里是发异步请求之前,先走的请求拦截器
↓
,就做一件事:给除了第一次登陆之后的每一次请求头都把token放在header里
//src/utils/request.js
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
//请求拦截器:把携带的token字段放到浏览器cookie里
service.interceptors.request.use(
config => {
//判断是否存在token,如果存在将携带的token字段放到浏览器cookie里
if (store.getters.token) {
config.headers['token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
//响应拦截器
response => {
const res = response.data
//服务器响应失败在干什么,因为咱们真实服务器返回code 20000也有可能200
if (res.code !== 20000 && res.code!=200) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: 非法token; 50012: 其他client初测; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
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)
}
)
这是把token放浏览器cookie的代码
↓
,//src/utils/auth.js
//src/utils/auth.js
//将携带的token字段放到浏览器cookie里
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
1.3)定义vuex,做两件事
a)用来异步拿到user/login接口返回的数据(code),存到state.token里以及浏览器缓存cookie里
b)计算出当前用户需要展示所有路由(静态路由+异步路由+任意路由)
//引入登录|退出登录|获取用户信息的接口函数
import { login, logout, getInfo } from '@/api/user'
// 获取token|设置token|删除token的函数
import { getToken, setToken, removeToken } from '@/utils/auth'
//路由模块当中重置路由的方法
import { anyRoutes, resetRouter,asyncRoutes,constantRoutes} from '@/router';
import router from '@/router';
import cloneDeep from 'lodash/cloneDeep'
//箭头函数
const getDefaultState = () => {
return {
//获取token
token: getToken(),
//存储用户名
name: '',
//存储用户头像
avatar: '',
//服务器返回的菜单信息【根据不同的角色:返回的标记信息,数组里面的元素是字符串】
routes:[],
//角色信息
roles:[],
//按钮权限的信息
buttons:[],
//对比之后【项目中已有的异步路由,与服务器返回的标记信息进行对比最终需要展示的理由】
resultAsyncRoutes:[],
//用户最终需要展示全部路由
resultAllRputes:[]
}
}
const state = getDefaultState()
//唯一修改state的地方
const mutations = {
//重置state
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
//存储token
SET_TOKEN: (state, token) => {
state.token = token
},
//存储用户信息
SET_USERINFO:(state,userInfo)=>{
//用户名
state.name = userInfo.name;
//用户头像
state.avatar = userInfo.avatar;
//菜单权限标记
state.routes = userInfo.routes;
//按钮权限标记
state.buttons = userInfo.buttons;
//角色
state.roles = userInfo.roles;
},
//最终计算出的异步路由
SET_RESULTASYNCROUTES:(state,asyncRoutes)=>{
//vuex保存当前用户的异步路由,注意,一个用户需要展示完成路由:常量、异步、任意路由
state.resultAsyncRoutes = asyncRoutes;
//计算出当前用户需要展示所有路由(静态路由+异步路由+任意路由)
state.resultAllRputes = constantRoutes.concat(state.resultAsyncRoutes,anyRoutes);
//给路由器添加新的路由
router.addRoutes(state.resultAllRputes)
}
}
//定义一个函数:两个数组进行对比,对比出当前用户到底显示哪些异步路由
const computedAsyncRoutes = (asyncRoutes,routes)=>{
//过滤出当前用户【超级管理|普通员工】需要展示的异步路由
return asyncRoutes.filter(item=>{
//数组当中没有这个元素返回索引值-1,如果有这个元素返回的索引值一定不是-1
if(routes.indexOf(item.name)!=-1){
//递归:别忘记还有2、3、4、5、6级路由
if(item.children&&item.children.length){
item.children = computedAsyncRoutes(item.children,routes);
}
return true;
}
})
}
//actions
const actions = {
//这里在处理登录业务
async login({ commit }, userInfo) {
//解构出用户名与密码
const { username, password } = userInfo;
//等待异步login函数返回请求后端返回后的结果,并判断响应code状态,这里的await后面login详见下段代码
let result = await login({ username: username.trim(), password: password });
//注意:当前登录请求现在使用mock数据,mock数据code是20000
if(result.code==20000){
//vuex存储token
commit('SET_TOKEN',result.data.token);
//这里本地持久化存储token
setToken(result.data.token);
return 'ok';
}else{
return Promise.reject(new Error('faile'));
}
},
//获取用户信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
//获取用户信息:返回数据包含:用户名name、用户头像avatar、routes[返回的标志:不同的用户应该展示哪些菜单的标记]、roles(用户角色信息)、buttons【按钮的信息:按钮权限用的标记】
const { data } = response;
//vuex存储用户全部的信息
commit('SET_USERINFO',data);
commit('SET_RESULTASYNCROUTES',computedAsyncRoutes(cloneDeep(asyncRoutes),data.routes));
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// user logout
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)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
2、目前为止,把token放到了cookie中,也放到了state里
接下里做两件事a、路由表的配置
,
路由分为三种,常态路由,异步路由。和任意路由
即(谁都能看得页面),(权限页面),和(兜底报错友好页面)
b)配置动态路由(全局路由守卫)
//router/index.js---路由表配置
//引入Vue|Vue-router
import Vue from 'vue'
import Router from 'vue-router'
//使用路由插件
Vue.use(Router)
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',
children: [{
path: 'dashboard',
name: "Dashboard",
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
},
]
//异步理由:不同的用户(角色),需要过滤筛选出的路由,称之为异步路由
//有的用户可以看见测试管理、有的看不见
export const asyncRoutes = [
{
name: 'Acl',
path: '/acl',
component: Layout,
redirect: '/acl/user/list',
meta: {
title: '权限管理',
icon: 'el-icon-lock'
},
children: [
{
name: 'User',
path: 'user/list',
component: () => import('@/views/acl/user/list'),
meta: {
title: '用户管理',
},
},
{
name: 'Role',
path: 'role/list',
component: () => import('@/views/acl/role/list'),
meta: {
title: '角色管理',
},
},
{
name: 'RoleAuth',
path: 'role/auth/:id',
component: () => import('@/views/acl/role/roleAuth'),
meta: {
activeMenu: '/acl/role/list',
title: '角色授权',
},
hidden: true,
},
{
name: 'Permission',
path: 'permission/list',
component: () => import('@/views/acl/permission/list'),
meta: {
title: '菜单管理',
},
},
]
},
{
path: '/product',
component: Layout,
name: 'Product',
meta: { title: '商品管理', icon: 'el-icon-goods' },
children: [
{
path: 'trademark',
name: 'TradeMark',
component: () => import('@/views/product/tradeMark'),
meta: { title: '品牌管理' }
},
{
path: 'attr',
name: 'Attr',
component: () => import('@/views/product/Attr'),
meta: { title: '平台属性管理' }
},
{
path: 'spu',
name: 'Spu',
component: () => import('@/views/product/Spu'),
meta: { title: 'Spu管理' }
},
{
path: 'sku',
name: 'Sku',
component: () => import('@/views/product/Sku'),
meta: { title: 'Sku管理' }
},
]
},
{
path: '/test',
component: Layout,
name: 'Test',
meta: { title: '测试管理', icon: 'el-icon-goods' },
children: [
{
path: 'test1',
name: 'Test1',
component: () => import('@/views/Test/Test1'),
meta: { title: '测试管理1' }
},
{
path: 'test2',
name: 'Test2',
component: () => import('@/views/Test/Test2'),
meta: { title: '测试管理2' }
},
]
},
];
//任意路由:当路径出现错误的时候重定向404
export const anyRoutes ={ path: '*', redirect: '/404', hidden: true };
3、这是全局路由守卫
↓
,主要做一件事------判断是否有token→有token再看下一步跳转哪里?
a)去login页面,就直接跳根路径,
b)不去login,看state里有没有用户信息,没有就派发getInfo接口,重新获取用户信息
(返回数据包含:用户名name、用户头像avatar、routes[返回的标志:不同的用户应该展示哪些菜单的标记]、roles(用户角色信息)、buttons【按钮的信息:按钮权限用的标记】),再跳转
//src/permission.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // 第三档插件进度条
import 'nprogress/nprogress.css' // 第三档插件进度条样式
import { getToken } from '@/utils/auth' // 从浏览器cookie里拿token
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress配置
//白名单
const whiteList = ['/login']
//全局前置路由守卫
router.beforeEach(async(to, from, next) => {
// 进度条开始
NProgress.start()
// 动态展示标题
document.title = getPageTitle(to.meta.title)
// 前置路由守卫判断是否有token,并跳转
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 代表已经有token,重定向到根路径,又因为路由表里跟路由被重定向到dashboard,因此跳到dashboard
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 {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})