拉钩教育管理系统项目实战(二)- 登录相关功能

657 阅读11分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

登录功能实现

页面布局处理

这里书写src > views > login >index.vue文件,使用element中的from组件

<template>
  <!-- 登陆界面  -->
  <div class="login">
    <!-- 表单组件,设置ref、绑定数据form,设置顶部对齐 -->
    <el-form ref="form" :model="form" label-width="80px" label-position="top">
      <!-- 表单元素 -->
      <el-form-item label="请输入手机号">
        <el-input v-model="form.userName"></el-input>
      </el-form-item>
      <el-form-item label="请输入密码">
        <el-input v-model="form.passeWord"></el-input>
      </el-form-item>
      <!-- 表单按钮 -->
      <el-form-item>
        <el-button type="primary" @click="onSubmit">登 录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  // 命名组件
  name: 'login',
  // 数据
  data () {
    return {
      // 表单绑定数据
      form: {
        userName: '',
        passWord: ''
      }
    }
  },
  // 方法
  methods: {
    // 点击按钮事件
    onSubmit () {
      console.log('我点击登陆了')
    }
  }
}
</script>

<style lang="scss" scoped>
.login {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  .el-form {
    width: 300px;
    padding: 20px;
    border-radius: 10px;
    background: #fff;
  }
  .el-button {
    width: 100%;
  }
}
</style>

接口测试

我们在每次使用接口之前都要测试接口,接口测试方式:

这里提供一个可用的用户名和密码:17201234567 - qsryja

postman使用方法

63WhH1.png

  1. 点击+新建测试
  2. 在顶部选择方法(例如get、post)
  3. 在后面的输入框内输入接口地址,注意在本项目文档中接口地址分为两部分,要写全
  4. 如果是get方法直接使用第一个参数,如果使用post需要在body(主体中输入参数值,具体参数查看文档)
  5. 点击发送按钮下面会获得返回的数据
  6. 我们可以新建集合把测试的接口存储起来,也可以在集合中设置变量使用差值表达式使用动态餐忽视代替一部分地址

表单校验

做本地输入文本校验,保证输入的内容格式正确,这里直接使用from提供的表单验证功能

校验规则设置

  • required:是否必填项
  • message:提示信息
  • trigger:触发方式,本项目可以使用失去焦点
  • pattern:正则匹配,直接书写正则
  • min:最少位数
  • max:最多位数

这里建议使用es6提供的异步函数功能,async配合await进行功能简化,使用try(可以书写任意代码)和catch(捕获try中的错误,一旦发生错误,执行catch中的代码)代替if条件语句

