【Vuex】前后端分离Vue路由拦截器与登录cookie保存

1.Vuex 初探

1.1 vuex 介绍

背景

我们在使用vue去写前端的时候,肯定遇到过对不同组件之间传递变量值的需求,但是我们又不能很高校地去解决。vuex就是用来解决这种需求的,让我们的组件们拥有共享变量。

vuex 作用

  • vuex的核心是提供了store容器,容器嘛,容器就是用来装东西的,这里装的可以理解成超级全局变量,每个vue组件都能在这个store仓库中共享超级全局变量。
  • 但是store可不止这个功能,因为vuex的功能是管理 状态state 而不是单单管理变量的

vuex 优点

为什么我要用超级全局变量来说呢?因为它和全局变量是不一样的:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。【从java的角度理解就是这些变量都是私有private,只能set()get()方法去修改,不能直接赋值】

ps:一定要注意,vuex在刷新页面即销毁,涉及刷新页面的数据需要重新赋值给vuex。可以多尝试在当前vue组件的mounted钩子函数去进行vuex数据初始化,保证使用vuex的时候一定有值。

安装

script : 不常用

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

npm :

# 注意!!vue2要安装vuex3.x
npm install vuex@3.6.2 --save
# 如果是这样写,会默认安装vuex4.x,是面向vue3的,无法使用
# npm install vuex --save

yarn :

# 同理要装vuex3.x
yarn add vuex@3.6.2--save

1.2 store 的使用

我们前面讲到 Vuex 的核心就是 store,所以对于我们来说,store就能够满足小项目的需求了。

目录准备

我们一般会在src下建立store目录,在里面创建一个index.js文件来使用vuex。

当然我们也可以在main.js中使用,但是考虑到代码耦合度,分开使用更佳
在这里插入图片描述

当然官方有最规范的目录结构,因为在一个完整项目中store内容一定很多,把整个store放在index.js中是不合理的,所以需要拆分。

store:.
│  actions.js
│  getters.js
│  index.js
│  mutations.js
│  mutations_type.js   ##该项为存放mutaions方法常量的文件,按需要可加入
│
└─modules
        Astore.js

简单使用

store/index.js中定义:

//导入这个是为了Vue.use(Vuex) 挂载vuex对象用来访问
import Vue from 'vue'

//导入vuex包
import Vuex from 'vuex'

//挂载Vuex
Vue.use(Vuex)

// 创建一个新的 store 实例
export default new Vuex.Store({
    state: {
        
        //存放的键值对就是要管理的状态
        user: {
            account: "window.localStorage.getItem()"
        },
        
    },
    //方法,一般包含功能性方法和状态变量的set()get()方法
    mutations: {
        loginSuccess (state, account) {
            state.user.account = account
            window.localStorage.setItem('account',JSON.stringify(account))
        },
        //修改状态变量只能通过这种方式
        setAccount (state, account) {
    		state.user.account = account
		}
    }
})

mian.js中配置:

import store from './store'
new Vue({
   el: '#app',
   router,
   store,
   components: { App },
   template: '<App/>'
})

vue组件中使用:

// 调用访问状态变量
console.log(this.$store.state.user.account)
// 调用访问状态方法  第一个实参是方法名,第二个实参开始是参数列表
this.$store.commit('loginSuccess',this.account)
this.$store.commit('setAccount','xxx')

//注意,只读状态变量可以直接this.去读,但是修改一定要设置一个方法,然后通过访问方法的格式去访问setXX的方法去修改状态变量【状态变量也就是共享变量】

2.localStorage使用

参考文章:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage

2.1 localStorage介绍

window.localStorage 和 window.sessionStorage 都可以用来存放全局变量。但是区别就在于window.sessionStorage在页面关闭的时候就会被回收,而window.localStorage是直接存放在本地浏览器的cookie里,每次vue启动都会扫描本地浏览器的cookie去找有没有要找的属性值。

另外,localStorage 中的键值对总是以字符串的形式存储。 (需要注意, 和js对象相比, 键值对总是以字符串的形式存储意味着数值类型会自动转化为字符串类型).

2.2 localStorage语法

一般写window.localStorage更好,当然不加window应该也没问题

