第13章 vue-admin-template
这是一套简单的vue基础课程,主要介绍vue,vue-router,axios,vuex,element-ui等等
本章节继续学习vue的内容,我们以开源项目“vue-admin-template”为基础进行练习。这是一个极简的 vue 后台管理模版,它主要使用的技术就是vue,vue-router,axios,vuex,ElementUI等等。很多开发人员都会基于它做二次开发。
下载地址为:https://github.com/PanJiaChen/vue-admin-template
大家也可以从这里下载:https://download.csdn.net/download/richieandndsc/89009387
下载完毕后将其解压后依次执行如下命令:
# 进入项目目录
cd vue-admin-template
# 安装依赖
npm install
# 启动服务(不是我们之前的npm run serve)
npm run dev
# 然后浏览器访问 http://localhost:9528/
以上就部分截图。如果想要打包正式环境的话,可以执行命令:npm run build:prod
然后将打包好的dist内文件复制到Apache或者Nginx下即可。我们并不打算在原来“vue-admin-template”项目的基础上进行修改,而是重新创建一个空的“my-admin-template”的项目,然后一点点的把需要的文件“迁移”过来。我们先使用“vue create my-admin-template”创建一个vue工程。
这里我们依然选择使用 vue2 类型工程。
工程创建完毕之后,我们就可以在Visual Studio Code中打开它。
接下来要做的就是添加我们需要的依赖,我们不使用之前的命令方式安装,我们直接修改“package.json”里面的“dependencies”和“devDependencies”,前者是我们开发使用的依赖库,后者是我们打包的依赖库。修改后如下:
"dependencies": {
"axios": "0.24.0",
"core-js": "3.25.3",
"echarts": "5.4.0",
"element-ui": "2.15.13",
"js-cookie": "3.0.1",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.12",
"vue-router": "3.4.9",
"vuex": "3.6.0"
},
"devDependencies": {
"@babel/core": "7.12.16",
"@babel/eslint-parser": "7.12.16",
"@vue/cli-plugin-babel": "4.4.6",
"@vue/cli-plugin-eslint": "4.4.6",
"@vue/cli-service": "4.4.6",
"eslint": "7.15.0",
"eslint-plugin-vue": "7.2.0",
"sass": "1.26.2",
"sass-loader": "8.0.2",
"vue-template-compiler": "2.6.12"
},
我们简单介绍一下dependencies依赖。其中axios,element-ui,vue,vue-router和vuex我们之前已经学习过了,并且这里使用的版本和我们学习的版本大致是一样的。
接下来的core-js 是专门用来做 ES6 以及以上API的补丁包。之前我们使用babel将ES6代码转换成ES5代码,目的是为了兼容更多浏览器,比如箭头方法的转换等等。但是如果是async方法、promise对象等等的话,它是没有办法处理了,这里就需要用到core-js了。
接下来echarts是一个图标库,目前使用的比较多,主要用来绘制柱状图,折线图和饼状图。
接下来js-cookie是一个简单的,轻量级的处理cookies的js API。我们有时候会将一些少量数据(用户账号)存储到本地浏览器的cookies中,后续就不需要从服务器端直接获取了。
接下来的Normalize.css 只是一个很小的CSS文件,但它在默认的HTML元素样式上提供了跨浏览器的高度一致性。Normalize.css是一种现代的、为HTML5准备的优质替代方案。就像我们之前写CSS的时候,在开始的位置总会对所以标签做一些统一的样式设置。
接下来的NProgress 是前端轻量级进度条插件。
最后的path-to-regexp也是一个插件,主要用来解析请求url地址以辅助vue-router路由器。
接下来,我们将“node_modules”目录和“package-lock.json”文件删除掉。然后执行“npm install”重新安装所有依赖。在我们日常开发工作中,使用所有依赖库的版本搭配很重要,并不是越新的版本越好。随意修改某个库的版本号,可能会出现一些问题。
我们在Visual Studio Code的终端窗口中运行“npm install”命令即可。
接下来,我们还有修改一下vue的配置文件“vue.config.js”,修改后内容为:
// vue.config.js 配置
module.exports = {
devServer: { port: 90 }
}
我们让其运行在90端口,不要跟Tomcat的8080端口冲突。
然后我们继续执行“npm run serve”命令
接下来,我们打开浏览器访问一下:http://localhost:90/
接下来,我们就开始一点一点的将“vue-admin-template”项目“迁移”到我们当前的“my-admin-template”项目中来。当然,这种“迁移”不是简单的粘贴复制,而是有选择性的改动添加,毕竟源项目中有很多东西不是我们想要的,也有些东西也不适合日常的开发。在“迁移”工作之前,我们需要先了解一下“vue-admin-template”项目的文件结构,这里我们只介绍“src”源码的文件结构:
1. api目录,访问服务端接口,主要包括table.js和user.js两个文件。前者是一个列表数据的获取,而后者是用户登录/信息获取/用户退出的操作。这里的服务端接口访问使用的是“utils/request.js”文件,它是对axios的一个封装,我们之前的练习中也这样做过。这个文件我们需要迁移过去,并且以后针对不同的业务模块,也会创建不同的js文件,里面放置针对该业务模块的一些访问服务端接口的js文件。
2. assets目录,就是资源目录,存储一些图片文件,我们可以保留这个目录。
3. components目录,组件目录,里面就包含Breadcrumb面包屑,Hamburger汉堡包,SvgIcon矢量图标三个vue组件。这个三个组件都是构成主页面的一部分,我们需要“迁移”过去。
4. icons目录,图标资源目录,里面存储了svg格式的矢量图标文件。由于数量比较少,无法满足我们开发需求,因此基本上不使用。我们会使用element-ui的图标库,或者其他第三方的图标库,例如font-awesome库等等。在本项目中我们就不使用它,所以么有必要“迁移”过去。
5. layout目录,布局组件,其实就是页面框架,里面包含导航条,侧边栏,主区域等等。这个是我们需要“迁移”过去的,它是我们后台的主页面,非常的重要。
6. router目录,就是vue-router配置目录,必须“迁移”过去,但会做大的调整。
7. store目录,就是vuex配置目录。里面包含app,settings,user三个模块。第一个app是侧边栏打开/关闭状态的存储,第二个是项目配置相关的存储,第三个是登录用户信息的存储,而getters.js则是vuex共享模块数据(app/settings/user)的一个快捷访问。这个也需要“迁移”过去。
8. styles目录,样式表目录。SCSS(Sassy CSS)是CSS的一种超集,它引入了许多增强的特性和功能,使得编写和维护CSS样式更加方便和灵活。当然,它需要借助“sass”和“sass-loader”插件来处理,不能直接当做css文件使用。我们肯定必须全部“迁移”过去。
9. utils目录,工具目录,提供一些与业务不相关的js操作。该目录中包含五个js文件。第一个auth.js是对token令牌的一个操作。第二个get-page-title.js就是获取页面标题。第三个index.js包含一些时间格式化的方法。第四个request.js是对axios的封装,api目录就需要它,非常的重要。第五个是validate.js包含一些检验方法。这里,我们只需要去“迁移”的是auth.js,request.js和validate.js三个文件即可,其他不需要了。
10. views目录,页面目录,我们要写的页面都在这里了。我们要“迁移”的只有dashboard控制台页面,login登录页面,404.vue页面三个,其他都不需要了。
11. App.vue文件,不用介绍了,里面主要就是一个“<router-view />”路由标签。
12. main.js文件,不用介绍了,里面要加载大部分的内容以及实例化vue对象。
13. permission.js文件,含义为权限相关,其实也没有那么严谨。主要是在vue路由跳转之前做一些检查操作。这个文件需要“迁移”过去,但需要做一些调整。
14. settings.js文件,配置项目相关信息。这个settings.js配置文件中就定义了三个内容,第一“title”页面标题,第二“fixedHeader”是否固定导航条Navbar,第三“sidebarLogo”是否在侧边栏Sidebar显示Logo图片。这个文件我们也需要“迁移”过去,后期我们自己项目的配置信息也可以存储在这里。
介绍完之后,我们先从layout页面说起。首先,我们先介绍几个页面布局名称:侧边栏Sidebar,主区域AppMain,导航条Navbar,汉堡包Hamburger,面包屑Breadcrumb。
在导航条Sidebar中又包含汉堡包Hamburger和面包屑Breadcrumb
其中,汉堡包Hamburger的作用就是打开和关闭侧边栏Sidebar。这是我们大概的一个页面Layout布局,我们要做的开发工作就是点击侧边栏Sidebar的某个菜单后,在AppMain主区域显示该菜单对应的页面内容。另外,当我们用手机模式打开页面的时候,侧边栏Sidebar也会自动关闭以减少占用整体页面的空间“让”给AppMain主区域。
在手机模式下,侧边栏Sidebar会隐藏起来,通过汉堡包Hamburger点击弹框出现。
这个是通过“ResizeHandler.js”来实现,还包括窗口改变的时候也可能会触发此操作。我们再次回到layout页面中,它包含Sidebar,AppMain,Navbar,Hamburger,Breadcrumb五个部分,在项目中它就是一个vue组件,就放在“src”的“layout”目录下“index.vue”文件。其中Sidebar,AppMain, Navbar 三个就定义在“layout”目录下的“components”下,而Hamburger,Breadcrumb则被定义在“src”下的“components”目录下(不知道为什么)。而在“layout”目录下还有一个“mixin”目录,里面就是“ResizeHandler.js”文件。
其实在“src”的“components”目录下还有一个“SvgIcon”组件,它是用来显示矢量图标的。我们不使用这个“SvgIcon”组件,也就不需要“src”下的“icons”目录,然后在“main.js”中也不能使用“import '@/icons'”引入代码,最后还要删除页面中所有使用“<svg-icon>”标签。但是,如果你想使用“SvgIcon”组件的话,你还需要在“package.json”文件中的“devDependencies”项目使用引入“svg-sprite-loader”等插件。
我们先从最简单,依赖最少的开始“迁移”。首先我们将“assets”和“styles”目录复制过来,不用任何改动。接下来我们先把“settings.js”文件复制过来,添加一些注释吧。
module.exports = {
// 页面标题
title: 'My Admin Template',
// 是否固定导航条Navbar
fixedHeader: false,
// 是否在侧边栏Sidebar显示Logo图片
sidebarLogo: false
}
我们修改了标题。接下来,我们复制“utils”下的auth.js和validate.js三个文件,暂时不复制request.js文件。其中auth.js文件内容不变,我们仅添加一些注释,代码如下
import Cookies from 'js-cookie'
// 存储token令牌的关键词
const TokenKey = 'my_admin_template_token'
// 从Cookie中获取token令牌
export function getToken() {
return Cookies.get(TokenKey)
}
// 保存token令牌到Cookie中
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
// 删除Cookie中的token令牌
export function removeToken() {
return Cookies.remove(TokenKey)
}
什么是token令牌,简单理解就是登录用户的唯一标示,是一个加密的字符串,对应的明文一般是用户的一些基本信息,比如用户ID,用户账号,用户姓名等等。对于前后端分离的Web系统开发,服务器端识别客户端的技术就是token令牌。客户端在用户登录成功后,获取服务器端返回的token令牌,后续的所有请求都会附带这个token令牌,这样服务器端就能通过token令牌的解析知道是这个客户端浏览器代表那个登录用户。请求附带token令牌的具体方法,一般都是将token令牌放置到请求头(Header)中。这样修改请求头的作用,也需要服务器的一些支持才可以这么做。
接下来,我们继续复制“validate.js”文件,内容如下所示:
// 检查超链接的协议
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
没错,只有一个方法而已。接下来,我们将“components”下的“Breadcrumb”和“Hamburger”组件复制过来,不需要我们做任何修改。不要忘记把我们当前工程“components”下的“HelloWorld.vue”文件删除掉。接下来,我们将“layout”目录一个复制过来。这里我们需要修改导航栏条目组件“src\layout\components\Sidebar\Item.vue”文件。
if (icon) {
//if (icon.includes('el-icon')) {
//vnodes.push(<i class={[icon, 'sub-el-icon']} />)
//} else {
//vnodes.push(<svg-icon icon-class={icon}/>)
//}
vnodes.push(<i class={icon} />)
}
修改的内容就是将之前的if-else判断全部注释,然后使用“vnodes.push(<i class={icon} />)”代码,主要是解决不使用“<svg-icon>”矢量图标标签,并且兼容其他三方图标样式。到目前为止,layout页面中的侧边栏Sidebar,主区域AppMain,导航条Navbar,汉堡包Hamburger,面包屑Breadcrumb都已经被我们“迁移”过来了。接下来,我们需要“迁移”vuex的store目录。这里我们需要修改“src\store\modules\user.js”文件,代码如下所示:
//import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
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 = {
// 登录操作
login({ commit }, userInfo) {
},
// 获取用户信息
getInfo({ commit, state }) {
},
// 登出操作
logout({ commit, state }) {
},
// 重置token令牌
resetToken({ commit }) {
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
其实就是删除了请求服务器端接口的方法调用。
接下来我们把“views”目录下的“login”目录和“dashboard”目录,以及“404.vue”文件复制进来。首先我们先看登录页面,也就是“src/views/login/index.vue”文件。我们之前讲过,每个vue页面都包含三个部分:<template>页面,<script>脚本,<style>样式。其中<template>页面主要使用Element-ui框架,这里登录页面就使用了表单<el-form>,绑定的数据为“loginForm”,当我们点击“<el-button>”的时候,就会触发“handleLogin”方法。在这个方法中,会先进行一个“this.$refs.loginForm.validate” 表单验证。如果验证成功的话,就会触发“this.$store.dispatch('user/login', this.loginForm)” 方法。这个方法是在vuex中定义的,位置就是“src/store/modules/user.js”文件中。如果登录成功的话,就会“this.$router.push({ path: this.redirect || '/' })” 跳转到控制台dashboard页面。
这里我们需要修改“src\views\login\index.vue”文件。
<svg-icon icon-class="user" /> 改成 <i class="el-icon-user"></i>
<svg-icon icon-class="password" /> 改成 <i class="el-icon-lock"></i>
然后直接删除如下代码:
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
被删除的其实是点击查看明文密码的效果,也就是“眼睛”关闭和打开的图标状态。
<script>
export default {
name: 'Login',
data() {
return {
loginForm: {
username: 'admin',
password: '111111'
},
loginRules: {
username: [{ required: true, message:'请输入账号', trigger: 'blur' }],
password: [{ required: true, message:'请输入密码', trigger: 'blur' }]
},
loading: false,
passwordType: 'password',
redirect: undefined
}
},
methods: {
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.$router.push({ path: this.redirect || '/' })
} else {
return false
}
})
}
}
}
</script>
我们简化了登录操作的逻辑,先不请求服务器端接口,而是直接进入控制台dashboard页面。
接下来,我们添加vue路由器,也就是“src\router\index.js”文件,代码如下:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// 引入layout页面
import Layout from '@/layout'
// 静态路配置
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: 'el-icon-menu' }
}]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
// mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// reset router
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
export default router
我们去掉了其他页面,只保留了登录页面和控制台页面。路由router的配置主要在“constantRoutes”数组中完成。这个配置扩展了vue路由器配置项,增加了类似于meta的属性配置,比如title菜单名称,icon菜单图标等等。还有一个hidden属性,它标识该菜单不再侧边栏上显示出来。因为不是所有路由都是菜单项的。接下来,我们修改“main.js”文件,代码如下:
import Vue from 'vue'
import 'normalize.css/normalize.css'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import '@/styles/index.scss'
import App from './App'
import store from './store'
import router from './router'
//import '@/permission'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
主要引入router和store以及实例化vue对象。
接下来,我们修改“App.vue”文件内容。
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
其实就将内容完全的复制过来。
最后我们执行“npm run serve”命令,然后浏览器访问:http://localhost:90/#/login
目前来看,我们的基本页面是运行成功了。接下来,开始做服务器接口的访问。
首先,我们需要把“src\utils\request.js”文件复制过来,它是对axios的封装,如下所示
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 实例化 axios 对象
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
// 请求拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
// 向请求头中添加token令牌
config.headers['X-Token'] = getToken()
}
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
// 服务器端返回的json数据res
const res = response.data
// 业务状态码code值不等于20000,代表服务器接口发生错误
if (res.code !== 20000) {
// 显示返回的错误信息
Message({ message: res.message || 'Error', type: 'error', duration: 5 * 1000 })
// 50008:非法token; 50012:重新登录; 50014:Token过期;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// 清空本地token令牌后返回登录页面
store.dispatch('user/resetToken').then(() => { location.href = '/login' })
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error)
Message({ message: error.message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
export default service
上述代码中,主要是实例化一个axios对象service,其中定义了“baseURL”和“timeout”两个属性,前者是请求服务器地址前缀,后者是请求服务端接口时效(5秒)。关于请求服务器地址前缀“process.env.VUE_APP_BASE_API”是什么,我们稍后介绍。接下来,就是请求拦截器,就是将登录后返回的token令牌放入请求头中,关键词为“X-Token”。然后就是响应拦截器,我们约定服务器返回的json格式数据必须包含code,message,data三项。其中code是业务状态码,它是一个数值类型,如果是20000则表示接口访问成功,如果不是则接口错误。这个错误可以是我们自定义的,也可以是异常信息等。如果code业务状态码不是20000的话,说明接口发生了错误,我们就需要给用户显示错误信息message。最后就是数据对象data,它可以是一个json对象,或者是复杂的json对象数组。因此,我们这里不处理data数据,而是交个对应的业务模型调用方(它知道data里面到底是什么内容的数据)。这里需要对一些特殊的code业务状态码做特殊的处理。例如50008代表非法token,50012代表有其他客户端重新登录了当前账号,50014代表Token过期,这些情况我们统统做一个处理,就是清除本地token令牌,然后退回到登录页面。
接下来,我们复制api目录。我们与后台交互的代码基本都放置在这里。说白了,就是使用“utils/request.js”对应后台接口的一个再次封装。一般情况下,一个业务模块对应一个js文件。最后在“views”中的页面中直接使用封装好的方法就行了。目前,api目录中有两个文件,一个是user.js文件,另一个是table.js。前者是用户登录的逻辑,后者是一个表格数据的获取。我们首先查看user.js文件中的“login”方法,代码如下所示:
import request from '@/utils/request'
export function login(data) {
return request({
url: '/vue-admin-template/user/login',
method: 'post',
data
})
}
以上代码我们不想详细介绍了,因为我们之前的axios学习就是这样做的。这里,我们需要注意的是,请求服务端地址就是:base url + request url 。其中base url就是“src\utils\request.js”中的“baseURL: process.env.VUE_APP_BASE_API”,而request url就是上面的“url”。那么,我们来解释一下“process.env.VUE_APP_BASE_API”是什么?
我们在“vue-admin-template”原项目根目录(不是src目录啊)下有三个环境配置文件:
开发环境配置文件:.env.development
生产环境配置文件:.env.production
测试阶段环境配置文件:.env.staging
我们查看就知道,三个文件中都定义了“VUE_APP_BASE_API”属性。其中“.env.development”中定义的“VUE_APP_BASE_API”属性值为“/dev-api”,而在“.env.production”中定义的“VUE_APP_BASE_API”属性值为“/prod-api”。
当运行 vue-cli-service 命令时,我们通过传递“--mode”选项参数来决定环境配置文件中载入。
我们来查看“package.json”文件,我们增加了“--mode”参数的设置。
"scripts": {
"serve": "vue-cli-service serve --mode development",
"build": "vue-cli-service build --mode production",
"lint": "vue-cli-service lint"
},
上面定义了三个命令,前两个是我们经常使用的,第一个“serve”对应的是我们经常使用的“npm run serve”命令,实际执行的就是“vue-cli-service serve --mode development”。也就是说,当我们使用“npm run serve”命令的时候,启用的就是“.env.development”环境配置文件,那么我们的“VUE_APP_BASE_API”属性值为“/dev-api”。因此,我们登录服务器地址就是“/dev-api/vue-admin-template/user/login”。这里需要提醒大家的是地址中第一个“/”符号的含义。它是服务器端的“绝对路径”,也就是自己所在WebServer的Web根路径。由于我们使用的是“vue-cli-service”,并且配置端口是90,因此这里的“/”就是“http://localhost:90”。同理,如果我们使用“npm run build”打包话,就会使用“.env.production”文件。那么登录服务器地址就是“/prod-api/vue-admin-template/user/login”。此时这里的“/”代表什么呢?因为是正式的生产环境,因此我们需要把打包好的“成品”放置到Apache或Nginx下,因此“/”代表了Apache或Nginx对应的Web根路径。一般情况下,正式的生产环境都使用域名来访问,并且配置的也是http协议的默认端口80。所以“/”就约等于服务器的域名。通过对比发现,开发环境和正式环境的区别在于“/dev-api”,还是“/prod-api”。
接下来,我们补全登录页面中“handleLogin”方法,代码如下所示
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('user/login', this.loginForm).then(() => {
this.$router.push({ path: this.redirect || '/' })
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
return false
}
})
}
如果表单验证成功,就会触发“this.$store.dispatch('user/login', this.loginForm)”方法。这个方法是在vuex中定义的,位置就是“src/store/modules/user.js”文件中。
import { login, logout, getInfo } from '@/api/user'
// 登录操作
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
其实继续调用了“api/user.js”中的“login”方法。如果登录成功的话,就会将服务器端返回的token令牌保存到vuex中,同时再保存到Cookie中后续使用。
这里我们稍微总结一下。我们在登录页面中先请求了vuex中该定义的login方法,然后继续调用api中的login方法。之所以这样做,是因为我们要将用户访问的数据保存到vuex中。如果我们不需要将返回数据保存到vuex中的话,我们可以在views页面中直接调用api中的接口就行了。请注意,请求参数就是表单<el-form>绑定的数据为“loginForm”,里面包含username和password两项。并且也说明清楚了,账号是“admin”,密码是“111111”。
我们重新访问浏览器地址:http://localhost:90/#/login
我们查看登录地址为:http://localhost:90/dev-api/vue-admin-template/user/login
那么,可能就有疑问了,哪里运行的服务器端呢?我们的接口地址“http://localhost:90/dev-api/vue-admin-template/user/login”在哪里呢?在原“vue-admin-template”项目中使用了mock模拟接口产生的数据。由于我们是进行二次开发,所以肯定是不使用这个mock东西了。我们需要真实的创建服务器端接口程序。如果是这样的话,这里存在两个问题。第一不能访问“http://localhost:90”地址,必须访问Tomcat的“http://localhost:8080”地址;第二我们需要修改登录url为自己配置的地址。为了简单处理,我们将登录地址修改成“http://localhost:8080/login.jsp”。如何修改呢?很明显,我们需要将“base url”改成“http://localhost:8080”,而把“request url”改成“/login.jsp”。
我们先修改“.env.development”文件内容:
# just a flag
ENV = 'development'
# base api
# VUE_APP_BASE_API = '/dev-api'
VUE_APP_BASE_API = 'http://localhost:8080'
那么我们需要“.env.production”文件中修改VUE_APP_BASE_API?可以修改,也可以不修改。一般情况下,正式环境下的Apache或Nginx只提供http的静态服务,也就是处理html,css,js文件,不能处理java文件,所以我们会使用Tomcat(默认8080端口)来提供java服务。如果我们不修改的话,请求的地址会指向Apache或Nginx,他们无法提供java服务。解决的办法就是使用“转发”(其实官方名称是代理)机制,就是让Apache或Nginx将所有访问前缀是prod-api的都转发到Tomcat上。这样大家各自其职,共同完成整个项目的运行。
接下来,我们继续修改“api/user.js”文件内容:
import Qs from 'qs'
import request from '@/utils/request'
export function login(data) {
return request({
url: '/login.jsp',
method: 'post',
data: Qs.stringify(data),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
}
export function getInfo(token) {
return request({
url: '/info.jsp',
method: 'get',
params: { token }
})
}
export function logout() {
return request({
url: '/logout.jsp',
method: 'get'
})
}
注意,我们上面的代码不光修改了url数据。对于post提交的话,我们还修改了Content-Type为表单数据,而其他两个都是get方式,就不需要修改Content-Type类型了。
接下来,我们把服务端的“login.jsp”代码给到大家。代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
// 设置返回类型为json数据
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
// 获取用户提交数据
String username = request.getParameter("username");
String password = request.getParameter("password");
// 正确数据
int code = 20000;
String message = "ok";
String token = String.valueOf(System.currentTimeMillis());
String data = "{\"token\":\""+token+"\"}";
// 检查并返回结果
if(username.equalsIgnoreCase("admin") && password.equalsIgnoreCase("111111")) {
response.getWriter().write("{\"code\":"+code+","+"\"message\":\""+message+"\",\"data\":"+data+"}");
} else {
response.getWriter().write("{\"code\":30000,"+"\"message\":\"error\"}");
}
%>
浏览器在发送跨域请求并且包含自定义 header 字段(我们在“utils/request.js”的请求拦截中添加了“X-Token”的字段)时,浏览器会先向服务器发送 OPTIONS 预检请求(preflight request),探测该请求服务是否允许自定义跨域字段。为了更好的解决这个问题,我们可以直接修改Tomcat安装目录conf/web.xml中,这个web.xml与应用下的“WEB-INF/web.xml”相似的。其实就是增加一个CorsFilter过滤器,配置代码如下
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.headers</param-name>
<param-value>Content-Type,X-Token</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
以上的配置中,我们要注意的是cors.allowed.headers的配置为“Content-Type,X-Token”,它表示我们构造请求时可以使用的请求头。这样我们就不需要在代码中处理跨域问题了。剩下的,我们就将“login.jsp”文件放置到Tomcat安装根目录下的“webapps\ROOT”目录。然后启动Tomcat后,再重启一下“npm run serve”命令,尝试登录试试。
我们打开浏览器访问:http://localhost:90/#/login
点击登录按钮。
我们发现,请求地址“http://localhost:8080/login.jsp”是正确的。
提交的表单数据也是正确的。
返回的响应也是正确的。
在上述的测试中,我们遇到一个问题,即使我们没有登录,也能够访问控制台页面。其实如果我们直接访问“http://localhost:90”的话,访问的就是控制台页面。显然这是不对的。接下来,我们就来增加“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'
import defaultSettings from '@/settings'
// 进度条(路由跳转效果而已)
NProgress.configure({ showSpinner: false })
// 访问白名单
const whiteList = ['/login']
router.beforeEach(async(to, from, next) => {
// 开始进度条
NProgress.start()
// 设置页面标题
document.title = to.meta.title ? to.meta.title : defaultSettings.title
// 获取token令牌
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 如果存在token令牌的话,无须登录直接去控制台页面
next({ path: '/' })
NProgress.done()
} else {
// 获取用户名称
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next() // 存在用户名称的话,就去目标路由地址
} else {
try {
// 没有用户名称的话,就访问接口获取
await store.dispatch('user/getInfo')
// 接口返回之后,再去目标路由地址
next()
} catch (error) {
// 获取用户名称异常就重置token令牌并退出登录页面
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 {
// 没有token令牌的话,必须去登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// 完成进度条
NProgress.done()
})
上述代码只有小小的改动,就是去掉了之前的“getPageTitle”方法,而直接去“settings.js”中获取,实质是一样的。上述使用了router.beforeEach()和 router.afterEach()方法。vue-router 提供“导航守卫”来通过跳转或取消的方式守卫导航。其中 router.beforeEach 是前置守卫,而router.afterEach则是后置守卫。说白了,就是在前端路由跳转中,首先会经过beforeEach方法,而beforeEach可以通过next来控制到底去哪个路由。根据这个特性我们就可以在beforeEach中做一些完全判断。比如验证用户是否登录(判断是否持有token令牌);或者检查用户权限(判断是否拥有当前路由对应的角色)等等。每个守卫方法接受三个参数:第一个 to 表示即将进入的目标路由对象,第二个 from 表示当前导航正要离开的路由,第三个next表示页面跳转。在permission.js文件中的router.beforeEach代码的大致含义为:
第一,用户是否持有token令牌,如果没有则去登录页面。
第二,用户是否持有name信息,没有则请求服务(store中user/getInfo方法)获取。如果条件都满足,则跳转到to目标页面
这个permission.js解决了不登录就能去控制台页面的问题。但是也存在另一个问题,就是我们同样也要获取用户名称的信息,也就是“src\store\modules\user.js”文件中的方法调用。
const actions = {
// 登录操作
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
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) { return reject('用户信息获取失败')}
// 存储用户名称和头像两项信息
const { name, avatar } = data
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// 登出操作
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
removeToken()
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},
// 重置token令牌
resetToken({ commit }) {
return new Promise(resolve => {
removeToken()
commit('RESET_STATE')
resolve()
})
}
}
完整的代码全部放出来了。不要忘记在“main.js”中引入“permission.js”文件,就是将“import '@/permission'”代码解开注释。我们在给出服务器端“info.jsp”文件内容:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
// 设置返回类型为json数据
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
// 硬代码直接返回结果
int code = 20000;
String message = "ok";
String name = "richie", avatar = "";
String data = "{\"name\":\""+name+"\","+"\"avatar\":\""+avatar+"\"}";
response.getWriter().write("{\"code\":"+code+","+"\"message\":\""+message+"\",\"data\":"+data+"}");
%>
最后再把“logout.jsp”文件内容给到大家:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
// 设置返回类型为json数据
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
// 直接返回数据
response.getWriter().write("{\"code\":20000,"+"\"message\":\"ok\"}");
%>
我们把这个两个文件也放入到放置到Tomcat安装根目录下的“webapps\ROOT”目录下。
接下来,我们就可以重新测试一下了,直接浏览器访问:http://localhost:90
由于没有token令牌,所以会强制退到登录页面。
登录接口依然没有问题。我们发现列表中有两个“info.jsp”的请求呢?
第一个是OPTIONS 预检请求,第二个才是get请求。
我们查看“info.jsp”请求返回数据情况。
最后在点击右上角下拉菜单中的“退出”按钮,请求“logout.jsp”接口。
右上角用户信息是在导航条“src\layout\components\Navbar.vue”文件中定义的。
<el-dropdown-item divided @click.native="logout">
<span style="display:block;">Log Out</span>
</el-dropdown-item>
async logout() {
await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
这里就不过多解释了。至于头像就是从vuex中读取的,因为我们服务器端返回空,所以头像是没有的。当然我们可以强制使用一个固定图片代替,例如下代码:
<img src="~@/assets/logo.png" class="user-avatar">
其实就是对应“assets”目录下的“logo.png”图片。
成功回到了登录页面。
接下来,我们就来创建新的页面,一个表单页面,一个表格页面。首先,我们在“views”目录下创建一个“demo”目录,然后在该目录下创建“form.vue”和“table.vue”两个页面文件。
首先是“table.vue”文件内容:
<template>
<div id="table-container">
<el-table :data="tableData" stripe border style="width:100%">
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column prop="id" label="编号" width="100"></el-table-column>
<el-table-column prop="name" label="姓名" width="200"></el-table-column>
<el-table-column prop="age" label="年龄" width="100"></el-table-column>
<el-table-column prop="born" label="生日"></el-table-column>
<el-table-column label="操作">
<el-button size="mini">编辑</el-button>
<el-button size="mini" type="danger">删除</el-button>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'table',
data() {
return {
tableData: [
{ id: 1, name: '张三', age: 20, born: '2010-09-01' },
{ id: 2, name: '李四', age: 20, born: '2010-09-01' },
{ id: 3, name: '王五', age: 20, born: '2010-09-01' },
{ id: 4, name: '赵六', age: 20, born: '2010-09-01' }]
}
}
}
</script>
<style scoped>
#table-container { padding:20px; }
</style>
接下来就是“form.vue”文件内容:
<template>
<div id="form-container">
<el-form :model="form" :rules="rules" ref="ruleForm" label-width="100px" label-position="left">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名"></el-input>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.gender">
<el-radio label="1">男</el-radio>
<el-radio label="2">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="爱好" prop="hobby">
<el-checkbox-group v-model="form.hobby">
<el-checkbox label="1">游泳</el-checkbox>
<el-checkbox label="2">篮球</el-checkbox>
<el-checkbox label="3">跑步</el-checkbox>
<el-checkbox label="4">看书</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="生日" prop="born">
<el-date-picker type="date" v-model="form.born" placeholder="请选择日期"></el-date-picker>
</el-form-item>
<el-form-item label="城市" prop="city">
<el-select v-model="form.city" placeholder="请选择城市">
<el-option label="北京" value="1"></el-option>
<el-option label="上海" value="2"></el-option>
<el-option label="广州" value="3"></el-option>
<el-option label="深圳" value="4"></el-option>
</el-select>
</el-form-item>
<el-form-item label="简介" prop="brief">
<el-input type="textarea" v-model="form.brief" placeholder="请输入简介"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'form',
data(){
return {
form: {
name: '',
gender: '',
hobby: [],
born: '',
city: '',
brief: ''
},
rules: {
name: [{ required: true, message: '请输入姓名', trigger: 'blur'}],
gender: [{ required: true, message: '请选择性别', trigger: 'blur'}],
hobby: [{ type: 'array', required: true, message: '请选择爱好', trigger: 'change'}],
born: [{ required: true, message: '请选择日期', trigger: 'blur' }],
city: [{ required: true, message: '请选择城市', trigger: 'blur' }],
brief: [{ required: true, message: '请输入简介', trigger: 'blur' }]
}
}
},
methods: {
onSubmit: function() {
this.$refs['ruleForm'].validate((valid) => {
if (valid) { alert('ok'); }
else { return false; }
});
}
}
}
</script>
<style scoped>
#form-container { padding:20px; }
</style>
接下来,给上面两个页面添加路由,
{
path: '/demo',
name: 'Demo',
component: Layout,
redirect: '/demo/table',
meta: { title: 'Demo菜单', icon: 'el-icon-menu' },
children: [
{
path: 'table',
name: 'Table',
component: () => import('@/views/demo/table'),
meta: { title: '表单页面' }
},
{
path: 'form',
name: 'Form',
component: () => import('@/views/demo/form'),
meta: { title: '表单页面' }
}
]
},
我们就放在控制台页面路由的后面就行了。
我们配置了两级菜单,我们点击“表格页面”这个子菜单
然后点击“表单页面”这个子菜单。
当然,这里只是静态页面,我们应该从服务器端获取表格数据。因此,我们需要给“table.vue”页面增加读取服务端表格数据的业务逻辑,代码如下:
<script>
import { getList } from '@/api/table'
export default {
name: 'table',
data() {
return {
tableData: []
}
},
created () {
this.getTableData();
},
methods: {
getTableData(){
getList().then(res => {
this.tableData = res.data;
})
}
}
}
</script>
我们在初始化“created”中调用“getTableData”方法,然后继续调用“api/table.js”中的getList方法。如果成功的话,就将返回的数据赋值给数据“tableData”。我们将之前硬代码写的“tableData”已经清空了,目的就是为了从服务器端获取。接下来我们查看“api/table.js”文件内容。
import request from '@/utils/request'
export function getList() {
return request({
url: '/table.jsp',
method: 'get'
})
}
最后就是我们服务端“table.jsp”文件内容
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
// 设置返回类型为json数据
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
// 硬代码直接返回结果
int code = 20000;
String message = "ok";
String data1 = "{\"id\":1,"+"\"name\":\"张3\","+"\"age\":20,"+"\"born\":\"2010-09-01\"}";
String data2 = "{\"id\":2,"+"\"name\":\"李4\","+"\"age\":20,"+"\"born\":\"2010-09-01\"}";
String data3 = "{\"id\":3,"+"\"name\":\"王5\","+"\"age\":20,"+"\"born\":\"2010-09-01\"}";
String data4 = "{\"id\":4,"+"\"name\":\"赵6\","+"\"age\":20,"+"\"born\":\"2010-09-01\"}";
String data = "["+data1+","+data2+","+data3+","+data4+"]";
response.getWriter().write("{\"code\":"+code+","+"\"message\":\""+message+"\",\"data\":"+data+"}");
%>
为了做前后数据对比,我们将人员姓名修改了一下。
数据获取并展示了出来。添加新页面以及与服务器端交互就介绍到这里了。
关于图标这一块,我们也可以使用font-awesome这个库。我们首先将“font-awesome-4.7.0”文件夹复制到“src”根目录下,然后在“main.js”中引入进来,代码如下:
import '@/font-awesome-4.7.0/font-awesome.min.css'
然后在“src/router/index.js”中修改路由的icon属性,例如如下代码:
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: 'Dashboard', icon: 'fa fa-dashboard' }
}]
},
这里就不再过多演示了。
本项目单独下载地址:https://download.csdn.net/download/richieandndsc/89025240
最后在介绍一下“vue-element-admin”开源项目,它与“vue-admin-template”是同一作者。这个“vue-element-admin”的依赖获取比较麻烦,所以我提供了现成的“node_modules”包。大家可以去下载:https://download.csdn.net/download/richieandndsc/89009393
看的出来,这个“vue-element-admin”是一个集成方案,里面包含了很多页面插件(基本上包含了大部分的页面需求),因为两者的基础架构是一样的,所以复用成本也很低。
本章节先介绍到这里。
本课程的内容可以通过CSDN免费下载:https://download.csdn.net/download/richieandndsc/89025243
更多推荐
所有评论(0)