前言
GPS系列——Vue前端,github项目地址
前面已经学习了Android、Java端的代码实现,现在开始介绍网站前端vue的管理框架。
文中也会有大量代码,对于admin管理框架,我是模仿iview-amin,然后新建一个项目,手敲下来的,只取了自己所需的模块,目的就是为了练手,期间也遇到了很多问题,建议大家也可以自己模仿者手敲一遍。也可以使用elementUI,这个框架整体而言比iview更好一些。
GPS定位系统系列
GPS定位系统(一)——介绍
GPS定位系统(二)——Android端
GPS定位系统(三)——Java后端
GPS定位系统(四)——Vue前端
GPS定位系统(五)——Docker
- Docker nginx 二级域名无端口访问多个web项目
- Docker nginx https二级域名无端口访问多个web项目
- 持续部署——Travis+Docker+阿里云容器镜像
收获
学习完这篇文章你将收获:
- Vue + Vue-cli + iview + axios + vue-router + vuex 的实践
- 高德地图 js api的使用
- axios restful接口的异常处理封装
- 上传头像
- modal弹框编辑个人信息template
- admin管理框架
[TOC]
正题
一、admin框架介绍
框架搭建了整体架构,单页面的web应用。通用缩放式菜单栏,选项卡式管理网页、面包屑导航、bug日志管理、全屏等功能。
框架使用了通用热门的 VUE一套框架,包括Vue + Vue-cli + vuex + vue-router+iview,具体还请参见源码。
单页面大致结构
对于缩放菜单的功能较为复杂,可以细品一下,能收获许多。
二、axios封装
我们的java后台的接口统一数据为restful结构的
{code:xxx,msg:xxx,data:xxx}
对于axios而言封装上面要注意其返回的respon的结构,以及异常response的结构的处理。
这里先放整体axios封装代码:
axios.js
import axios from 'axios'
import store from '@/store'
import {Message} from 'iview'
// import { Spin } from 'iview'
const addErrorLog = errorInfo => {
const {statusText, status, request: {responseURL}} = errorInfo
let info = {
type: 'ajax',
code: status,
mes: statusText,
url: responseURL
}
console.log("addErr:" + JSON.stringify(info))
if (!responseURL.includes('save_error_logger')) store.dispatch('addErrorLog', info)
}
class HttpRequest {
constructor(baseUrl = baseURL) {
this.baseUrl = baseUrl
this.queue = {}
}
getInsideConfig() {
const config = {
baseURL: this.baseUrl,
// headers: {
// 'Content-Type': "application/json;charset=utf-8"
// }
}
return config
}
destroy(url) {
delete this.queue[url]
if (!Object.keys(this.queue).length) {
// Spin.hide()
}
}
interceptors(instance, url) {
// 请求拦截
instance.interceptors.request.use(config => {
// 添加全局的loading...
if (!Object.keys(this.queue).length) {
// Spin.show() // 不建议开启,因为界面不友好
}
this.queue[url] = true
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截
instance.interceptors.response.use(res => {
console.log("res:" + JSON.stringify(res))
this.destroy(url)
const {data: {code, data, msg}, config} = res
if (code == 200) {
return data;
} else {
this.dealErr(code, msg)
let errorInfo = {
statusText: msg,
status: code,
request: {responseURL: config.url}
}
addErrorLog(errorInfo)
return Promise.reject(res.data)
}
}, error => {
console.log("error:" + JSON.stringify(error))
this.destroy(url)
let errorInfo = error.response
if (!typeof(errorInfo) === undefined && !errorInfo) {
const {request: {statusText, status}, config} = JSON.parse(JSON.stringify(error))
errorInfo = {
statusText,
status,
request: {responseURL: config.url}
}
addErrorLog(errorInfo)
const data = {code: status, msg: statusText}
this.dealErr(data.code, data.msg)
} else {
Message.error('网络出现问题,请稍后再试')
}
return Promise.reject(error)
})
}
dealErr(c, msg) {
console.log("code:" + c)
console.log("msg:" + msg)
switch (c) {
case 400:
Message.error(msg)
break;
case 401:
Message.error('登录过期,请重新登录')
break;
// 404请求不存在
case 404:
Message.error('网络请求不存在')
break;
// 其他错误,直接抛出错误提示
default:
Message.error("系统错误")
}
}
request(options) {
const instance = axios.create()
options = Object.assign(this.getInsideConfig(), options)
this.interceptors(instance, options.url)
return instance(options)
}
}
export default HttpRequest
api.request.js:
import HttpRequest from '@/libs/axios'
import config from '@/config'
const baseUrl = config.baseUrl
const axios = new HttpRequest(baseUrl)
export default axios
调用:
import axios from '@/libs/api.request'
import Qs from 'qs'
export const login = ({username, password}) => {
const data = {
username,
password
}
return axios.request({
url: 'login',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: Qs.stringify(data),
method: 'post'
})
}
注意:如果要使用form表单的形式,需要做转化,这里可以简单方便的使用Qs
库来直接stringify
,也别忘了设置headers的'Content-Type': 'application/x-www-form-urlencoded'
,因为axios默认的是json格式。
1、interceptors的response编写
res => {
console.log("res:" + JSON.stringify(res))
this.destroy(url)
const {data: {code, data, msg}, config} = res
if (code == 200) {
return data;
} else {
this.dealErr(code, msg)
let errorInfo = {
statusText: msg,
status: code,
request: {responseURL: config.url}
}
//添加到日志
addErrorLog(errorInfo)
return Promise.reject(res.data)
}
}
{
"data":{
"code":200,
"data":{
xxxx
},
"msg":"请求成功"
},
"status":200,
"statusText":"",
"headers":{
"content-length":"487",
"content-type":"application/json;charset=UTF-8"
},
"config":{
"url":"http://127.0.0.1:9090/get_info",
"method":"post",
"headers":{
"Accept":"application/json, text/plain, */*",
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbk5hbWUiOiJrayIsImV4cCI6MTU5Njk1Nzg1MSwidXNlcklkIjoiMTMifQ.ChaBg4n5KKsF7ISj8uzHV0eh_JKadoVIBtNG4oUtp8U"
},
"baseURL":"http://127.0.0.1:9090/",
"transformRequest":[
null
],
"transformResponse":[
null
],
"timeout":0,
"xsrfCookieName":"XSRF-TOKEN",
"xsrfHeaderName":"X-XSRF-TOKEN",
"maxContentLength":-1
},
"request":{
}
}
注意,axios接口请求的response的数据结构如上。
可以看到获取数据需要res.data.data
,我们这里用解构 const {data: {code, data, msg}, config} = res
一下,code==200(其实是res.data.code)的时候返回data(其实就是返回res.data.data)
2、error处理
这里的error可以分为3类:
- 服务器端的业务的http error
- 前端的http error
- 前端非http error
dealErr(c, msg) {
console.log("code:" + c)
console.log("msg:" + msg)
switch (c) {
case 400:
Message.error(msg)
break;
case 401:
Message.error('登录过期,请重新登录')
break;
// 404请求不存在
case 404:
Message.error('网络请求不存在')
break;
// 其他错误,直接抛出错误提示
default:
Message.error("系统错误")
}
}
这里封装一个方法,用于处理服务器端的业务的http error和前端的https error,因为他们结构都是相同的。
error => {
console.log("error:" + JSON.stringify(error))
this.destroy(url)
let errorInfo = error.response
if (!typeof(errorInfo) === undefined && !errorInfo) {
const {request: {statusText, status}, config} = JSON.parse(JSON.stringify(error))
errorInfo = {
statusText,
status,
request: {responseURL: config.url}
}
addErrorLog(errorInfo)
const data = {code: status, msg: statusText}
this.dealErr(data.code, data.msg)
} else {
Message.error('网络出现问题,请稍后再试')
}
return Promise.reject(error)
}
但是还有一种error也要处理,这里判断如果error.response不为空,则为http类型的error,使用dealErr,如果为空,则说明是非http类型的err,直接toast 网络出现问题,请稍后再试(比如,前端跨域报错,请求不符合规范等错误,就是非http类型的err)
结构如下
{
"message":"Network Error",
"name":"Error",
"stack":"createError handleError",
"config":{
"url":"http://127.0.0.1:9090/login",
"method":"post",
"data":"username=kk&password=kk",
"headers":{
"Accept":"application/json, text/plain, */*",
"Content-Type":"application/x-www-form-urlencoded"
},
"baseURL":"http://127.0.0.1:9090/",
"transformRequest":[
null
],
"transformResponse":[
null
],
"timeout":0,
"xsrfCookieName":"XSRF-TOKEN",
"xsrfHeaderName":"X-XSRF-TOKEN",
"maxContentLength":-1
}
}
三、高德地图相关功能
1、引入sdk使用
1)在public文件夹下的index.html的
中加入
注意这里需要在body前面,不然有时候地图加载不出来
vue.config.js配置文件中加入
module.exports = {
configureWebpack: {
externals: {
'AMap': 'AMap',
'AMapUI': 'AMapUI'
},
},
}
2)vue文件的template中加入``
注意map需要设置宽高
#map{
width: 100%;
height: 100%;
}
文件中引入mapUI
使用:
通过isEdit去判别是编辑还是新建
这里有一点比较重要:
isShowEdit是用于我们动态去展示和隐藏modal弹框,使用的是v-if。网上很多人,是使用modal的v-model或者value来控制modal的显示和隐藏的,原本我也是那样做的,但是后来发现,那样做非常不稳定和可靠,有时候弹框弹出来,其双向绑定的is-show,并未能和modal的状态统一。所以,最终使用的是div+v-if的方式来控制。
还有就是,关于父组件和子组件之间传值的问题:
父给子,一般是通过props来接收,并且,我们希望的是单向的,父可以改变控制子,但是,子不能改变去控制父,不然会报错。而需要使用,子发事件回调给父,来改变父的状态的方式来实现。
//处理确定
handleConfirm(name) {
this.$refs[name].validate((valid) => {
console.log('handleConfirm validate',valid)
if (valid) {
//发送ok事件
this.$emit('ok', {user: this.user, isEdit: this.isEdit})
//关闭弹框
this.$emit('visible', false)
}
})
},
handleVisible(visible) {
//每次都清空验证信息 因为编辑和创建不一样
this.$refs['user'].resetFields();
//发送事件给父组件 修改自己的visible状态(注意这里 不能用v-model数据绑定 子组件不能修改父组件传来的prop的对象状态)
this.$emit('visible', visible)
}
this.$emit('visible', false)
使用这个来改变其父的isShowEdit
的值,从而隐藏或者显示自身modal。
上传头像
上传头像注意下,如果我们上传图标需要header,比如传token的话,需要把header作为参数传进来。
五、登录验证
对于登录验证,我们这边是使用vue-router的beforeEach统一处理页面的跳转来实现的。
const router = new Router({
routes: routers,
mode: 'history',
})
const turnTo = (to, access, next) => {
// if (canTurnTo(to.name, access, routes)) next() // 有权限,可访问
// else next({replace: true, name: 'error_401'}) // 无权限,重定向到401页面
next()
}
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 {
//这由于暂时没有权限系统 直接 跳转即可
// if (store.state.user.hasGetInfo) {
turnTo(to, store.state.user.access, next)
// } else {
// store.dispatch('getUserInfo').then(user => {
// // 拉取用户信息,通过用户权限和跳转的页面的name来判断是否有权限访问;access必须是一个数组,如:['super_admin'] ['super_admin', 'admin']
// turnTo(to, user.access, next)
// }).catch(() => {
// setToken('')
// next({
// name: 'login'
// })
// })
// }
}
})
router.afterEach(to => {
//设置标题
setTitle(to, router.app)
//隐藏进度条
iView.LoadingBar.finish()
window.scrollTo(0, 0)
})
总结
源码很多,所以其实很多东西都在源码里面了,可能内容篇幅较长,很少有人能够完整看完,但是,写在这里只为某些时候可能遇到类似问题,有一个借鉴参考的地方即可。就如同,我自己手敲admin框架的时候,很多时候iview-amin就是我的一个可以借鉴和参考的项目,遇到有不会的或者没有思路的,就可以参考借鉴一下,这样会好很多。
整个系列,前端、移动端、后端,都有了,打通了,接下来就是要学习一下,怎么打包,部署到服务器那些东西了。
服务器呢,我打算再使用docker,学习一番docker+nginx+mysql等实现前端和后端的线上部署,具体请参看
GPS定位系统(五)——Docker
关于作者
作者是一个热爱学习、开源、分享,传播正能量,喜欢打篮球、头发还很多的程序员-。-
热烈欢迎大家关注、点赞、评论交流!
:https://www.jianshu.com/u/d234d1569eed
github:https://github.com/fly7632785
CSDN:https://blog.csdn.net/fly7632785
掘金:https://juejin.im/user/5efd8d205188252e58582dc7/posts