文章目录
环境前提:
- 安装
node
:控制台输入node -v
,如果出现相应的版本号,则说明安装成功。 - 安装淘宝镜像:
npm install -g cnpm --registry=https://registry.npm.taobao.org
- 安装
webpack
:npm install webpack@3.8.1 -g
,webpack -v
查看版本号 - 安装
vue-cli
脚手架构建工具:npm install vue-cli -g
安装完成之后输入vue -V
(注意这里是大写的“V”),如果出现相应的版本号,则说明安装成功。
1. 初始化项目:
vue init webpack [项目名]
创建一个基于 webpack 模板的新项目,输入命令: vue init webpack my_vue ,my_vue是文件夹的名字。项目初始化及环境配置见另一篇博文 nvm安装配置/node安装配置/vue项目初始化
2. 项目目录
项目目录(在自动生成目录基础上自行修改):
projectName
├── build // 构建相关
├── config // 配置相关
├── src // 源代码
│ ├── api // 所有请求
│ ├── assets // 主题 字体等静态资源
| └──img // 项目所有图片子u按
│ ├── components // 全局公用组件
│ ├── directive // 全局指令
│ ├── filtres // 全局 filter
│ ├── lang // 国际化 language
│ ├── mock // 项目mock 模拟数据
│ ├── router // 路由
│ ├── store // 全局 store管理
│ ├── styles // 全局样式
│ ├── utils // 全局公用方法
│ ├── pages // 页面
│ ├── App.vue // 入口页面
│ ├── main.js // 入口 加载组件 初始化等
│ └── permission.js // 权限管理
├── static // 第三方不打包资源
├── .babelrc // babel-loader 配置
├── eslintrc.js // eslint 配置项
├── .gitignore // git 忽略项
├── index.html // html模板
└── package.json // package.json
在pages问价夹下新建login.vue文件并随便输入内容,修改router指向后,运行,看是否可以正确加载。
3. 基础配置:
3.1 安装element-UI
安装:npm install element-ui
引入:
// main.js
import ElementUI from 'element-ui'
Vue.use(ElementUI, { size: 'mini'});
import('element-ui/lib/theme-chalk/index.css')
3.2 Vuex
使用vuex进行数据管理
录token,用户信息、全局个人偏好设置等用vuex管理
安装vuex:npm install vuex --save
引入:
// main.js
import store from './store/'
new Vue({
el: '#app',
store,
components: { App },
template: '<App/>'
})
3.3 封装 axios
安装axios : npm install --save axios
// src/utils/axios.js
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'
// 创建axios实例
let service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(config => {
if (store.getters.token) {
config.headers = {
'Authorization' : "Token " + getToken('Token'), //携带权限参数
};
}
return config
}, error => {
Promise.reject(error)
})
// respone拦截器
service.interceptors.response.use(
response => {
/**
* code:200,接口正常返回;
*/
const res = response.data
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
// 根据服务端约定的状态码:5001:非法的token; 5002:其他客户端登录了; 5004:Token 过期了;
if (res.code === 5001 || res.code === 5002 || res.code === 5004) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('LogOut').then(() => {
location.reload()// 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else { // res.code === 200,正常返回数据
return response.data
}
},
error => {
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
3.4 封装使用 阿里icon插件
index.html中,引用
<script src="//at.alicdn.com/t/font_1258069_e40c6mwl0x8.js"></script>
创建 icon-component 组件
// components/iconSvg/IconSvg.vue
<template>
<svg class="svg-icon" aria-hidden="true">
<use :xlink:href="iconName"></use>
</svg>
</template>
<script>
export default {
name: 'icon-svg',
props: {
iconClass: {
type: String,
required: true
}
},
computed: {
iconName() {
return `#${this.iconClass}`
}
}
}
</script>
<style>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
// components/iconSvg/index.js
import Vue from 'vue'
import IconSvg from './IconSvg'
//全局注册icon-svg
Vue.component('icon-svg', IconSvg)
main.js中引入
import './components/iconSvg' // iconSvg
3.5 Cookies封装
登录验证 采取方式为cookie验证 , 在utils文件夹下新建auth.js,用来对cookie进行操作
安装:npm install --save js-cookie
auth.js
// utils/auth.js
// cookies的获取、设置、移除
import Cookies from 'js-cookie'
export function getToken(TokenKey) {
return Cookies.get(TokenKey)
}
export function setToken(TokenKey,token) {
return Cookies.set(TokenKey, token)
}
export function removeToken(TokenKey) {
return Cookies.remove(TokenKey)
}
4. 登录
思路:针对前后端完全分离的情况下,实现对Vue的Token登录验证,大致思路如下:
- 点击登录后,前端调后端的登陆接口,发送用户名和密码;
- 后端收到请求,验证用户名和密码,在验证成功返回token;
- 前端拿到token,将token存储到localStorage和vuex中,并跳转路由页面
- 前端每次跳转路由,就判断 localStroage 中有无 token ,没有就跳转到登录页面,有则跳转到对应路由页面
- 每次调后端接口,都要在请求头中加token,后端判断请求头中有无token:有token,就拿到token并验证token,验证成功就返回数据,验证失败就返回401;无token也返回401。
- 前端拿到状态码为401,就清除token信息并跳转到登录页面
4.1 mockjs模拟后台用户信息:
mockjs/user.js
import Mock from 'mockjs'
let List = []
const count = 1000
let typelist = ['联通', '移动', '电信', '铁通']
for (let i = 0; i < count; i++) {
List.push(Mock.mock({
id: Mock.Random.guid(),
sortnum: i + 1,
username: Mock.Random.cname(),
address: Mock.mock('@county(true)'),
createTime: Mock.Random.datetime(),
updateTime: Mock.Random.now(),
ip: Mock.mock('@ip'),
region: Mock.mock('@region'),
areaId: /\d{7}/,
email: Mock.Random.email(),
'isp|1': typelist
}))
}
let users = {
code:0,
data:[
{
id:1,
usertitle: '管理员',
username: 'admin',
password: '123456',
roles: ['admin'],
token: 'admin',
avatar: '../../static/img/avatar.png'
},{
id:2,
usertitle: '编辑',
username: 'editor',
password: '112233',
roles: ['editor'],
token: 'editor',
avatar:'../../static/img/avatar.png'
},
]
}
export default {
// 用户登录
login: config => {
let data = JSON.parse(config.body);
let flag = false;
let localToken='';
let localName='';
for(let value of users.data){
if((data.username === value.username)&&(data.username === value.username)){
console.log('密码正确')
flag = !flag
localToken = value.token
localName = value.username
}
}
if(flag){
flag = !flag
return {
code: 200,
data: {
userInfo: {
token: localToken,
name: localName,
},
status: 'success'
}
}
}else {
return {
code: -1,
data: {
msg: "用户名错误",
status: 'fail'
}
}
}
},
// 用户登出
logout: config => {
return {
code: 200,
data: {
userList: ""
}
}
},
// 获取登录用户信息
getUserInfo: config => {
let data = JSON.parse(config.body);
let flag = false;
let localroles='';
let localName='';
let localAvatar='';
for(let value of users.data){
if (data.token === value.token) {
console.log('密码正确')
flag = !flag
localroles = value.roles;
localName = value.username;
localAvatar = value.avatar;
}
}
if(flag){
flag = !flag
return {
code: 200,
data: {
userInfo: {
roles: localroles,
name: localName,
avatar: localAvatar,
}
}
}
}else {
console.log('data',data)
console.log('value',value)
console.log('后台验证到token过期')
return {
code: 401,
data: {
userInfo: {
}
}
}
}
},
/**
* 获取用户列表
* 要带参数 name, page, limt; name可以不填, page,limit有默认值。
* @param name, page, limit
* @return {{code: number, count: number, data: *[]}}
*/
getUserList: config => {
const {
limit,
page
} = JSON.parse(config.body);
let mockList = List;
const userList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
return {
code: 200,
data: {
total: mockList.length,
userList: userList
}
}
}
}
4.2 axios封装
调用axios获取mockjs模拟数据。
// api/user.js
import request from '@/utils/axios'
export function login(params) {
return request({
url: '/user/login',
method: 'get',
data:params
})
}
export function logout(params) {
return request({
url: '/user/logout',
method: 'get',
data:params
})
}
4.3 登录验证
提交用户账号和密码,向后端验证通过之后,返回用户唯一标识的token(mockjs/user.js中模拟设置),拿到token之后,将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态。然后根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
// login.vue
submitForm(loginForm) {
this.$refs[loginForm].validate((valid) => {
if (valid) {
let userinfo = this.loginForm;
window.console.log("this.loginForm",userinfo)
login(userinfo).then(res => {
const data = res.data
if(res.code == 200){
console.log('登录成功')
let userInfo = data.userInfo;
// token存贮到cookie
setToken("Token",userInfo.token)
this.$router.push({ path: '/' })
this.$store.dispatch('initLeftMenu'); //设置左边菜单始终为展开状态
}else if(data.code == -1){
alert(res.data.msg)
}else{
console.log('登录失败')
}
})
}
});
},
存储用户信息到vuex
// store/user.js
import { getStore } from '@/utils/mUtils'
import { getUserInfo } from '@/api/user' // 导入用户信息相关接口
import { getToken, setToken, removeToken } from '@/utils/auth'
const user = {
state : {
name:'',
avatar:'',
token: getToken('Token'),
roles: [],
browserHeaderTitle: getStore('browserHeaderTitle') || 'ColdCoder'
},
getters : {
token: state => state.token,
roles: state => state.roles,
avatar: state => state.avatar,
name: state => state.name,
browserHeaderTitle: state => state.browserHeaderTitle,
},
mutations: {
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_BROWSERHEADERTITLE: (state, action) => {
state.browserHeaderTitle = action.browserHeaderTitle
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
},
......
}
4.4 获取用户信息及权限判断
用户登录成功之后,我们会在全局钩子router.beforeEach
中拦截路由,判断是否已获得token,在获得token后,通过 token 获取用户的 role ,动态根据用户的 role 动态生成相应权限的路由表,再通过router.addRoutes
动态挂载路由,这属于前端页面级的权限控制。
// src/permission.js
router.beforeEach((to, from, next) => {
// 加载进度条
NProgress.start()
// 设置浏览器头部标题
const browserHeaderTitle = to.meta.title
store.commit('SET_BROWSERHEADERTITLE', {
browserHeaderTitle: browserHeaderTitle
})
// 点击登录时,拿到了token并存入了cookie,保证页面刷新时,始终可以拿到token
if (getToken('Token')) {
if (to.path === '/login') {
next({
path: '/'
})
NProgress.done()
} else {
// 用户登录成功之后,每次点击路由都进行了角色的判断;
if (store.getters.roles.length === 0) {
let token = getToken('Token');
getUserInfo({"token": token}).then().then(res => { // 根据token拉取用户信息
if (res.code == 401){
store.dispatch('LogOut').then(() => {
Message.error(err || 'Token was expired, please login again')
next({ path: '/' })
})
}
if (res.code == 200) {
let userList = res.data.userInfo;
console.log('(userList.avatar',userList.avatar)
store.commit("SET_ROLES", userList.roles);
store.commit("SET_NAME", userList.name);
store.commit("SET_AVATAR", userList.avatar);
store.dispatch('GenerateRoutes', {
"roles": userList.roles
}).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问权限路由表
next({
...to,
replace: true
}) // hack方法 确保addRoutes已完成
})
}
}).catch((err) => {
store.dispatch('LogOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断
next()
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
// 点击退出时,会定位到这里
next()
} else {
next('/login')
NProgress.done()
}
}
})
4.5 路由控制及动态路由生成
根据不同角色登录后台返回权限以及目录信息,动态的生成路由以及菜单目录,设计思路如下:
- 路由配置
将路由分为两个部分:权限路由、非权限路由。初始化时,将非权限路由对象赋值给Router;同时设置权限路由的中meta:{roles:[‘admin’,‘editor’]},标识对应用户的权限。 - 全局前置守卫中动态添加路由,校验页面权限。用户登录成功之后返回的roles值,进行路由的匹配并生成新的路由对象,调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由,用户成功登录并跳转到首页时,使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。
store/modules/permission.js
用户是否有roles信息,进行不同的业务逻辑:
- 初始情况下,用户roles信息为空:通过getUserInfo()函数,根据token拉取用户信息;并通过store将该用户roles,name,avatar信息存储于vuex;通过store.dispatch('GenerateRoutes', { roles })去重新过滤和生成路由,通过router.addRoutes()合并路由表;如果在获取用户信息接口时出现错误,则调取store.dispatch('LogOut')接口,返回到login页面。
- 用户已经拥有roles信息:点击页面路由,通过roles权限判断 hasPermission()。如果用户有该路由权限,直接跳转对应的页面;如果没有权限,则跳转至401提示页面;
// store/modules/permission.js
import { asyncRouterMap, constantRouterMap } from '@/router'
import * as mutils from '@/utils/mUtils'
/**
* 通过meta.role判断是否与当前用户权限匹配
* @param roles
* @param route
*/
function hasPermission(roles, route) {
// roles为权限身份数组
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.indexOf(role) >= 0)
} else {
return true
}
}
/**
* 递归过滤异步路由表,返回符合用户角色权限的路由表
* @param asyncRouterMap
* @param roles
*/
function filterAsyncRouter(asyncRouterMap, roles) {
// 返回满足条件的子路由对象
const accessedRouters = asyncRouterMap.filter(route => {
if (hasPermission(roles, route)) {
if (route.children && route.children.length) {
// route.children重新过滤赋值;
route.children = filterAsyncRouter(route.children, roles)
}
return true // 返回该权限路由对象;
}
return false
})
return accessedRouters
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: [],
topRouters:[],
topTitle:'',
menuIndex:0
},
getters:{
permission_routers: state => state.routers, // 所有路由
addRouters: state => state.addRouters, // 权限过滤路由
// topRouters: state => state.topRouters, // 顶部三级路由
topTitle:state => state.topTitle, // 顶部的title
menuIndex:state => state.menuIndex, // 顶部菜单的index
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers // 权限路由
state.routers = constantRouterMap.concat(routers) // 总路由
},
CLICK_INNER_LEFT_MENU:(state,data) => { // titleList:arr
state.topRouters = data.titleList;
},
},
actions: {
// 根据角色,重新设置权限路由;并保存到vuex中,SET_ROUTERS;
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
let roles = data.roles;
window.console.log(data)
let accessedRouters = '';
if (roles.indexOf('admin') >= 0) {
// 如果是管理员,直接将权限路由赋值给新路由;
accessedRouters = asyncRouterMap
} else {
// // 非管理员用户,如roles:['editor','developer'],则需要过滤权限路由数据
accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
}
commit('SET_ROUTERS', accessedRouters)
resolve()
})
},
ClickLeftInnerMenu({ commit },data) {
commit('CLICK_INNER_LEFT_MENU',data)
},
// ClickTopMenu({ commit },data) {
// commit('CLICK_TOP_MENU',data)
// }
}
}
export default permission
前端路由控制router.js
// router.js
import Vue from 'vue'
import Router from 'vue-router'
import { Layout} from "../layout"; // 页面整体布局
// import { topRouterMap } from "./topRouter";
process.env.NODE_ENV === "development" ? Vue.use(Router) : null;
//手动跳转的页面白名单
const whiteList = [
'/'
];
//默认不需要权限的页面
export const constantRouterMap = [{
path: '',
component: Layout,
redirect: '/index/index',
hidden: true
},
{
path: '/login',
name: 'login',
component: () => import('@/pages/login'),
hidden: true
},
{
path: '/404',
component: () => import('@/pages/errorPage/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/pages/errorPage/401'),
hidden: true
},
{
path: '/index',
name: 'index',
component: Layout,
meta: {
title: '首页',
icon: 'icondashboard',
},
noDropdown: true,
children: [{
path: 'index',
meta: {
title: '首页',
icon: 'icondashboard',
routerType: 'leftmenu'
},
component: () => import('@/pages/index/index'),
}]
}
]
//注册路由
export default new Router({
mode: 'history', // 默认为'hash'模式
base: '/', // 添加根目录,对应服务器部署子目录
routes: constantRouterMap
})
//异步路由(需要权限的页面)
export const asyncRouterMap = [
{
path: '/error',
component: Layout,
name: 'errorPage',
meta: {
title: '错误页面',
icon: 'iconError'
},
children: [{
path: '401',
name: 'page401',
component: () => import('@/pages/errorPage/401'),
meta: {
title: '401',
noCache: true
}
},
{
path: '404',
name: 'page404',
component: () => import('@/pages/errorPage/404'),
meta: {
title: '404',
noCache: true
}
}
]
},
{
path: '*',
redirect: '/404',
hidden: true
}
];
注意事项: 404
页面一定要在异步路由的最后加载,如果放在constantRouterMap
一同声明404
,页面都会被拦截到404
.