Vuex到底如何使用

一、简介

1.1什么是Vuex

官方的定义如下:

 Vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件状态,并以相应的规则保证状态以一种可预测的方式发生改变。

官方的解释看的一团迷雾,简单来说就是把一堆可能很多地方(各组件)会用到的数据管理起来,可以对这些变量进行修改,需要的时候就去取。

2.两个小例子:

1) 在项目中很多地方都要进行请求,vue中一个页面的每个组件都可能会有请求,服务器每次请求都需要判断用户的权限、token等信息,这里就可以把用户的权限信息、token等放入一个单独的仓库里面进行管理,每个组件都能对其状态(说白了就是值)进行读取、修改。

说到这里,可能有很多人就会说,这些信息直接放进cookies和session里面就行了啊,干嘛还要单独弄一个空间来存放这些信息。实际上,更多的时候是使用cookie与vuex配合一起使用,vuex获取信息,cookie控制时间和token的销毁。

2) 基于Vue的组件开发,肯定避免不了组件间的传参,父子传参还好说,直接使用prop传,或者父组件调用子组件的函数(使用emit/on)。但涉及到非父子组件之间的传参,常规的办法是建立一个空组件event bus,通过这个中间组件来传参,但这种方式只适用于不涉及到大量的信息传递,涉及到多个组件之间传参的时候,显然event bus就不适用了,这时候vuex就派上用场了,后面会对比vuex和event bus的区别。

所以,说白了,你可以把vuex看做一个数据库,把可能多个组件会用到的共享的参数放到里面,每个组件都可以获取和修改这些数据,用来解决多组件共享一个数据和组件间的通信问题。

说起存储数据,可能有些小伙伴或会想起localstorage,需要注意的是,vuex适用于单页面开发的,如果刷新页面了,或者页面跳转了(注意,是跳转页面,不是页面中组件改变),那vuex中的数据就自动清空了。而localstorage是一种物理存储,是针对于整个应用(application)的,页面刷新数据依然存在。后面也会介绍vuex和localStorage的区别。

二、应用原理

1.原理简述

既然说到vuex相当于一个数据库,那肯定就要定义里面存储那些信息、如何读取或者使用里面的变量,定义的这个文件就是store.js,现在来讲这个store.js里面是如何定义要用到的变量和使用变量的方法的。

store.js里面主要分为了一下几个模块:

1)state: 这里面就定义了我们存入的变量,它可以是任何类型的变量,在此定义之后,才能在vue的组件里面获取。

2)mutation: 定义更改state中变量状态的函数,所有对state中变量的修改只有一条途径,就是通过调用mutation中定义的对变量的操作方法,通过在组件中使用storage.commit(“定义的函数名”,参数)来修改、提交、删除state中的变量。

3)action:刚才说的mutation中定义的方法是同步的,假若想支持异步,比如在其中加入axios请求,就需要在action中定义函数,然后将mutation中的函数作为回调,执行storage.commit(“定义的函数名”,参数),达到异步的效果。关于mutation和action后面会单独分析。

4)getter类似于组件中的计算属性,当需要对state里面的变量进行一些计算再返回的时候,就要使用getter。他会接受state作为顶一个参数,然后从state中获取相应的变量进行计算,返回计算值。

5)module

当state中的变量越来越多,共享参数涉及到的组件也越来越多,如果不进行分类,就会显得比较臃肿,不利于维护,module的作用就是将store分模块,每个模块拥有自己的state/mutation/action,便于维护。

2.了解了以上之后,来看一个简单的例子:

准备好环境之后(搭建vue项目),引入vuex:

npm install vuex --save

src目录下建立store文件夹,文件夹中新建store.js文件,下面是一个简单的store,js:

Vuex到底如何使用_第1张图片

 

这样,就可以在组件中引入store,

通过使用store.state.user获取user,

使用store.commit(“setUser”,”fanrui”)修改属性值;