下面的代码片段访问了当前域名下的本地 Storage对象,并通过 Storage.setItem() 对象,并通过 Storage.setItem()增加了一个数据项目。这个数据会直接被添加到本地浏览器的cookie中

放置localStorage 项

localStorage.setItem('myCat', 'Tom');

获取 localStorage 项

let cat = localStorage.getItem('myCat');

移除指定 localStorage 项

localStorage.removeItem('myCat');

移除所有的 localStorage 项

localStorage.clear();

3.路由钩子函数[导航守卫]

参考文章:https://www.jianshu.com/p/ddcb7ba28c5e

3.1 导航守卫介绍

导航表示路由正在从单个页面跳转到另一个页面的过程,这个过程可以添加操作来保证用户是否有权限进行跳转路由。

路由钩子函数一共三种:

  1. 全局钩子,一般可以写在main.js下方或者路由index.js下方:beforeEach、afterEach、beforeResolve

  2. 单个路由进入的钩子:beforeEnter

  3. 进入组件的钩子:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

3.2 全局守卫的使用

无论访问哪一个路径,都会触发全局的钩子函数,位置是调用router的方法

router.beforeEach() 进入前触发

router.afterEach() 进入后触发

当前项目仅依赖router.beforeEach()

/**
* @Param to : 即将要进入的目标路由对象
* @Param from : 正在离开的路由
* @Param next : 必须要调用的方法,next()表示允许进入to这个路由对象,
*               next(false)表示不能进入,next({path:'/'})定向到某地址
*/
router.beforeEach((to,from,next)=>{
    xxxx
})

若没有调用next() 则beforeEach钩子不能被resolved调用

解决刷新页面不执行beforeEach()拦截

原因是router的beforeEach方法定义在vue渲染router之后,如果router.beforeEach放在new Vue之后,因为vue渲染在刷新页面的时候不重复,所以不会重新渲染router,也就无法进入router的钩子函数。因此要修改成下面的顺序:路由钩子在前,vue渲染在后。

//在每次访问路由前调用
router.beforeEach((to,from,next)=>{
   if (to.meta.requireAuth) {
       if(store.state.user.account ) {
          next()
       } else {
          next({
             name: 'login',
             query: {redirect: to.fullPath}
          })
       }
   } else {
      next()
   }
})

//每次调用beforeEach后再渲染页面
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

4.vue的登录拦截器实现

4.1 实现理论

  1. 我们在登录组件提交登录信息的时候,获取后端返回的结果,如果返回信息为成功登录,我们就把当前登录信息放入vuex的store状态变量中,同时把信息存进localStore,让vue放进浏览器的cookie中,这样我们在退出页面一段时间内,仍然能直接登录。
  2. 当我们访问其他需要权限的路由的时候(比如访问 我的空间 这种需要用户登录后才能进入的页面),需要在进入该路由页面之前使用beforeEach钩子函数判断当前浏览器是否有登录信息,如果没有说明没有登录,重定向到login页面,不放行路由。

4.2 安装

npm install vuex --save

参考文章:Vue + Spring Boot 项目实战(六):前端路由与登录拦截器_Evan-Nightly的博客-CSDN博客

4.3 配置store

  1. 在src下创建store文件夹,在store文件夹中创建index.js:
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

//创建store对象
export default new Vuex.Store({
    state: {
        
        //如果本地浏览器有登录信息就直接获取
        user: {
        	//默认是空
            account: "",
        },
        
    },
    
    //方法
    mutations: {
        //成功登录才会进来这里
        loginSucess (state, account) {
            //先把登录信息放进store.state的状态变量中方便后面调用
            state.user.account = account
            //把登录信息放进localStorage,让vue放进浏览器cookie中
            window.localStorage.setItem('account',account)
        },
        //用来改变account的值的方法
        //外界只能通过该方法更改account的值
        initAccount(state, account){
			state.user.account = account
		}
    }
    
})
  1. 在main中注入store的index文件
    在这里插入图片描述

  2. 让vue渲染的时候挂载store
    在这里插入图片描述

4.4 使用localStore

在自己的登录组件login.vue中:

login() {
     if (this.isAccount && this.isPwd) {
        var salt = bcrypt.genSaltSync(11)
        this.password = bcrypt.hashSync(this.password, salt)
         
        //在请求判断是否为网站合法用户的时候加入判断如果成功,调用store的成功登录方法,把登录信息放入cookie中
        this.$http.post("/user/login", {
            account: this.account,
            password: this.password
        }).then((res) => {
            //如果成功登录就调用loginSuccess方法
            if(res.data == "OK"){
            	this.$store.commit('loginSuccess',this.account)
            }
        })
    }
},

4.5 配置路由

  1. 设定history的模式
    在这里插入图片描述

  2. 以下场景作为假设场景,假设我们需要 登录之后 才能访问 我的空间。 那么给/myspace路由一个参数放在meta中,也就是该路由需要验证身份权限。
    在这里插入图片描述

  3. 在main.js中调用路由的钩子函数

router.beforeEach((to,from,next)=>{
   //如果当前要跳转的路由需要认证,则认证
   if (to.meta.requireAuth) {
   	   //获取当前浏览器cookie是否存在账号
   	   let curAccount = window.localStorage.getItem('account')
       //如果cookie中保存有登录信息,就说明登陆过,放行
       if(curAccount) {
          next()
       //如果没有登录到,就强制重定向到login
       } else {
          next({
             name: 'login',
             //下面的query参数会让地址栏多一个重定向提示
             //可以注释掉,这样地址栏会简洁一点
             query: {redirect: '/login'}
          })
       }
   //如果不需要认证就放行
   } else {
      next()
   }
})

4.6 结果检查

未能成功登录的情况

不允许进入,自动重定向到login
在这里插入图片描述

并且爆出异常:
在这里插入图片描述

成功登录,后端返回ok后
在这里插入图片描述

刷新页面后有缓存响应并成功获取

304状态码就是从cookie中获取到信息。
在这里插入图片描述

5.实现定时清理cookie

为了保证用户登录状态安全,一般登录后的cookie缓存会保持一段时间然后自动清除,需要重新登陆以防止被恶意使用。

5.1 理论

  1. 我们登录成功的时候存登录信息的时候追加一个存入登录当前的时间戳(自行了解时间戳),命名为startTime
  2. 当我们每次跳转路由之前的beforeEach函数中都需要判断一下现在访问的时间戳cur和startTime的差是否超过设定的期限,如果超过就remove掉并跳转到登录界面。
  3. 而如果没有超期,那就直接 next() 放行

5.2 实践

1.登录请求部分调用store的成功状态方法

login() {
    if (this.isAccount && this.isPwd) {
        var salt = bcrypt.genSaltSync(11)
        this.password = bcrypt.hashSync(this.password, salt)
        this.$http.post("/user/login", {
            account: this.account,
            password: this.password
        }).then( (res) =>{
            if(res.data == 'OK'){
                //console.log(res.data)
                //如果登录成功就调用成功状态的方法
                this.$store.commit('loginSuccess',this.account)
            }
        })
    }
},

2.store的mutation修改loginSuccess方法
需要在setItem写cookie的时候多写一个当前时间戳

mutations: {
        loginSuccess (state, account) {
            state.user.account = account
            //注意,因为多了个参数组合成json格式,要string化才能写进cookie
            window.localStorage.setItem('account', JSON.stringify({'account':account,'startTime':Date.now()}))
        },
        initAccount (state, account){
            state.user.account = account
        }
    }

3.main.js的beforeEach函数修改

//设定12小时清一次cookie,单位是ms毫秒
//const DEADTIME = 43200000
//测试的时候使用的时间短一点
const DEADTIME = 10000

//在每次访问路由前调用
router.beforeEach((to, from, next) => {
   if (to.meta.requireAuth) {
      //初始化account,如果account令牌存在,则判断是否过期
      let curAccount = window.localStorage.getItem('account')
      if (curAccount) {
         let startTime = JSON.parse(curAccount).startTime

         //如果现在时间的时间戳 - 当前这个令牌创建的时间戳的结果超期了,就清掉token重新登录
         if (new Date().getTime() - startTime > DEADTIME) {
         	//清掉cookie,并且初始化store的account值
            window.localStorage.removeItem('account')
            store.commit('initAccount', "")
            //同时跳转回/login登录页面
            next({
               name: 'login',
               //query: { redirect: '/login' }
            })

         //如果没超期,直接放行
         } else {
            next()
         }

      } else {
         next({
            name: 'login',
            //下面这行会让浏览器多一个重定向提示
            //query: { redirect: '/login' }
         })
      }
   //如果当前要进入的路由不需要验证,则直接放行
   } else {
      next()
   }
})

