1. 前言
在开发 vue 应用中,有时候 webpack 热更新不好使,于是就手动按 F5 刷新页面,页面居然重新跳转到登录页面去了。经过排除,发现是存储在 vuex 中的 userCode 值为 undefine 了,才导致了出现要重新登录的问题。在 vue 应用中,登录权限功能在 vuex 中到底应该如何做,为什么刷新页面 vuex 里的东西都被清空了呢?下面我们来探索一波。
本文主要讲述以下几点内容:
- 理解 Vuex 模式的原理
- 通过一个用户登录的例子来讲述 Vuex 的应用
- 最后,说说刷新浏览器vuex 内存被清空的原因
2. 理解 Vuex 模式
2.1 介绍
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态一种可预测的方式发生变化。
什么是“状态管理模式”? 让我们从一个简单的 Vue 计数应用开始:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `<div>{{ count }}</div>`,
// actions
methods: {
increment() {
this.counte++
}
}
})
这个状态自管理应用包含了以下几个部分:
- state,驱动应用的数据源
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
以下是一个表示“单向数据流”理念的简单示意:
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏。
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件,一层层传递将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。 对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。(组件的传递、深拷贝等)
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。
2.2 Vue 核心思想
Vuex 应用的核心就是 store(仓库),“store” 基本上就是一个容器,它包含着你的应用中大部分的状态(state)。有些同学可能会问,那我定义一个全局对象,再去上层封装了一些数据存取的接口不也可以么? Vuex 和的单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 从 store 中读取状态的时候,若 store 中的状态变化 ,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交(commit)mutation。这样使得我们可以方便跟踪每一个状态的变化。
通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化和容易维护。
vuex 约定了四个属性,state(初始化变量值),getter(获得变量值),action(异步改变值),mutation(同步改变值)。这个借鉴了命令-查询职责分离。
- 如果一个方法修改了这个对象的状态,那就是一个 command(命令),并且一定不能返回值。
- 如果一个方法返回了一些值,那就是一个 query(查询),并且一定不能修改状态。
下面通过一个用户例子来说说 vuex 的应用。
3. 实战
一个现代的 web 应用中,通常包括日志搜集、埋点、登录验证等功能,而这些功能的状态往往会被整个应用程序中的各个组件使用,有 vuex 会更方便。
- 使用 window 的话,缺少时间旅行功能,调试将变为噩梦,像只使用发布订阅的机制一样,非常难管理。(如一个侧边栏的菜单树,其他组件要对它进行交互的话。)因为,在任何时间,我们应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。也就是可以随意更改,而且没有留下任何记录,后期项目会无法维护。而 vuex 可以追踪变量的改变,而且必须通过固定的属性才能修改。
- 全部变量多了会造成命名污染,vuex 不会,由一个统一的方法去修改数据,全部变量是可以任意修改的。同时解决了父组件与孙组件,以及兄弟组件之间通信的问题。
- vuex 提供了 vue-tool,可以让我们非常直观地查看应用的数据状态。
3.1 用户登录
用户信息是整个应用都要使用的状态,大多数页面都需要判断当前是否已经登录、获取用户的信息,因此放到 vuex 中,可以大大减少后端的请求。
过程:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个 token,拿到 token 之后(可以把这个 token(可以是 userId) 存储到 cookie 中,保证刷新后能记住用户登录状态,前端会根据 token 再去拉去一个 user_info 的接口。
当然实际应用中,在向服务端提交之前对账号和密码做一次简单的检验,以及对用户名进行加密处理,再向服务器提交账号和密码进行验证。
在登录组件中,click 事件点击登录按钮
try {
await this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push({ path: '/'}); // 登录成功后重定向到首页
}) catch (err) {
this.$message.error(err); // 登录失败提示错误
})
action:
// store/moudules/user.js 文件
actions {
async loginByUserName({ commit }, userInfo) {
const loginName = userInfo.loginName.trim()
// 这里可以加密密码
const data = await login({
loginName,
loginPwd,
})
const { userCode } = data;
// 调用 mutation 更改用户状态
commit(SET_USER_INFO, data);
commit(SET_USER_SESSION, userCode)
// 存储到 cookies 中
`Cookies.set('userCode', userCode)`
return data;
}
},
// 对应的 getter 设置
usercode: state => {
return state.userInfo.userCode ? state.userInfo.userCode: getUserCode();
}
这个时候,我把 token 存储到 cookie 中去了,这样就算刷新,也能保持用户的登录状态了。(PS:我们还可以把上次用户访问的路径保存起来,还原用户的历史访问页面)
由于登录是异步的,因此用到 action 来处理,如果不实用 action 的话,涉及到异步的时候,我们需要这样处理:
const data = await getUserInfo(); // 请求后端信息
store.commit('SET_USER_INFO', data); // 再保存到 Vuex 中
上面的操作还可以按login和getUserInfor拆分,初次登录成功后,后端只返回 userCode。之后在用户登录成功之后,可以通过路由全局钩子 router.beforeEach进行拦截,再去请求具体的用户信息,然后把信息更新存储到 vuex 上去。这样就不会出现,某个页面需要用户的信息(如 regioncode)时,页面刷新后不走登录逻辑,就丢失了行政区代码的情况了。
router.beforeEach(async (to, from, next) => {
const hasSession = getUserCode(); // 从 cookies 中取数据
if (hasSession) {
...// 获取用户权限信息
} else {
...// 跳到登录页面
}
}
组件使用存储到 vuex 的数据
this.$store.getters.usercode
上述所有的数据和操作都是通过 vuex 全局管理控制的,刷新页面后 vuex 的内容也会丢失,所以也需要重复上述的那些操作。
3.2 为什么页面刷新后 vuex 的内容会清空呢?
因为 store 里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载 Vue 实例,store 里面的数据就会被重新赋值初始化。
我们来看看 Vuex 的使用的例子:
export default new Vuex.Store({
actions,
getters,
state,
mutations,
modules
// ...
})
再看看 Vuex 的 store 的具体实现:
export class Store {
constructor (options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
if (process.env.NODE_ENV !== 'production') {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `Store must be called with the new operator.`)
}
const {
plugins = [],
strict = false
} = options
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
// strict mode
this.strict = strict
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
if (Vue.config.devtools) {
devtoolPlugin(this)
}
}
}
JavaScript 代码是运行在内存中的,代码运行时的所有变量、函数都是保存在内存中的。刷新页面,以前申请的内存被释放,重新加载脚本代码,变量重新赋值,所有这些数据要想存储就必须存储到外部,如果要进行持久化数据,就要考虑 localStorage、Session Storage、IndexDB 等,让我们可以把数据储存到硬盘上。如果选择 Cookies 的话,不设置过期时间,关闭浏览器就会被清除了。
4. 总结
学会 vuex 可以让我们更好地管理数据状态,什么时候应该使用 Vuex,我们根据数据的类型进行判断。
- 需要响应式的
- 跨级组件通信
- 数据是公用的,像用户信息、或者是控制一些容器组件(如弹出栏)