使用dispatch(“commitUser”,”fanrui”)来进行异步的操作。

3.模块化

上面是一个简单的小例子,刚才说了,对于涉及模块比较多,那这里可以引入module,结构目录如下:

Vuex到底如何使用_第2张图片

 

一个一个来看,这里将所有状态量分为了六个模块(对应modules下的六个文件),每个文件里面都自定义了自己的state、mutation,action,例如user.js文件下:

Vuex到底如何使用_第3张图片

 

然后在统一的一个index.js(这里的index.js就是store.js,可以自己命名)文件中设置每个模块的对外输出:

Vuex到底如何使用_第4张图片

 

在组件中使用时只需要引入外层store文件夹即可调用:

        

Store.state.user.token获取user中的变量

Store.state.user.commit() 调用user中的mutation方法修改变量

Store.state.user.dispatch()调用user中的action的方法异步执行

这就是采用module的方法,有些同学可能觉得每次调用都要写Store.state.user这么长一串比较麻烦,别担心,vuex提供了一个mapActions辅助函数,解决这个问题。具体使用方法在后面解释。

最后一个,解释一下这个getter.js,刚刚说了这个getter相当于计算属性,其实看一下这个getter.js文件,你可能就明白了

Vuex到底如何使用_第5张图片

 

getter里面定义了各种计算变量,默认传入的参数就是整个store的state变量,可以利用state中的任意变量来进行计算,得到想要的属性进行输出,在组件中只需引入getter.js,就可以获取相应状态。   

三、补充说明

1.Vuex和eventbus

先来看看各组件的传参:

1.1 父—>子:

每个子组件上都有一个prop,需要传递的参数放入其中,父组件可以把参数传给子组件,例如:

   父组件中使用子组件,并定义想要传过去的参数: 

子组件通过prop接收:

  

1.2 子—>父:

子组件可以用vue.$emit传递往上传递消息,父组件通过vue.$on回应子组件穿过来的消息:

   子组件中:

        this.$emit(“todo”,{

res: ”子组件发送的消息”
)

父组件中:

this.$on(“todo”,function(data){

     data为传的消息,对data进行处理

})

1.3 兄弟组件间的传递:

emit/on只能在上下层之间传递消息,兄弟之间使用事件总线event bus传递。Evnetbus就相当于一个顺丰快递,将参数在组件传递;首先新建一个传递的媒介bus.js,为空就行,在需要传递的组件中引入bus.js,在一个组件间传出:

        bus.$emit(“queryParam”,message)

在另一个组件中接受:

        bus.$on(“queryParam”,message=>{…….}) 

这里多个组件都可以接受同一个组件发出的消息,只要第一个参数名相同就行。

 1.4 Eventbus缺点: 

这就是组件间的传参,既然已经实现,哪位什么还要引入vuex?

在最开始,我们的项目可能比较简单,但当项目越来愈大,组件越来越多,组件之间的交互也越来越多,组件里就会不断出现$emit这样的代码,主要产生以下几个问题:

1)代码变的冗余,可读性下降(总之就是看着好累,不舒服)

2)对于每一个组件都需要使用emit或者on来处理,设置不同的通信函数(emit、on的第一个参数)

2)很难查找每个事件是从哪里触发的,因为到处都是关于传参的业务逻辑

1.5 小例子说明:

说到这里可能对两者的区别有了些了解,但具体到应用中的区别还不是特别清楚,下面来举一个计数器的小例子。

有一个根组件,姑且叫做S,里面有一属性count。

需求1,写一个组件A作用是实现其中的按钮一次,然后将count计数加一,并在自身的组件中展示当前count的数字;这个比较好办,点击时count计数加1,同时使用event bus将参数count传递到根组件中,进行根组件的展示。

需求2,写一个组件B,作用是将组件S中的count数值为0,这个也好办,直接使用event bus向S组件发起$emit,S接收后,将count置为0;