<template>
  <!-- 登陆界面  -->
  <div class="login">
    <!-- 表单组件,设置ref、绑定数据form,设置顶部对齐 -->
    <!-- 使用:rules="rules"对输入内容进行校验 -->
    <el-form ref="form" :model="form" label-width="80px" label-position="top" :rules="rules">
      <!-- 表单元素,上面使用prop指向校验规则 -->
      <el-form-item label="请输入手机号" prop="phone">
        <el-input v-model="form.phone"></el-input>
      </el-form-item>
      <el-form-item label="请输入密码" prop="password">
        <el-input v-model="form.password"></el-input>
      </el-form-item>
      <!-- 表单按钮 -->
      <el-form-item>
        <el-button type="primary" @click="onSubmit">登 录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  // 命名组件
  name: 'login',
  // 数据
  data () {
    return {
      // 表单绑定数据
      form: {
        phone: '17201234567',
        password: 'qsryja'
      },
      // 校验规则
      rules: {
        // 手机号
        phone: [
          // 必填、正则、错误提示、失去焦点触发校验
          { required: true, pattern: /^1\d{10}$/, message: '请输入正确的手机号码', trigger: 'blur' }
        ],
        // 密码
        password: [
          // 必填、6-18位、错误提示、失去焦点触发校验
          { required: true, min: 6, max: 18, message: '请输入6-18位密码', trigger: 'blur' }
        ]
      }
    }
  },
  // 方法
  methods: {
    // 点击按钮事件。
    // async - 进行异步语法处理,这职位async函数
    async onSubmit () {
      // 使用try - catch替换掉原来的if-else
      // try里面的代码依次执行,一旦发生错误,立马跳到catch里面执行catch里面的代码
      try {
        // 使用await接收结果,这里接收返回的值,两个全部校验通过返回true,否则返回false
        await this.$refs.form.validate()
        console.log('可以登录啦')
      } catch (err) {
        alert('校验失败')
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.login {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  .el-form {
    width: 300px;
    padding: 20px;
    border-radius: 10px;
    background: #fff;
  }
  .el-button {
    width: 100%;
  }
}
</style>

登陆请求发送及处理

  1. 使用之前封装的数据模块访问资源,因为之前使用的是urlencoded格式数据,所以我们这里推荐使用qs插件将对象格式转换为urlencoded,当然这里手动设置请求头也可以
  2. 判断返回的数据是都正确,争取则登陆系统
<script>
// 引入接口模块
import request from '@/utils/request'
// 引入qs插件
import qs from 'qs'
export default {
  // 命名组件
  name: 'login',
  // 数据
  data () {
    return {
      // 表单绑定数据
      form: {
        phone: '17201234567',
        password: 'qsryja'
      },
      // 校验规则
      rules: {
        // 手机号
        phone: [
          // 必填、正则、错误提示、失去焦点触发校验
          { required: true, pattern: /^1\d{10}$/, message: '请输入正确的手机号码', trigger: 'blur' }
        ],
        // 密码
        password: [
          // 必填、6-18位、错误提示、失去焦点触发校验
          { required: true, min: 6, max: 18, message: '请输入6-18位密码', trigger: 'blur' }
        ]
      }
    }
  },
  // 方法
  methods: {
    // 点击按钮事件。
    // async - 进行异步语法处理,这职位async函数
    async onSubmit () {
      // 使用try - catch替换掉原来的if-else
      // try里面的代码依次执行,一旦发生错误,立马跳到catch里面执行catch里面的代码
      try {
        // 使用await接收结果,这里接收返回的值
        // 判断校验收否通过,通过继续向下执行,否则执行catch
        await this.$refs.form.validate()
        // 通过await获取axios获取的数据然后解构获得data
        const { data } = await request({
          method: 'post',
          url: '/front/user/login',
          // 直接使用form,因为form内部的数据和接口的数据一样,并且使用qs插件进行转换
          data: qs.stringify(this.form)
        })
        // 判断data的state是否为1,1表示用户名和密码正确
        if (data.state === 1) {
          // 跳转到主页
          this.$router.push('/home')
          // 提示登录成功
          this.$message.success('登陆成功')
        } else {
          // 提示错误信息
          this.$message.error('请检查您输入的手机号或密码是否有误')
        }
      } catch (err) {
        console.log('校验失败')
      }
    }
  }
}
</script>

处理重复请求

使用button的加载中方式避免重复,当点击登陆按钮之后让按钮处于加载中不可再次点击

<template>
  <!-- 登陆界面  -->
  <div class="login">
    <!-- 表单组件,设置ref、绑定数据form,设置顶部对齐 -->
    <!-- 使用:rules="rules"对输入内容进行校验 -->
    <el-form ref="form" :model="form" label-width="80px" label-position="top" :rules="rules">
      <!-- 表单元素,上面使用prop指向校验规则 -->
      <el-form-item label="请输入手机号" prop="phone">
        <el-input v-model="form.phone"></el-input>
      </el-form-item>
      <el-form-item label="请输入密码" prop="password">
        <el-input v-model="form.password"></el-input>
      </el-form-item>
      <!-- 表单按钮 -->
      <el-form-item>
        <!-- 绑定按钮加载中数据!!!!!!!!!!!!!!!!!!! -->
        <el-button type="primary" @click="onSubmit" :loading="loading">登 录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
// 引入接口模块
import request from '@/utils/request'
// 引入qs插件
import qs from 'qs'
export default {
  // 命名组件
  name: 'login',
  // 数据
  data () {
    return {
      // 表单绑定数据
      form: {
        phone: '17201234567',
        password: 'qsryja'
      },
      // 校验规则
      rules: {
        // 手机号
        phone: [
          // 必填、正则、错误提示、失去焦点触发校验
          { required: true, pattern: /^1\d{10}$/, message: '请输入正确的手机号码', trigger: 'blur' }
        ],
        // 密码
        password: [
          // 必填、6-18位、错误提示、失去焦点触发校验
          { required: true, min: 6, max: 18, message: '请输入6-18位密码', trigger: 'blur' }
        ]
      },
      // 控制按钮是否处于加载中不可点击状态!!!!!!!!!!!!!!!!!!!!
      loading: false
    }
  },
  // 方法
  methods: {
    // 点击按钮事件。
    // async - 进行异步语法处理,这职位async函数
    async onSubmit () {
      // 使用try - catch替换掉原来的if-else
      // try里面的代码依次执行,一旦发生错误,立马跳到catch里面执行catch里面的代码
      try {
        // 使用await接收结果,这里接收返回的值
        // 判断校验收否通过,通过继续向下执行,否则执行catch
        await this.$refs.form.validate()
        // 点击后设置按钮加载中为true!!!!!!!!!!!!!!!!!!
        this.loading = true
        // 通过await获取axios获取的数据然后解构获得data
        const { data } = await request({
          method: 'post',
          url: '/front/user/login',
          // 直接使用form,因为form内部的数据和接口的数据一样,并且使用qs插件进行转换
          data: qs.stringify(this.form)
        })
        // 判断data的state是否为1,1表示用户名和密码正确
        if (data.state === 1) {
          // 跳转到主页
          this.$router.push('/home')
          // 提示登录成功
          this.$message.success('登陆成功')
        } else {
          // 提示错误信息
          this.$message.error('请检查您输入的手机号或密码是否有误')
          // 如果用户名和密码错误恢复按钮状态!!!!!!!!!!!!!!!!!!!!!!!!!
          this.loading = false
        }
      } catch (err) {
        console.log('校验失败')
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.login {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  .el-form {
    width: 300px;
    padding: 20px;
    border-radius: 10px;
    background: #fff;
  }
  .el-button {
    width: 100%;
  }
}
</style>

封装请求方法

改造index.vue文件,将请求方法单独封装起来,后期使用的时候直接调用

  1. 在src目录创建services/user.js文件

    // 引入接口模块
    import request from '@/utils/request'
    // 引入qs插件
    import qs from 'qs'
    
    // 用户登录
    // data为参数
    export const login = (data) => {
      // 使用接口模块
      return request({
        // post方法
        method: 'post',
        // 接口地址
        url: '/front/user/login',
        // 使用qs转换参数
        data: qs.stringify(data)
      })
    }
    
    
  2. 改造vue文件

    <script>
    // 引入外部mogin用户登录模块!!!!!!!!!!!!!!!!!!
    import { login } from '../../services/user'
    export default {
     .........
      // 方法
      methods: {
        // 点击按钮事件。
        // async - 进行异步语法处理,这职位async函数
        async onSubmit () {
          // 使用try - catch替换掉原来的if-else
          // try里面的代码依次执行,一旦发生错误,立马跳到catch里面执行catch里面的代码
          try {
            // 使用await接收结果,这里接收返回的值
            // 判断校验收否通过,通过继续向下执行,否则执行catch
            await this.$refs.form.validate()
            // 点击后设置按钮加载中为true
            this.loading = true
            // 直接调用login进行用户登陆,传入form数据!!!!!!!!!!!!
            const { data } = await login(this.form)
           ...................
          }
        }
      }
    }
    </script>
    

身份认证

当前登录功能还不够完整,表现在我们手动更改为/home后也能进入系统,为了避免这种有门无墙的状态,我们就需要在登录网站使用中时时刻刻确认用户信息,进行身份认证,如果无法认证身份跳转回登陆界面

所以我们在用户登录后应该在系统内添加一个标记,用来证明这个用户是登录的,并且所有组件都可以访问这个标记,但是目前的组件间通信方式都需要有亲密的关系才可以传值,所以我们推荐食使用vuex进行身份认证处理

Vuex

  • vuex是vue官方提供的状态(数据)管理工具
  • 解决了vue项目众多组件数据共享的问题,如果我们只有几个组件,那么使用传统方式即可,只有多组件共享数据才需要使用

Vuex安装和引入

安装:npm install vuex --save

  1. 本项目中我们创建vue的时候安装了vuex,所以系统自动在src目录下创建了store目录并且在下面创建了index.js文件

    // 引入vue
    import Vue from 'vue'
    // 引入vuex
    import Vuex from 'vuex'
    
    // 把vbuex注册为vue插件
    Vue.use(Vuex)
    
    // 使用vuex,在这里面设置vuex
    // 在这里导出vuex实例
    export default new Vuex.Store({
      state: {
      },
      mutations: {
      },
      actions: {
      },
      modules: {
      }
    })
    
    
  2. 在main.js中设置vuex

    // 引入vue
    import Vue from 'vue'
    // 引入根实例
    import App from './App.vue'
    // 引入路由功能
    import router from './router'
    // 引入vuex
    import store from './store'
    // 引入element-ui
    import ElementUI from 'element-ui'
    // 引入element-ui主题文件,因为index.scss里面已经引入了这个文件,这里就不需要再引入了
    // import 'element-ui/lib/theme-chalk/index.css'
    // 引入自定义样式文件
    import './styles/index.scss'
    
    // 将element-ui注册为vue插件
    Vue.use(ElementUI)
    Vue.config.productionTip = false
    
    new Vue({
      // 注入路由和vuex
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
    
    

Vex-state - 全局数据

存储需要共享的数据,全局所有根组件下的组件都可以使用

  • 在视图中访问使用{{$store.state.数据名}}
  • 在js代码中访问使用this.$store.state.数据名
// 使用vuex,在这里面设置vuex
export default new Vuex.Store({
  state: {
    // 这里面设置的数据能被全局组件访问,我们可以把登录的用户信息存放在这里,就可以全局访问,每次进行操作之前先验证数据
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

vuex-mutations - 方法

更改store中数据状态的唯一方法就是提交mutations操作,不允许直接更改store中的数据

export default new Vuex.Store({
  state: {
    num: 1
  },
  // mutations里面书写对state的操作方法,第一个参数必须接受state,第二个参数可选(可以是对象)
  mutations: {
    jia (state, n) {
      state.num += n
    }
  },
  actions: {
  },
  modules: {
  }
})



// 其他文件中调用方法,我们我们也可以把第二个参数写为一个对象,这样就可传入多个值
this.$store.commit('jia', 5)
// 上面的变种写法,type会自动设置为第一个参数,剩下的值自动组成一个对象传入第二个参数
this.$store.commit({type:'jia',x: 5......... })

vuex-action - 异步

vuex-action类似于mutations,不同点在于

  • action只能提交mutations,而不能直接修改状态数据
  • Action内部可以书写异步操作,而mutations内部不能书写异,所以我们一般吧异步放在sction中
// 使用vuex,在这里面设置vuex
export default new Vuex.Store({
  // 全局数据
  state: {
    num: 1
  },
  // 方法
  mutations: {
    jia (state, n) {
      // 设置num为n
      state.num = n
    }
  },
  // 异步处理
  actions: {
    // 参数1随意,作用同$stroe
    jiaGo (store, obj) {
      // 定时器,内部使用数据
      setTimeout(() => {
        // 内部commit到mutations上的方法
        store.commit('jia', obj.num)
      }, obj.time)
    }
  },
  modules: {
  }
})
// 外部调用$store.dispatch从而调用actions里面的方法
this.$store.dispatch('jiaGo', { num: 5, time: 2000 })
this.$store.dispatch('jiaGo', { num: 3, time: 1000 })
this.$store.dispatch('jiaGo', { num: 1, time: 500 })
  1. 外部想使用action中的方法需要使用this.$store.dispatch('方法名', {数据 })实现异步操作
  2. 在方法内部使用commit的方式调用mutations中的同步代码

更多vuex可以查看文档

登陆状态存储

  1. 存储登录信息:将登陆时获取到的数据content存储起来,但是发现一旦页面刷新存储的信息就消失了
  2. 持久化数据:借助浏览器的windows.localStorage的setItem方法将数据存储在浏览器缓存中,然后vuex的state使用和getItem方法读取用户数据
// login - index.vue文件

// 写在login模块点击登陆按钮的方法中,一旦判断确定账号密码正确,就将登陆信息保存起来
..........
if (data.state === 1) {
    // 调用vuex的store模块,向内传输数据
    this.$store.commit('setUserInfo', data)
................
// store - index.js文件

// 使用vuex,在这里面设置vuex
export default new Vuex.Store({
  // 全局数据
  state: {
    // 用户信息,后面鉴权会使用
    // 获取浏览器持久化数据中的user-cwn,转换成对象存起来,并且如果没有的话用空对象替代
    user: JSON.parse(window.localStorage.getItem('user-cwn')) || {}
  },
  // 方法
  mutations: {
    // 添传入数据做本地持久化
    setUserInfo (state, obj) {
      // 不管怎样先把数据存进user一份,否则会导致第一次登陆卡在登陆界面(别问我怎么知道的)
      state.user = JSON.parse(obj.content)
      // 直接把传入进来的数据中的content持久化道本地浏览器,这样就可以保证刷新页面数据不会丢失
      window.localStorage.setItem('user-cwn', obj.content)
    }
  },

注意json数据的转换,存储起来的数据可以在application(应用程序) - storage(存储) - localstorage(本地存储)中查看

校验访问权限

  • 使用路由导航守卫在每次路由跳转的时候判断
  • 需要设置哪些页面路由需要权限校验,需要配合路由元信息
// router - index.js文件

// 引入vue和router
import Vue from 'vue'
import VueRouter from 'vue-router'
// 引入vuex的store模块!!!!!!!!!!!!!!!!!
import store from '../store/index'
Vue.use(VueRouter)
// 设置路由规则
// 这里把除login和404之外的其他组件都设置为layout的子路由
// 将使用的模块全部替换为懒加载路由并使用webpack的魔法注释功能设置打包后的文件名前缀
const routes = [{
  // 登陆模块,单独为一个页面
  path: '/login',
  component: () => import(/* webpackChunkName:'login' */ '@/views/login/index')
}, {
  // 设置/和其子路由都需要校验!!!!!!!!!!!!!!!!!!!!!!!!
  meta: { requiresAuth: true },
  path: '/',
  component: () => import(/* webpackChunkName:'layout' */ '@/views/layout/index'),
  // 给布局容器添加子路由,其他子路由要全部都在布局容器下显示
  children: [{
    // homepath直接设置为空,这样/就也会显示home的内容,把hone作为登陆后的首页内容
    path: '',
    name: 'home',
    component: () => import(/* webpackChunkName:'home' */ '@/views/home/index')
  }, {
    path: '/role',
    component: () => import(/* webpackChunkName:'role' */ '@/views/role/index')
  }, {
    path: '/menu',
    name: 'menu',
    component: () => import(/* webpackChunkName:'menu' */ '@/views/menu/index')
  }, {
    path: '/resources',
    name: 'resources',
    component: () => import(/* webpackChunkName:'resources' */ '@/views/resources/index')
  }, {
    path: '/course',
    name: 'home',
    component: () => import(/* webpackChunkName:'coures' */ '@/views/course/index')
  }, {
    path: '/user',
    name: 'user',
    component: () => import(/* webpackChunkName:'user' */ '@/views/user/index')
  }, {
    path: '/Advertising',
    name: 'Advertising',
    component: () => import(/* webpackChunkName:'Advertising' */ '@/views/Advertising/index')
  }, {
    path: '/AdvertisingSpace',
    name: 'AdvertisingSpace',
    component: () => import(/* webpackChunkName:'AdvertisingSpace' */ '@/views/Advertising-space/index')
  }, {
    // 404模块,单独为一个页面
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName:'404' */ '@/views/404/index')
  }]
}]
// 创建vuetouter实例
const router = new VueRouter({
  routes
})

// 导航守卫!!!!!!!!!!!!!!!!!!!!!!!!!!!!
router.beforeEach((to, from, next) => {
  // 判断要跳转的路由是否需要校验,如果不需要校验直接跳转
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 如果需要校验,判断vuex的user是都为空
    if (!store.state.user) {
      // 如果为空代表没有登陆,就要跳转到登陆界面
      next('/login')
    } else {
      // 不为空则代表已经登陆了,直接跳转就可以了
      next()
    }
  } else {
    next()
  }
})

// 导出实例
export default router

本项目中除了login和404页面都需要验证权限

登陆后跳转到上次访问页面

当前的项目还不能实现登陆后跳转到之前的页面,每次登陆只能跳转到首页,我们需要实现访问需要登陆之后跳转回我们当初登陆前想要访问的页面

导航守卫中设置,把to的多fullPath(fullPath可以存储地址+参数,而path只能存储地址)作为参数传给login(使用query),在login中登陆跳转的时候尝试获取这个参数,如果有这个参数则跳转到这个参数,否则跳转到首页

// 导航守卫
router.beforeEach((to, from, next) => {
  console.log(to)
  // 判断要跳转的路由是否需要校验,如果不需要校验直接跳转
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 如果需要校验,判断vuex的user是都为空
    if (!store.state.user) {
      // 如果为空代表没有登陆,就要跳转到登陆界面
      next({
        name: 'login',
        // 传递参数
        query: {
          // 把访问失败的路由地址存起来!!!!!!!!!!!!!!!!!!!!!!!!!
          go: to.fullPath
        }
      })
    } else {
      // 不为空则代表已经登陆了,直接跳转就可以了
      next()
    }
  } else {
    next()
  }
})
if (data.state === 1) {
          // 调用vuex的store模块,向内传输数据
          this.$store.commit('setUserInfo', data)
          // 跳转到主页,并且直接使用参数,如果没有参数跳转到主页!!!!!!!!!!!!!!!!!
          this.$router.push(this.$route.query.go || '/')
          // 提示登录成功
          this.$message.success('登陆成功')
        } else {

用户信息与接口鉴权

在每次使用接口的时候还要进行鉴权,否则任何用户只要知道接口就能获得我们的数据

用户登陆系统后,服务器返回的数据data中会带一个access_token属性,这个属性值作为请求头的参数之一要参与每次接口操作进行鉴权,可以在postman中进行调试

通过postman统一设置token

可以在postman集合中设置统一的变量,本项目里面使用的是Authorization,可以在变量中新建

  1. 集合上选择编辑
  2. 选择授权(Authorization)
  3. 类型:Api key
  4. 键:Authorization
  5. 值:我们用户登录使用的token

6teI76.png

这样操作之后,postman请求接口的时候会自动加上Authorization头的设置

实现用户信息展示

要实现页面信息展示功能,需要我们在layout中的header组件创建的时候(钩子函数)获取当前用户的信息,然后渲染到组件上,接口还是单独封装

// src - services - user.js文件

// 引入接口模块
import request from '@/utils/request'
// 引入qs插件
import qs from 'qs'
// 引入vuex
import store from '@/store'

// 用户登录
// data为参数
export const login = (data) => {
  // 使用接口模块
  return request({
    // post方法
    method: 'post',
    // 接口地址
    url: '/front/user/login',
    // 使用qs转换参数
    data: qs.stringify(data)
  })
}

// 用户信息获取模块
export const getUserInfo = () => {
  return request({
    // get方法
    method: 'get',
    // 接口地址
    url: '/front/user/getInfo',
    // 设置请求头,带上token作为校验
    headers: {
      Authorization: store.state.user.access_token
    }
  })
}

// src - views - layout - components - app-header.vue文件

<template>
<!-- 创建一个元素包裹全部顶栏 -->
  <div class="header">
    <!-- 面包屑导航,这里没有设置任何功能,后期会对这里进行修改 -->
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>活动管理</el-breadcrumb-item>
      <el-breadcrumb-item>活动列表</el-breadcrumb-item>
      <el-breadcrumb-item>活动详情</el-breadcrumb-item>
    </el-breadcrumb>
    <!-- 下拉菜单 -->
    <el-dropdown>
      <span class="el-dropdown-link">
        <!-- 把文字换成头像,尺寸35,绑定src属性并设置默认值 -->
        <el-avatar :size="35" :src="src || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"></el-avatar>
        <!-- 右侧箭头 -->
        <i class="el-icon-arrow-down el-icon--right"></i>
      </span>
      <!-- 下拉菜单选项,现在是静态值,后期会设置 -->
      <el-dropdown-menu slot="dropdown">
        <!-- 使用差值表达式绑定数据 -->
        <el-dropdown-item>{{userName}}</el-dropdown-item>
        <el-dropdown-item divided>登出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
// 引入user.js并解构获取获取用户信息模块
import { getUserInfo } from '@/services/user'
export default {
  // 设置需要绑定的数据
  data () {
    return {
      src: '',
      userName: ''
    }
  },
  // 生命周期钩子函数
  created () {
    // 调用函数
    this.getInfo()
  },
  // 函数
  methods: {
    // 用于获取用户信息,本项目主要使用名字和头像数据
    async getInfo () {
      // 调用获取用户信息模块并获取其中的data数据
      const { data } = await getUserInfo()
      // 将data中的数据更新到组件data中
      this.userName = data.content.userName
      this.src = data.content.portrait
    }
  }

}
</script>

<style lang="scss" scoped>
// 容器高度、flex布局、居中对齐
.header {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  // 下拉菜单头像区域设置flex布局,居中对齐
  .el-dropdown-link {
    display: flex;
    align-items: center;
  }
}

</style>

注意:生命周期钩子里面不建议直接书写js解构,最好使用封装后的方法

通过拦截器设置token

在拦截器内添加统一的token值吗,这样以后就不需要设置了

// src - store - index.js文件

// 引入axios
import axios from 'axios'
// 引入vuex的store
import store from '@/store'

// 创建axios实例,并设置通用属性
const request = axios.create({
  // 超时时间 - 2s
  timeout: 2000,
  // 默认前缀
  baseURL: ''
})

// axios拦截器,在发起请求之前
request.interceptors.request.use(config => {
  // 根判断传入的 url 前缀,设置不同的 baseURL
  config.baseURL = /^\/front/.test(config.url) ? 'http://edufront.lagou.com' : 'http://eduboss.lagou.com'
  // 获取vuex-state-user
  const { user } = store.state
  // 判断是否存在存user和user.access_token是否为空
  if (user && user.access_token) {
    // 设置请求头
    config.headers.Authorization = user.access_token
  }
  // 返回新的配置对象
  return config
})

// 导出实例
export default request


//现在可以删除src - services - user.js文件中的vuex引入和请求头设置了

用户退出

清除用户信息(清空持久化数据),回到登陆界面

使用native修饰符给组件设置事件,因为我们直接给组件设置事件都是自定义事件

这是弹框组件确认退出

// src - views - layout - components - app-header.vue文件

<template>
<!-- 创建一个元素包裹全部顶栏 -->
  <div class="header">
    <!-- 面包屑导航,这里没有设置任何功能,后期会对这里进行修改 -->
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>活动管理</el-breadcrumb-item>
      <el-breadcrumb-item>活动列表</el-breadcrumb-item>
      <el-breadcrumb-item>活动详情</el-breadcrumb-item>
    </el-breadcrumb>
    <!-- 下拉菜单 -->
    <el-dropdown>
      <span class="el-dropdown-link">
        <!-- 把文字换成头像,尺寸35,绑定src属性并设置默认值 -->
        <el-avatar :size="35" :src="src || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"></el-avatar>
        <!-- 右侧箭头 -->
        <i class="el-icon-arrow-down el-icon--right"></i>
      </span>
      <!-- 下拉菜单选项,现在是静态值,后期会设置 -->
      <el-dropdown-menu slot="dropdown">
        <!-- 使用差值表达式绑定数据 -->
        <el-dropdown-item>{{userName}}</el-dropdown-item>
        <!-- 按钮添加自定义事件,使用修饰符转换为默认事件!!!!!!!!!!!!!! -->
        <el-dropdown-item divided @click.native="logout()">登出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
// 引入user.js并解构获取获取用户信息模块
import { getUserInfo } from '@/services/user'
export default {
  // 设置需要绑定的数据
  data () {
    return {
      src: '',
      userName: ''
    }
  },
  // 生命周期钩子函数
  created () {
    // 调用函数
    this.getInfo()
  },
  // 函数
  methods: {
    // 用于获取用户信息,本项目主要使用名字和头像数据
    async getInfo () {
      // 调用获取用户信息模块并获取其中的data数据
      const { data } = await getUserInfo()
      // 将data中的数据更新到组件data中
      this.userName = data.content.userName
      this.src = data.content.portrait
    },
    // 用于用户退出事件!!!!!!!!!!!!!!!!!
    logout () {
      // 弹出弹框,二次确认
      this.$confirm('确认退出吗?', '退出提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        // 确认之后调用vuex的退出方法
        this.$store.commit('logout')
        // 跳转到登陆页
        this.$router.push({
          name: 'login'
        })
        // 提示框
        this.$message({
          type: 'success',
          message: '退出成功!'
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '取消退出'
        })
      })
    }
  }

}
</script>

<style lang="scss" scoped>
// 容器高度、flex布局、居中对齐
.header {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  // 下拉菜单头像区域设置flex布局,居中对齐
  .el-dropdown-link {
    display: flex;
    align-items: center;
  }
}

</style>

// src - store - index.js文件

// 引入vue
import Vue from 'vue'
// 引入vuex
import Vuex from 'vuex'

// 把vbuex注册为vue插件
Vue.use(Vuex)

// 使用vuex,在这里面设置vuex
export default new Vuex.Store({
  // 全局数据
  state: {
    // 用户信息,后面鉴权会使用
    // 获取浏览器持久化数据中的user-cwn,转换成对象存起来,并且如果没有的话用空对象替代
    user: JSON.parse(window.localStorage.getItem('user-cwn')) || null
  },
  // 方法
  mutations: {
    // 添传入数据做本地持久化
    setUserInfo (state, obj) {
      // 不管怎样先把数据存进user一份,否则会导致第一次登陆卡在登陆界面(别问我怎么知道的)
      state.user = JSON.parse(obj.content)
      // 直接把传入进来的数据中的content持久化道本地浏览器,这样就可以保证刷新页面数据不会丢失
      window.localStorage.setItem('user-cwn', obj.content)
    },
    // 用于用户退出!!!!!!!!!!!!!!!!!
    logout (state) {
      // 向localStorage传空值
      window.localStorage.setItem('user-cwn', null)
      // 清空user
      state.user = null
    }
  },
  // 异步处理
  actions: {
  },
  modules: {
  }
})

token处理

token过期处理

token并不是永久有效的,本项目token过期时间为24小时,如果token过期后再将token发送给服务器,服务器就会认为这是一个错误的token,会返回错误

服务器返回信息中一些关于token的信息

  • access_token:用户鉴权使用的token
  • expires_in:token过期时间,一般单位为分钟,具体要看接口文档
  • refresh_token:用于自动刷新token的标记,当服务器发现token过期,服务器接收到refresh就会认为我们是登陆的但是过期了,会自动帮我们刷新token,我们只需要把新的token替换掉旧的token即可

当然我们可以手动重新登录也会刷新token

自动更新token

  • 方案1:
    • 在本地计时监测expires_in时间是否快要过期了,如果快过期或者已经过期了(在请求拦截器中)立马将refresh_token放松到服务端获取新的token
    • 问题:本地监测依赖于本地时间data,所以可能会因为时间问题出现一些错误。可能会导致失败
  • 方案2(推荐使用,稳定):
    • 客户端数据正常发送,在响应拦截器中监测一旦服务器返回401就证明我们的tiken已经过期,这时我们就将expires_in发送给服务器获得新的token,然后再使用新的token发送一次请求
    • 问题:会请求两次数据。浪费了一次请求

错误处理

我们需要在axios拦截器的响应拦截器中设置失败之后的处理函数,可参考axios处理错误

还在要拦截器内部处理全部4XX状态吗的处理,并使用element通知组件通知用户

  • 400:请求参数错误
  • 401:token过期,权限校验失败
  • 403:没有权限,请联系管理员
  • 404:请求资源不存在
  • >=500:请求超时
// src - utils - requset.js文件

// 引入axios
import axios from 'axios'
// 引入vuex的store
import store from '@/store'
// 引入element的通知组件
import { Message } from 'element-ui'

// 创建axios实例,并设置通用属性
const request = axios.create({
  // 超时时间 - 2s
  timeout: 2000,
  // 默认前缀
  baseURL: ''
})

// axios拦截器,在发起请求之前
request.interceptors.request.use(config => {
  // 根判断传入的 url 前缀,设置不同的 baseURL
  config.baseURL = /^\/front/.test(config.url) ? 'http://edufront.lagou.com' : 'http://eduboss.lagou.com'
  // 获取vuex-state-user
  const { user } = store.state
  // 判断是否存在存user和user.access_token是否为空
  if (user && user.access_token) {
    // 设置请求头
    config.headers.Authorization = user.access_token
  }
  // 返回新的配置对象
  return config
})

// 响应拦截器,在服务器响应请求后触发
request.interceptors.response.use(function (response) {
  // 响应成功触发 - 即状态码 2xx
  return response
}, function (error) {
  // 响应失败触发 - 即状态码非 2xx
  //  - 判断返回状态码,实现不同的功能
  if (error.response.status === 400) {
    // 使用element-ui组件
    Message.error('参数错误')
  } else if (error.response.status === 401) {
    Message.error('token错误')
  } else if (error.response.status === 403) {
    Message.error('您没有权限,请联系管理员')
  } else if (error.response.status === 404) {
    Message.error('您访问的资源不存在')
  } else if (error.response.status >= 500) {
    Message.error('请求超时')
  }
  // 将错误抛出
  return Promise.reject(error)
})

// 导出实例
export default request

刷新token

状态码为401还会分为两种情况:

  • 无token信息:用户未登录,跳转回login并且把当前路由信息带过去,可以使用router.currentRoute.fullpath
  • token无效:错误或者过期,使用文档中的刷新token接口处理,传递refresh_token得到新token更新旧的token,但是要注意监测有没有请求到新的token
    • 没有则表示请求错误,清空用户数据并返回首页,注意保存路由参数
    • 如果请求到了新的token,则保存新token(把整个返回的数据保存起来),并且重新发送请求,参数直接使用错误信息中的config
// src - utils - requset.js文件

// 引入axios
import axios from 'axios'
// 引入vuex的store
import store from '@/store'
// 引入element的通知组件
import { Message } from 'element-ui'
// 引入router路由模块
import router from '@/router'
// 引入qs
import qs from 'qs'

// 创建axios实例,并设置通用属性
const request = axios.create({
  // 超时时间 - 2s
  timeout: 2000,
  // 默认前缀
  baseURL: ''
})

// axios拦截器,在发起请求之前
request.interceptors.request.use(config => {
  // 根判断传入的 url 前缀,设置不同的 baseURL
  config.baseURL = /^\/front/.test(config.url) ? 'http://edufront.lagou.com' : 'http://eduboss.lagou.com'
  // 获取vuex-state-user
  const { user } = store.state
  // 判断是否存在存user和user.access_token是否为空
  if (user && user.access_token) {
    // 设置请求头
    config.headers.Authorization = user.access_token
  }
  // 返回新的配置对象
  return config
})

// 响应拦截器,在服务器响应请求后触发
request.interceptors.response.use(function (response) {
  // 响应成功触发 - 即状态码 2xx
  return response
}, function (error) {
  // 响应失败触发 - 即状态码非 2xx
  // 判断返回状态码,实现不同的功能
  if (error.response.status === 400) {
    // 使用element-ui组件
    Message.error('参数错误')
  } else if (error.response.status === 401) {
    // 我们只能处理 401 状态码,但是 401 也可能有两种情况:无token 和 token错误
    // - 1、无token信息(用户未登录)--> 带参数跳转回登陆页
    if (!store.state.user) {
      // 调用封装的跳回首页函数
      toLogin()
      return Promise.reject(error)
    }
    // - 2、token过期或者错误
    // - 使用refresh_token获取新数据替换掉旧数据
    return request({
      method: 'post',
      url: '/front/user/refresh_token',
      data: qs.stringify({
        refreshtoken: store.state.user.refresh_token
      })
    }).then((res) => {
      // - 响应成功判断返回数据是否正常
      // - - 异常带参数跳回首页
      if (res.data.state !== 1) {
        store.commit('setUserInfo', null)
        toLogin()
        return Promise.reject(error)
      }
      // - - 正常则将新数据替换掉旧数据
      store.commit('setUserInfo', res.data)
      return request(error.config)
    }).catch((err) => {
      console.log(err)
    })
  } else if (error.response.status === 403) {
    Message.error('您没有权限,请联系管理员')
  } else if (error.response.status === 404) {
    Message.error('您访问的资源不存在')
  } else if (error.response.status >= 500) {
    Message.error('请求超时')
  }
  // 将错误抛出
  return Promise.reject(error)
})

// 封装跳转回首页的函数
function toLogin () {
  router.push({
    name: 'login',
    query: {
      // 因为zaijs文件中不能直接使用route所以使用router.currentRoute.fullPath保存跳转前的路由地址
      go: router.currentRoute.fullPath
    }
  })
}
// 导出实例
export default request

处理token重复刷新

一个页面中可能会出现多个接口请求,因为请求是异步的,所以每个请求携带的token都是一样的,但一旦token过期了,即会导致每个请求都会重新获取一次新token,导致重复刷新token(但实际上用来刷新token的refresh_token只能使用一次)

  • 设置一个变量(默认为false),当刷新token之前判断现在有没有人刷新token,如果有则不刷新,否则刷新并设置变量为true,并设置变量刷新完毕token之后把变量设置回false(在请求的.finally中设置,finally无论成功或者失败都要执行)
  • 我们使用变量导致只能有一个请求能向下执行,导致如果存在多个请求一旦token过期只能有一个成功获取数据,所以我们要将所有因为变量而不能更新token的请求存起来,刷新token之后再发送一次
// src - utils - requset.js文件

// 引入axios
import axios from 'axios'
// 引入vuex的store
import store from '@/store'
// 引入element的通知组件
import { Message } from 'element-ui'
// 引入router路由模块
import router from '@/router'
// 引入qs
import qs from 'qs'
// 创建信号量,用于标记当前是否正在刷新token,默认为true
let loading = false
// 创建请求数组,用于接纳所有因为token过期而无法正常获取数据的请求
let requests = []

// 创建axios实例,并设置通用属性
const request = axios.create({
  // 超时时间 - 2s
  timeout: 2000,
  // 默认前缀
  baseURL: ''
})

// axios拦截器,在发起请求之前
request.interceptors.request.use(config => {
  // 根判断传入的 url 前缀,设置不同的 baseURL
  config.baseURL = /^\/front/.test(config.url) ? 'http://edufront.lagou.com' : 'http://eduboss.lagou.com'
  // 获取vuex-state-user
  const { user } = store.state
  // 判断是否存在存user和user.access_token是否为空
  if (user && user.access_token) {
    // 设置请求头
    config.headers.Authorization = user.access_token
  }
  // 返回新的配置对象
  return config
})

// 响应拦截器,在服务器响应请求后触发
request.interceptors.response.use(function (response) {
  // 响应成功触发 - 即状态码 2xx
  return response
}, function (error) {
  // 响应失败触发 - 即状态码非 2xx
  // 判断返回状态码,实现不同的功能
  if (error.response.status === 400) {
    // 使用element-ui组件
    Message.error('参数错误')
  } else if (error.response.status === 401) {
    // 我们只能处理 401 状态码,但是 401 也可能有两种情况:无token 和 token错误
    // - 1、无token信息(用户未登录)--> 带参数跳转回登陆页
    if (!store.state.user) {
      // 调用封装的跳回首页函数
      toLogin()
      return Promise.reject(error)
    }
    // 节流器 - 只允许一个请求可以更新token
    if (!loading) {
      loading = true
    } else {
      // 其他请求全部以函数的方式存储在requests数组中
      requests.push(() => {
        request(error.config)
      })
      return
    }
    // - 2、token过期或者错误
    // - 使用refresh_token获取新数据替换掉旧数据
    return request({
      method: 'post',
      url: '/front/user/refresh_token',
      data: qs.stringify({
        refreshtoken: store.state.user.refresh_token
      })
    }).then((res) => {
      // - 响应成功判断返回数据是否正常
      // - - 异常带参数跳回首页
      if (res.data.state !== 1) {
        store.commit('setUserInfo', null)
        toLogin()
        return Promise.reject(error)
      }
      // - - 正常则将新数据替换掉旧数据
      store.commit('setUserInfo', res.data)
      return request(error.config)
    }).catch((err) => {
      console.log(err)
    }).finally(() => {
      // finally在请求最后无论如何都要执行
      // - 重置信号量、执行数组中的每一个函数、最后清空数组
      loading = false
      requests.forEach((item) => item())
      requests = []
    })
  } else if (error.response.status === 403) {
    Message.error('您没有权限,请联系管理员')
  } else if (error.response.status === 404) {
    Message.error('您访问的资源不存在')
  } else if (error.response.status >= 500) {
    Message.error('请求超时')
  }
  // 将错误抛出
  return Promise.reject(error)
})

// 封装跳转回首页的函数
function toLogin () {
  router.push({
    name: 'login',
    query: {
      // 因为zaijs文件中不能直接使用route所以使用router.currentRoute.fullPath保存跳转前的路由地址
      go: router.currentRoute.fullPath
    }
  })
}
// 导出实例
export default request