目录
上篇文章:【VUE】demo01-VUE做后台管理系统页面实例-创建基本环境+页面布局
工具:Visual Studio Code + Vue + Vue cil2 + Vuex + Vue Router + ElementUI + axios
项目代码地址:macrozheng_mall学习: 学习macrozheng的mall项目,进行学习记录与代码拆解 - Gitee.com
1.5加入axios
我们先尝试加入接口调用的工具,通过登录接口来测试 axios 是否成功。(mall 项目的登录接口,是通过调用 vuex 操作的,但我们还没有加入vuex ,因为暂时还用不到,vuex 比较复杂。其实是一样哒,后续会加入的!)
注意:mall 的后端项目是使用的 token 来验证用户信息。也就是调用接口时,如果不是公开的接口需要提供 token 值的。
由于 axios 是一个工具,一般来说就像我们使用 Ajax 一样,调用一个方法就行,axios 也给我们提供了封装接口得到方法,一般就需要一个创建实例的文件,然后就是封装接口的js文件。
首先创建在src下一个创建 axios 实例文件 util/request.js
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 15000 // 请求超时时间
})
// request请求拦截器,每次请求接口前都会执行这个
service.interceptors.request.use(config => {
//可以在这里设置请求头等内容
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})
// respone相应拦截器
service.interceptors.response.use(
response => {
//这里是获取返回值,例如mall系统的后端返回内容是 code,data,message
//所以我们拿到code,判断是否等于200,不是就走接口错误的处理方式
const res = response.data
if (res.code !== 200) { //处理错误的方式
return Promise.reject('error')
} else {//处理正确的方式
return response.data
}
},
error => {
console.log('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 3 * 1000
})
return Promise.reject(error)
}
)
//输出对象
export default service
这里需要注意 axios 实例中的 baseURL ,我们可以在这里写死,也可以在项目的配置文件中设置,然后获取。
baseURL: process.env.BASE_API
“process.env.BASE_API”获取的就是 config/dev.env.js 文件中的 key= BASE_API 的 value 数据.
所以我们打开 config/dev.env.js 文件,添加一个 k:v 。
// --------------------config/dev.env.js---------------------
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
//记住这里必须加上 // 或者加上 http://,否则会被认为是没有IP端口协议的,会自动带上当前前端路径,
//就像这样 :http://localhost:8080/139.196.220.24:80/***/***
BASE_API: '"http://admin-api.macrozheng.com"' //就是这里
})
注意,我们这里 BASE_API 是 mall 系统提供的测试域名和接口,这样我们就不用在本地部署一遍后端程序了(后端项目学习也会有笔记记录),感谢 macrozheng !
axios 实例准备完成!!接下来我们需要进行一下测试,保证 axios 添加成功。因为很多接口都有访问权限,也为了直观,我们从登录接口添加 axios 调用接口的封装 js 文件。
首先,我们在 src 文件夹中创建一个文件 api/login.js ,之后的接口调用文件都在 src/api 文件夹中。
//------api/login.js--------------------------------
//引入我们写的 axios
import request from '@/utils/request'
export function login(username, password) {
return request({
url: '/admin/login',
method: 'post',
//data是添加到请求体(body)中的, 用于post请求。
data: {
username,
password
}
})
}
export function getInfo() {
return request({
url: '/admin/info',
method: 'get',
})
}
export function logout() {
return request({
url: '/admin/logout',
method: 'post'
})
}
export function fetchList(params) {
return request({
url: '/admin/list',
method: 'get',
//params是添加到url的请求字符串中的,用于get请求
params: params
})
}
之后我们调用 login 相关的接口时,直接引入这个 login.js 中暴露出去的方法就可以啦!
配置都完成啦,接下来进行测试,首先在 view 文件夹添加一个登陆页面 login/index.vue
//------login/index.vue 有删减----------------
<template>
<div>
<el-card class="login-form-layout">
<el-form autoComplete="on"
:model="loginForm"
:rules="loginRules"
ref="loginForm"
label-position="left">
<div style="text-align: center">
<svg-icon icon-class="login-mall" style="width: 56px;height: 56px;color: #409EFF"></svg-icon>
</div>
<h2 class="login-title color-main">mall-admin-web</h2>
<el-form-item prop="username">
<el-input name="username"
type="text"
v-model="loginForm.username"
autoComplete="on"
placeholder="请输入用户名">
<span slot="prefix">
<svg-icon icon-class="user" class="color-main"></svg-icon>
</span>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input name="password"
:type="pwdType"
@keyup.enter.native="handleLogin"
v-model="loginForm.password"
autoComplete="on"
placeholder="请输入密码">
<span slot="prefix">
<svg-icon icon-class="password" class="color-main"></svg-icon>
</span>
<span slot="suffix" @click="showPwd">
<svg-icon icon-class="eye" class="color-main"></svg-icon>
</span>
</el-input>
</el-form-item>
<el-form-item style="margin-bottom: 60px;text-align: center">
<el-button style="width: 45%" type="primary" :loading="loading" @click.native.prevent="handleLogin">
登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { isvalidUsername } from "@/utils/validate";
import { login, logout, getInfo } from "@/api/login";
import login_center_bg from "@/assets/images/login_center_bg.png";
export default {
name: "login",
data() {
const validateUsername = (rule, value, callback) => {
if (!isvalidUsername(value)) {
callback(new Error("请输入正确的用户名"));
} else {
callback();
}
};
const validatePass = (rule, value, callback) => {
if (value.length < 3) {
callback(new Error("密码不能小于3位"));
} else {
callback();
}
};
return {
loginForm: {
username: "",
password: "",
},
loginRules: {
username: [
{ required: true, trigger: "blur", validator: validateUsername },
],
password: [
{ required: true, trigger: "blur", validator: validatePass },
],
},
loading: false,
pwdType: "password",
login_center_bg,
dialogVisible: false,
supportDialogVisible: false,
};
},
created() {
console.log("beforeEach:"+this.$router.options.routes);
},
methods: {
showPwd() {
if (this.pwdType === "password") {
this.pwdType = "";
} else {
this.pwdType = "password";
}
},
//登陆核心
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
const username = this.loginForm.username.trim()
login(username, this.loginForm.password)
.then((response) => {
this.loading = false
const data = response.data;
const tokenStr = data.tokenHead + data.token;
console.log("tokenStr="+tokenStr)
})
.catch((error) => {
this.loading = false
//抛出错误
});
} else {
console.log("参数验证不合法!");
return false;
}
});
},
},
};
</script>
<style scoped>
.login-form-layout {
position: absolute;
left: 0;
right: 0;
width: 360px;
margin: 140px auto;
border-top: 10px solid #409eff;
}
.login-title {
text-align: center;
}
.login-center-layout {
background: #409eff;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
margin-top: 200px;
}
</style>
我们可以直接复制mall项目里面的,注意,有些东西我们之前没有添加,如果觉得不需要也可以去掉,反正以后也要用,添加部分如下:
1.SvgIcon组件和icons文件;
复制src/components/SvgIcon所有文件到我们项目中同样位置,之后复制 src/icons 所有文件到我们的项目中同样的位置。
2.util包中的validate文件;
复制src/views/validate.js 文件到我们项目中同样的位置。
3.images文件
复制src/assets/images 文件到我们项目中同样的位置。
如果不添加,记得将index文件中对应的标签或者引入组件删除,不然会编译报错的。
添加成功后我们还不可以访问,因为我们还没有添加路由呢!
打开 router/index.js 文件,添加 login 页面路由:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '../views/layout/Layout'
export const constantRouterMap = [
//这里添加的路由
{path: '/login', component: () => import('@/views/login/index'), hidden: true},
{
path: '',
component: Layout,
redirect: '/home',
children: [{
path: 'home',
name: 'home',
component: () => import('@/components/HelloWorld'),
meta: {title: '首页', icon: 'home'}
}]
}
]
export default new Router({
// mode: 'history', //后端支持可开
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
运行项目后,打开 http://localhost:8080/#/login 页面试一下,mall 后端接口测试提供的账号密码是: admin macro123(希望可以看看这位作者的公众号哈),
访问成功!说明我们的 axios 引入成功!
1.6加入js-cookie
当我们能够写基本的页面,能够调用接口时,基本的使用就可以,我们可以直接通过现有的工具编写系统页面。
注意,我们之前有说后端项目是使用的 token 来验证用户信息,管理系统除了登录页面,其余的页面都是需要权限的。所以思考一下,如果仅用现有的工具是非常麻烦的,我们登录之后需要保存 token 值,每次调用接口时都需要在请求头中加入 token 值,那么在好多路由中使用时就需要不停的传递 token 参数,非常麻烦!!!!
所以我们就需要有一个总的工具帮我们保存这个 token 值,只要我们需要就从他中获取,就不需要路由跳转传递啦!
因为保存的数据很小,所以直接用cookie保存,js-cookie就是关于cookie存储的一个js的API。可以看看这篇文章vue 项目中使用 js-cookie细则。
我们登陆成功之后,就需要调用 cookie 保存,(mall 项目是通过 vuex 调用 cookie 保存的,其实是一样的,只不过我们暂时没有加入 vuex 模块),我们直接在调用登录接口成功后,就调用 cookie 进行保存。
之后就需要在每次调用接口时,都加上token,我们也不可能每个接口都逐个添加,所以 axios 就提供了拦截器,我们在 1.5 中 util/request.js 文件中就添加了请求拦截器了,每次调用接口时都会拦截请求,然后我们在拦截中加上 token 值,这样就保证每个请求都携带 token ,当然没有 token 就不加了。
首先我们需要配置 js-cookie 工具,mall 项目有提供这个工具文件,就在 util 文件夹中的 auth.js,这个就是专门保存登录 token 的,注意不是专门操作 cookie 的,而是专门操作保存登录的cookie 的。我们拷贝过来,可以修改这个 TokenKey 为自己想要的。
//----auth.js-------------
import Cookies from 'js-cookie'
const TokenKey = 'loginToken'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
然后再 login/index.vue 文件中引入 auth.js 文件,并在调用接口成功后使用 setToken,添加到cookie 中。
//
//...
<script>
import { isvalidUsername } from "@/utils/validate";
import { login, logout, getInfo } from "@/api/login";
import login_center_bg from "@/assets/images/login_center_bg.png";
import { setToken } from '@/utils/auth' //引入cookie的setToken 方法
export default {
name: "login",
data() {
...
};
},
...
methods: {
...
//登陆核心
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
const username = this.loginForm.username.trim()
login(username, this.loginForm.password)
.then((response) => {
this.loading = false
const data = response.data;
const tokenStr = data.tokenHead + data.token;
console.log("tokenStr="+tokenStr)
setToken(tokenStr) //添加到 cookie
this.$router.push({path: '/home'}) //成功后跳转到首页
})
.catch((error) => {
this.loading = false
//抛出错误
});
} else {
console.log("参数验证不合法!");
return false;
}
});
},
},
};
...
最后要在 axios 请求拦截器中 getToken 添加 token 值到请求头中
//----util/request.js--------
...
import { getToken } from '@/utils/auth' //引入 gettoken 方法
// 创建axios实例
const service = axios.create({
...
})
// request请求拦截器,每次请求接口前都会执行这个
service.interceptors.request.use(config => {
//可以在这里设置请求头等内容
if (getToken()) { //如果cookie有 token 就加到请求头中,这里注意需要与后端代码结合
config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
...
})
...
添加成功后我们打开登陆页面试一下,填写正确的用户信息,会登陆成功到 /home页面,因为我们现在只有 login 这一个请求后端的接口,所以我们返回登陆页面,再次输入一个错误的用户信息调用一下接口,发现这个请求接口的请求头中是有我们添加的 token 值的!
只有 axios 调用的接口会被拦截,添加 token 值,其余的非axios调用接口,是没有添加这个的!
成功!!!
1.7修改vue-router为动态路由并修改sidebar为动态的导航栏
终于到这里了,理一下思路,我们现在也登陆成功了,也保存了token了,那么按照这个方式也可以继续编写代码了。
我们接下来需要添加权限模块的代码了,每一个登陆账号都有自己的权限,一方面是后端控制,一方面是前端控制,如果前端不控制那用户体验感是非常不好的。
那么当用户登录后,就需要获取用户的权限,这里的权限指:导航栏、按钮是否显示。我们先实现左侧导航栏的显示。
router路由给我们提供了一个动态路由的工具,可以帮助我们在运行时添加路由页面地址。那就是路由守卫,也就是路由过滤器,如果我们已经登陆账号并且此时访问非白名单路径(例如添加用户),那么就获取用户的权限,然后生成对应的动态导航栏。
思考一下,感觉路由守卫的router.beforeEach逻辑步骤应该是这样的:
1.判断是否登录,如果未登录,跳转到步骤5;
2.如果已登录,判断是否已获取添加动态路由,如果有获取动态路由,跳转到步骤4;
3.如果已登录,当没有获取动态路由,则调用接口获取到动态路由,并加入我们的路由里面,然后进行放行,结束。
4.直接放行,结束。
5.直接定位到登陆页面,不允许未登录时访问业务页面,结束。
上面的步骤是没问题的,当然也得根据实际的场景进行修改,具体就看业务啦~~
我们将上面的步骤转化为代码:
首先需要新建一个路由配置文件,在 src 文件夹中直接创建 permission.js :
//路由守卫,也相当于路由前的拦截器
router.beforeEach((to, from, next) => {
NProgress.start() //进度条开始
// console.log("未登录访问 /");
if (getToken()) { //若登录则获取权限等
if (to.path === '/login') { //已登陆,若访问登陆页面直接到首页
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else { //否则获取用户权限信息,并设置动态路由
//这里需要加个判断哈,如果添加了当前用户的路由就不需要再次添加了,否则警告重复。是通过 cookies 值判断的,true就是一天假,false是未添加
if(!getHaveAuth()){
getInfo().then(response => { //不用传值,已登陆的话后台会根据 token 拿到用户信息的。
const data = response.data
let menus = data.menus;
//获取当前用户的权限整理出来,若有则显示,若没有则隐藏,也就是最终 accessedRouters = asyncRouterMap 的修改版的,asyncRouterMap 就是前端写的所有动态路由数组。
const accessedRouters = asyncRouterMap.filter(v => {
... //太长了,这里是获取动态路由
});
//对菜单进行排序
sortRouters(accessedRouters);
router.addRoutes(accessedRouters); //通过方法将动态路由添加到可访问路由表
setHaveAuth(true); //已添加,所以设置值为true
next({ ...to, replace: true }) //中断当前路由,并再次进入路由守卫,那么此时的路由请求还是当前请求,比如 /
// next() // 一般动态添加路由后不能直接用这个,可能会找不到新添加的路由,这里是为了测试演示使用的(因为我们没有是否获取权限的判断,所以只能从这里放行)
}).catch(error => {
reject(error)
})
}
next()
}
} else { //若没有登陆
if (whiteList.indexOf(to.path) !== -1) { //判断是否进入白名单页面,是则放行,并跳出守卫进入页面
next() //放行
NProgress.done() //进度条结束
} else { //不是则进入登录页面
next('/login') //中断当前路由,并再次进入路由守卫,那么此时的路由请求就是 /login
}
}
})
//----------auth.js 文件中添加如下--------记得在 permission.js 中引入
const isHaveAuth = 'isHaveAuth'
export function getHaveAuth() {
return Cookies.get(isHaveAuth)
}
export function setHaveAuth(auth) {
return Cookies.set(isHaveAuth, auth)
}
写好后,记得将这个文件引入项目中,在 main.js 文件中引入:
import '@/permission' // permission control router路由的拦截器配置
第二步:添加路由页面,这里我们直接在login文件夹同级创建一些页面,就像这样:
第三步,我们修改路由,前端这里也需要加上动态路由的配置,这里要记得我们是使用的mall系统的后台,所以权限的名称匹配,要跟 mall 系统对接,不能随便写的!我们使用的 admin 账号是全部权限的账号,你也可以自己跑个后台然后进行对应的修改!打开 router/index.js :
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '../views/layout/Layout'
export const constantRouterMap = [
...
]
//------------------------------------------------------
//这个就是动态的全部权限!里面的数据,根据你的业务酌情修改
export const asyncRouterMap = [
{
path:'/ums',
component: Layout,
redirect: '/ums/admin',
//最终在 permission.js 的 getMenu() 方法中通过 name 进行匹配,也就是后端返回的权限中也有一个 name,这两个 name 匹配中了,就代表用户有这个权限
name: 'ums',
//这里的可以修改,反正已后端返回的为准
meta: {title: '权限', icon: 'ums'},
children: [
{
path: 'admin',
//同理
name: 'admin',
component: () => import('@/views/ums/admin/index'),
//同理
meta: {title: '用户列表', icon: 'ums-admin'}
},
{
path: 'role',
name: 'role',
component: () => import('@/views/ums/role/index'),
meta: {title: '角色列表', icon: 'ums-role'}
},
]
}
]
export default new Router({
...
})
最后我们需要将 router 传给 layout 组件中的左侧导航栏组件,只有传给 sidebar ,sidebar 才能够使用,之前我们用的是纯静态的。
打开 src\views\layout\components\Sidebar\index.vue
//-----sidebar/index.vue------
<template>
<el-menu
mode="vertical"
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
>
<sidebar-item :routes="routes"></sidebar-item> //添加父向子传递的数据
</el-menu>
</template>
<script>
import SidebarItem from "./SidebarItem";
export default {
components: { SidebarItem },
data() {
return {
routes: this.$router.options.routes, //在这里加入数据
};
},
};
</script>
//-----sidebar/sidebaeItme.vue------
<template>
<div class="menu-wrapper">
<!-- 当前这个父权限没有隐藏并且有孩子才会显示 -->
<template v-for="item in routes" v-if="!item.hidden && item.children">
<!-- 这个是没有子路由的 -->
<router-link v-if="hasOneShowingChildren(item.children) && !item.children[0].children&&!item.alwaysShow" :to="item.path+'/'+item.children[0].path"
:key="item.children[0].name">
<el-menu-item :index="item.path+'/'+item.children[0].path" :class="{'submenu-title-noDropdown':!isNest}">
<!-- <svg-icon v-if="item.children[0].meta&&item.children[0].meta.icon" :icon-class="item.children[0].meta.icon"></svg-icon> -->
<span v-if="item.children[0].meta&&item.children[0].meta.title" slot="title">{{item.children[0].meta.title}}</span>
</el-menu-item>
</router-link>
<!-- 这个是有子路由的 -->
<el-submenu v-else :index="item.name||item.path" :key="item.name">
<template slot="title">
<!-- <svg-icon v-if="item.meta&&item.meta.icon" :icon-class="item.meta.icon"></svg-icon> -->
<span v-if="item.meta&&item.meta.title" slot="title">{{item.meta.title}}</span>
</template>
<template v-for="child in item.children" v-if="!child.hidden">
<sidebar-item :is-nest="true" class="nest-menu" v-if="child.children&&child.children.length>0" :routes="[child]" :key="child.path"></sidebar-item>
<router-link v-else :to="item.path+'/'+child.path" :key="child.name">
<el-menu-item :index="item.path+'/'+child.path">
<!-- <svg-icon v-if="child.meta&&child.meta.icon" :icon-class="child.meta.icon"></svg-icon> -->
<span v-if="child.meta&&child.meta.title" slot="title">{{child.meta.title}}</span>
</el-menu-item>
</router-link>
</template>
</el-submenu>
</template>
</div>
</template>
<script>
export default {
name: "SidebarItem",
//获取父组件传的数据
props: {
routes: {
type: Array,
},
isNest: {
type: Boolean,
default: false
}
},
methods:{
hasOneShowingChildren(children) {
const showingChildren = children.filter(item => {
return !item.hidden
})
if (showingChildren.length === 1) {
return true
}
return false
}
}
};
</script>
这样,我们就可以获取并拿到动态路由啦,访问 http://localhost:8080/#/login 链接,登陆成功后,发现左侧并没有变化??????添加断点,发现确实在路由前置守卫中添加动态路由了,但是,并没有显示在左侧导航栏。
说明权限是有获取到的,是前端有问题!!
首先我想到了:我们 sidebar 组件中的 data 数据是跟着项目运行而初始化的,也就是说,data 已经赋值为静态路由了,并且我们也没有在这个页面中主动修改过 data 数据,就导致,这个组件的 routes 并没有改变!!!!!(这个描述不确定是不是对的,确实不清楚这样能不能拿到修改后的 routes 。但是不成功的原因不是这个!)(待学习)
but,在之后的测试中我又发现了一件事:
我们登陆之后,左侧导航并没有改变,sidebar 组件中的 data 数据也没有改变,但是确实会跳转到,我们在网址上输入 http://localhost:8080/#/pms/product 就能够打开我们添加的路由页面!!!
也就是说路由是添加进去了,但是并没有传给sidebar 组件!于是我网上搜索找到了这样一个描述:this.$router.options.routes 可以拿到初始化时配置的路由规则。所以,我们动态路由 add 后他是拿不到的,<( ̄ ﹌  ̄)@m 生气!不过还好找到原因了。
那么我们单纯的使用路由 router 来解决动态路由是不行的,所以我们需要一个缓存器,帮助我们保存动态的路由,然后在 sidebarItem 组件中拿到渲染到 router-view 中,就可以啦!mall 里面就是用 vuex 这样写的,奈何我一开始没这样试,不过也了解了这个问题,以后就不会抓头发啦!
说到缓存器,mall里面用的 vuex ,我也按照这个使用,因为我们需要进行父子组件之间传递。不清楚缓存的可以看看这个,只是说个大概:vuex和缓存的区别
下一篇就开始加入 vuex 啦!!