问题在于,当你点击A组件时,在S组件和A组件上的count都会加1,但是当点击B时,S组件中的count变为0了,但是A组件的count不会发生变化,因为B组件只通知了S,并不知道到A组件会展示count,这就是问题的所在。

 有的小伙伴会说,这不简单,在B中在写一个emit传给A就可以了;的确,这是一个办法,但上面说过了,这只适用于组件较少的情况,当你的组件比较多的时候,就会出现问题

比如在这里有多了个C组件、D组件同样是对count进行操作,也要展示count,那我们是不是得在这些组件之间为每一个组件都写一个emit进行传递,假设有N各组件,那是不是就总共得写N*N+1个emit/on?

第二个问题在于,你在添加组件的时候,你可能并不知道有哪些组件使用到了count,就像上面的例子一样,B只通知了S,而没有通知A。假设在一个项目开发过程中,突然有一个需求让你添加一个对count的操作,暂且不说你要写多少个emit,问题在于你首先得知道在之前有哪些组件用到了count,这也是个问题。

所以针对这种类似的问题,使用vuex是不是更加合理呢,下面来看看vuex是怎么解决上面的问题的吧:

第一个解决的是上面通知组件的问题:

引入vuex之后,将我们的countState放进store的state中作为共享变量,将countState映射到count中,这样每个组件中的count就会随着countState的改变而改变(数据是响应式),任何地方对countState的操作都会映射到各组件中,只要仓库中的countState改变了,各组件中的count也随着更新。

第二个是解决所有组件对状态的操作问题:

比如在需求的增加中,突然对count的大小做个规定,小于50,那对应是不是每个组件对countState进行操作的时候都要先判断其值是不是小于50?这个时候vuex提供了mutation方法,提供对countState变量统一的处理方法,此时就可已在这个方法中加入其值小于50的判断,就不用了在每个组件中单独判断了,将这个逻辑放到vuex中处理,组件只需要调用相应commit方法就行,例如下面的方法:

 mutation:{

    setCountState(state){
            if(state.countState < 50)
             {
                state.countState+=1;

}

else{

          state.countState = 0

}
    }

1.6 结论

在大型应用方面,vuex是一个比eventBus好的解决方案。

Vuex式结构更加清晰,更易调试。

若果不涉及到大量的共享数据、组件通信,其实eventbus就足够了,但是。。。。个人还是觉得vuex用着舒服点。

  1. Vuex、LocalStorage:

  Vuex和LocalStorage都可以用存储数据,对于LocalStorage我会另写一篇文章来详细的描述其原理,这里只单纯的讲一讲这两者的区别:

2.1 区别:

先说说存储方式,Vuex是存在内存中的,而localStorage是存在文件中的,并且localStorage的存储到小有限制,为5M

LocalStorage以文件形式存到本地,主要用于页面间的传参,不同页面通过访问storage记性传参;而vuex主要实在单页面内进行组件间的传值,为响应式数据(重点),各组件可以对其进行修改。

由此可以得出结论,在数据是静态不变的情况下,可以使用LocalStorage;当数据被多个组件共同使用,且要求数据改变后达到响应数据的变化的要求,则使用vuex。

2.2常见用法:

其实一般这两者可以搭配使用,由于LocalStorage不能相应数据的变化,所以一般将LocalStorage的操作写在vuex的mutation里面,使其随着vuex的变化而进行够改变;而对于多页面或者页面刷新之后,vuex将会被清空,这个时候就需要从LocalStorage里面取数据,填补vuex;不过需要注意的是:

vuex和LocalStorage要进行同步更新,这个就是在vuex数据改变之后,在mutation对应的方法中把值传到LocalStorage中;

存入vuex和LocalStroage是否有失效时间,比如像存储用户名、密码、token等这样的字段,就要控制这些变量失效时间;

不是所有放在vuex里面的变量都要在放到LocalStorage里面一份,对于刷新页面或者页面跳转(注意是页面跳转而不是组件跳转)涉及到需要保存内的数据存到LocalStorage里面就行了。

以上仅为个人理解。

3.mutations和actions

在vuex中,操作state中的变量为方法就是使用mutation中的方法,即调用commit()方法,action中修改的方法实际上也是在其中调用commit的方法来完成state的修改。

mutation只能进行同步

action可以进行异步

这两点区别带来的使用方式就在于action里面可以写回调函数,可已将mutation中的方法写进回调函数中。

有些同学可能对js的同步和异步问题有些模糊,这边我也会整理一个文档简述一下js的同步和异步。

4.mapSatete/ mapMutation/ mapAction辅助函数

之前说到,若果使用了module方法,可以将store中的变量模块话,便于管理;但是在调用国的时候显得比较臃肿,如

Store.state.user.token获取user中的变量

Store.state.user.commit() 调用user中的mutation方法修改变量

Store.state.user.dispatch()调用user中的action的方法异步执行

mapXXX系列就是解决这个问题的,画布是多说,基于上面之前讲到的架构,直接上代码解释首先在需要用到的组件中引入并定义:

Vuex到底如何使用_第6张图片

 

之后就可在组件中使用this.方式来进行调用了,特别方便:

Vuex到底如何使用_第7张图片

  

五 实例:使用cookies+vues+localstorage 实现用户的登录管理和token

1.要求

1)匹配用户名正确之后,进入系统,否则提示用户名会密码不正确

