第13章 vue-admin-template

本章节继续学习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
 

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值