6.异常解决

6.1 Cannot read properties of undefined (reading ‘$store’)

参考文章:https://www.cnblogs.com/lbzli/p/12873502.html

错误原因

异步请求中的then后使用function(){},this不指向全局vue

}).then(function(res){
    if(res.data == 'OK'){
        console.log(res.data)
        this.$store.commit('loginSuccess',this.account)
    } 
})

解决方法

then后使用 (res)=>{}

}).then( (res) =>{
    if(res.data == 'OK'){
        console.log(res.data)
        this.$store.commit('loginSuccess',this.account)
    }
})

6.2 NavigationDuplicated: Avoided redundant navigation to current location: “/XXX“

意思是在Vue中为了节约资源,一样的内容不会重复加载(浏览器如果发现请求地址一样也不会跳)。所以当你请求跳转相同的路径时,不会直接拼接上去URL上,给你一个警告。

解决方法:判断当前路由位置即可

//我们刚刚编写的beforeEach函数
router.beforeEach((to, from, next) => {
//可以看到有to和from,他们都是router对象
//那我们可以判断他们两者不同的时候再放行即可
//把所有的next()改为下面:
if(to.path != from.path) {
	next();
}
//这样就不会加载重复的路由了。

6.3 [vuex] unknown mutation type: XXX

检查方法名是否一致。

6.4 Error: Redirected when going from “/login” to “/mySpace” via a navigation guard.

报错不影响功能,凑合着吧

7.源码

7.1 登录组件响应调用

login() {
    if (this.isAccount && this.isPwd) {
        var salt = bcrypt.genSaltSync(11)
        this.password = bcrypt.hashSync(this.password, salt)
        this.$http.post("/user/login", {
            account: this.account,
            password: this.password
        }).then( (res) =>{
            if(res.data == 'OK'){
                //console.log(res.data)
                this.$store.commit('loginSuccess',this.account)
            }
        })
    }
},

7.2 store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

//创建store对象
export default new Vuex.Store({
    state: {
        
        //存放的键值对就是要管理的状态
        user: {
            account: "",
        },
        
    },
    //方法s
    mutations: {
        loginSuccess (state, account) {
            state.user.account = account
            window.localStorage.setItem('account', JSON.stringify({'account':account,'startTime':Date.now()}))
        },
        initAccount (state, account){
            state.user.account = account
        }
    }
    
})

7.3 main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import axios from 'axios'

Vue.config.productionTip = false
Vue.prototype.$http = axios

//12小时清一次,ms为单位
//const DEADTIME = 43200000
const DEADTIME = 10000
//在每次访问路由前调用
router.beforeEach((to, from, next) => {
   if (to.meta.requireAuth) {
      //初始化account,如果account令牌存在,则判断是否过期
      let curAccount = window.localStorage.getItem('account')
      if (curAccount) {
         let startTime = JSON.parse(curAccount).startTime

         //如果现在时间的时间戳 - 当前这个令牌的开始时间戳 超期 了,就清掉token重新登录
         if (new Date().getTime() - startTime > DEADTIME) {
            window.localStorage.removeItem('account')
            store.commit('initAccount', "")
            next({
               name: 'login',
               //query: { redirect: '/login' }
            })

         //如果没超期,直接放行
         } else {
            next()
         }

      } else {
         next({
            name: 'login',
            //下面这行会让浏览器多一个重定向提示
            //query: { redirect: '/login' }
         })
      }
   } else {
      next()
   }
})

new Vue({
   el: '#app',
   router,
   store,
   components: { App },
   template: '<App/>'
})

7.4 router/index.js

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/login',
      name: 'login',
      component: Login,
    },
    {
      path: '/mySpace',
      name: 'myspace',
      component: MySpace,
      //重点参数
      meta: {
        requireAuth: true
      }
    }
  ]
})
  • 9
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玖等了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值