2)登录成功后,组件中所有的请求都需要进行登录状态和token的验证(使用vuex实现)

3)登录成功后,一小时内可以直接进入,不需要重新登录(使用vuex和cookies实现,1小时时间使用cookies控制)

4)登陆过的用户,下次登录时保存了当前的用户名,只需要输入密码即可(使用LocalStorages实现)

5)其中登录状态变量用来管用户的登录状态,token是用户发起请求的权限验证问题;这两个也有交叉的地方,比如登录状态过期了,token没过期;token过期了登录状态没过期,都有对应的处置办法。(所以我是不是应该。。。先分开写)

2.先来理一下思路:

用户登录:

登录状态,token存入vuex

将用户名、token存入LocaStorage(这里token为什么存在localstorage里而不是cookies里,后续说明)

登录状态存入cookies

用户请求:

每次请求,判断两个参数:

第一是否处于登录状态,若处于,则可请求,否则退回登录页面,登录状态失效时间由cookies控制

第二,从vuex中获取token,在请求header中携带token发送请求,

第三、服务段要进行token的验证,要判断token是否失效,失效了则转到登录页面(token失效时间由服务端控制)

用户登出:

清空vuex中的token和登录状态置为未登录,且把loacalsorage中的token清楚

一小时之后或登出之后的登录:

从localstorage中获取用户名填入,不需用户在进行填写

关于登录状态:

使用cookies来进行登录状态的,在其失效时写入vuex中,每次请求要从vuex中进行判断,若果失效,则重新登录

token失效日期:

请求前从vuex中获取token放入请求的header中,由服务端返回判断是否失效,失效转到登录页面

刷新页面问题:

因为vuex是单页面的,所以页面刷新后数据清空,需要从localstroage中从新获取token和登录状态,获取登录状态之前同样要判断登录状态是否失效,失效或需重新登录。

这里的问题是登录状态失效了、token失效了都要重新登陆。问题在于如果登录状态没失效,token失效了,那刷新页面之后不会重新登录,但紧接着如果有一个请求事件,由于token失效,那就会回到登录界面,给用户的体验不太好。关于这一点和token的安全性问题,也是为什么这里token要放到localstorage里面的原因,放到之后再说。

3.代码:

先写好需要用到的cookies、localstorage和stroage

3.1 Cookies的封装

这里主要简单封装一下js-cookies

 import Cookies from "js-cookie";

const token = "token";

const name = "userName";

const login_state = false;

 

export function getoken(){

    return Cookies.get(token);

}

 

export function setToken(userToken){

    return Cookies.set(token,userToken)

}

 

//另写一个设置登录有效时间的函数

export function setTimeLoginState(state,time){

    return Cookies.set(login_state,state,{expires:time})

}

 

export function getUserName(){

    return Cookies.get(name);

}

 

export function setUserName(userName){

    return Cookies.set(name,userName)

}

 

export function getLoginState(){

    return Cookies.get(login_state);

}

 

export function setLoginState(state){

    return Cookies.set(login_state,state)

}

 

export function removeCookies(param){

    return Cookies.remove(param);

}

3.2 Locastorage的封装

引入vue自带的good-storage:

import storage from 'good-storage'

 

export function get(key)

{

    return storage.set(key)

}

 

export function set(key,value)

{

    return storage.set(key,value)

}

export function remove(key){

    return storage.remove(key)

}

//其他的方法用到了再来封装

3.3 store仓库

目录架构如下,主要用到user里面的信息,ws是为了体现module的模块化

Vuex到底如何使用_第8张图片

 

第一个是store.js,主要是将user、getters、ws抛出:

import Vue from 'vue';

import Vuex from 'vuex';

import user from './modules/user'

import ws from './modules/ws'

import getters from './getters'

 

Vue.use(Vuex)

 

const store = new Vuex.store({

 

    modules:{

        user,

        ws

    },

    getters

});

 

export default store;

 

然后是getters,这里面主要定义了一些计算属性,使在外部拿到user或ws里面的state属性时更加方便:以下是将user中的token和登录状态定义,外界在获取时通过store.getters.token(其实这样做没必要,我只是想简单的体现一下getters的作用)

const getters = {

    token:state=>state.user.token,

    login_state:satte=>state.user.login_state

}

 

下面就是重点了,user.js,首先是状态,这里面主要存了两个状态:token和登录状态。

因为登录状态要到cookies里面进行超时的设置,所以这里的登录状态是从cookies里面获取,以保证拿到的登录过期时能够一致。

    state :{

        token:"",

        login_state:cookies.getLoginState(),

    },

 

然后就是对状态的操作mutations,这里主要是对token的设置;token同时会放进lostorage中,在页面刷新后vuex中的token丢失后可以总localstorage中获取

    mutations:{

        setToken:(state,token)=>{

           state.token =token;

           localStorage.set("token",token);

        },

 

最后就是action,这里面主要定义了登录、登出的方法,第一个是登录,登录成功后会把后台返回的token存入vuex和localstorage中,并将登录状态设置为true,时间限制为一个小时,放到cookies中,将用户名放入localstorage中,在重新登录时就不用了输入用户名了,直接输入密码。登录失败则提示登录错误,重新尝试。

     login({

          dispatch,

          commit},userInfo){

              return new Promise((resolve,reject)=>{

                userLogin.login(userInfo)

                  .then(({

                      code,

                      data,

                      message

                  })=>{

                      if(code == 200){

                          let {token,isInitPassword} = data

                          commit("setToken",token);

                          cookies.setTimeLoginState(true,1/24);

                          localStorage.set("userName",userName);

                          this.$router.replace("/userInformation");

                          resolve();

                      }else{

                          reject({

                              data,

                              message

                          });

                      }

                  }).catch(()=>{

                      reject({

                          data:0,

                          message:"登录错误,请重新尝试!"

                      });

                  });

              });

 

          },

 

然后就是登出,登出主要是把登录状态、token给清理掉:

  logOut({

            commit

        }){

            return new Promise((resolve,reject)=>{

                userLogin.loginOut().then(()=>{

                    commit("setToken","");

                    cookies.setLoginState(false);

                    resolve();

                })

                .catch(error =>{

                    reject(error);

                })

            })

        }

    }    

    }

3.4 配置界面

首先在App.vue中设置,如果已经登录井进入主页面,否则转到登录页面:

<template>

  <div id="app">

    

    <el-container v-if="isLogin">  el-container>

  

    <router-view v-else>router-view>>

  div>

template>

 

<script>

export default {

  name: 'App'

}

script>

 

 

 

路由配置为:

import Vue from 'vue'

import Router from 'vue-router'

import userInformation from '@/components/init/userInformation'

import login from '@/components/login'

 

Vue.use(Router)

 

export default new Router({

  routes: [

    {

      path: '/',

      redirect:"/login"     //跳转到登录页面

    },

    {

      path:'/login',

      name:login,

      component:login

    },

    {

      path:"userInformation",

      name :"userInformation",

      component:userInformation   //进入个人主页

    }

  ]

})

 

 

为了避免用户直接输入个人主页的路由(即/userInformation)进行访问,所以在main.js中设置对路由的控制,凡是直接访问个人主页路由的,先判断登录状态,若是已登录,则直接进入个人主页,否则跳转到登录页面进行登录:

 

// The Vue build version to load with the `import` command

// (runtime-only or standalone) has been set in webpack.base.conf with an alias.

import Vue from 'vue'

import App from './App'

import router from './router'

import cookies from  './util/useCookies'

 

Vue.config.productionTip = false

 

/* eslint-disable no-new */

router.beforeEach(function(to,from,next){

  let  login_state = cookies.getLoginState();

  if(to.path == '/login'){

    next();

  }else if(!login_state){

    next({path:'/login',replace:true}); 

  }else{

    next();

  }

})

 

new Vue({

  el: '#app',

  router,

  components: { App },

  template: ''

})

 

 

然后就是登录的页面,直接调用store中的action方法进行登录即可:

import request from '../util/request'

 

/*

 *@description:登录

 *@author: fanrui

 *@date: 2019-11-30 15:59:32

 *@version: V1.0.0

*/

export function login(data){

    return request({

        url:"/login",

        method:"post",

        data

    });

}

 

export function logOut(){

    return request({

        url:"/loginOut",

        method:"get"

    })

}

 

 

最后就是个人主页,个人主页在用户如果刷新后,vue中的数据不见了,这时候就要重cookies和localstorage中获取登录状态和token;若果获取刅的登录状态已经过期,则跳转到登录页面。

<script>

import store from  '../../store'

import localstorage from '../../util/localStorage';

import cookies from '../../util/useCookies'

export default{

   name:"userInfromation",

   mounted(){

     let isLogin = islogin();

     if(isLogin){

        store.getters.login_state = cookies.getLoginState();

        store.getters.token = localstorage.get("token");

     }

     else{

         store.dispatch("loginOut");

         this.$router.replace("/login");

     }

   },

   method:{

      isLogin:function(){

          return cookies.getLoginState();

      }

   }

}

script>

 

3.5请求拦截、响应拦截

除此之外,在每次发送请求的时候,都要判断登录状态是否过期,若是过期则会到登录页面;若没过期,则取出vuex中0的token放入请求头部发送给服务端

在得到服务端相应后,根据服务端的会傅token是否过期,若是过期则同样回到登录界面,重新登录吗,以获取最新token.

以上的实现是放在axios中请求拦截器和相应拦截器中:

 

Vuex到底如何使用_第9张图片

 

我这里是将axios封装成了一个request.js,并配置拦截器,详细的封装方法见另外一篇文档:封装axios成request。

以上就是用户登录及请求的管理,可能会存在很多有问题的地方,毕竟我对这个的认知也有限,尤其是登录状态和token这一块,其实有很多方法,这里只是简单的介绍了相关的其中一种用法。

 

六、本文涉及到的其他问题会在另外的文章中说明:

  1. cookies、sessioin、token的区别
  2. 关于token和浏览器访问安全问题
  3. 有些小伙伴可能还不清楚为什么要区分mutation/action,一个是同步一个是异步,所有后面会有一篇专门写前端js的同步和异步

完成日期:2019年12.03

你可能感兴趣的:(前端)