目录
结合Vuex和Keep Alive实现缓存的动态建立和动态清除
同一组件在多个使用了keep-alive的页面中多次调用的问题
什么是Vuex?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
状态管理模式
这是官方文档中给出的计数例子:
const Counter = {
// 状态
data () {
return {
count: 0
}
},
// 视图
template: `
<div>{{ count }}</div>
`,
// 操作
methods: {
increment () {
this.count++
}
}
}
createApp(Counter).mount('#app')
这个状态自管理应用包含以下几个部分:
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化。
以下是一个表示“单向数据流”理念的简单示意:
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
结合Vuex在多组件中使用Ant Design步骤条组件
Vuex - 单一状态树(State)
Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
在 Vue 组件中获得 Vuex 状态
那么我们如何在 Vue 组件中展示状态呢?由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
每当 store.state.count
变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
然而,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。
Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里。且子组件能通过 this.$store
访问到。让我们更新下 Counter
的实现:
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
实现背景
在项目的创新引导模块中共有问题引导、模型搭建、途径生成、专利检索、方案编辑五个步骤。
在实现时,Stepper步骤条作为独立子组件嵌入到五个页面中,需要实现在各个页面中共享当前步骤状态,除此之外,需要实现根据最大步骤数,实现允许在该步骤之前的点击步骤条页面跳转,而不允许跳转到该步骤之后的页面。
例如,当前最大步骤完成数为n,则允许用户点击步骤条跳转到n之前的步骤,而大于n的步骤页面不允许访问。
实现方式
在单一状态树中定义currentStep和maxStep两个参数,currentStep绑定步骤条当前步骤参数,maxStep在每次步骤完成时实时更新(保存)。
stroe/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// 当前引导步骤
currentStep: 0,
// 当前引导最大已完成步骤
maxStep: 0,
},
})
步骤条组件代码
<a-steps :current="this.$store.state.currentStep" type="navigation" :style="stepStyle" @change="onChange">
<a-step title="问题引导" />
<a-step title="问题模型建立" />
<a-step title="生成解决途径" />
<a-step title="专利智能搜索及标记" />
<a-step title="解决方案形成" />
</a-steps>
步骤变更回调
onChange(current) {
// console.log('onChange:', current);
// this.$emit("current", current)
if (this.$store.state.maxStep >= current){
this.$store.state.currentStep = current
if (current === 0)
this.$router.push('/intelligent_guidance/questions')
if (current === 1 || current === 2)
this.$router.push('/intelligent_guidance/model')
if (current === 3)
this.$router.push('/intelligent_guidance/retrieval')
if (current === 4)
this.$router.push('/intelligent_guidance/solution_edit')
}
},
需要注意的是,vuex的存储内容在页面刷新时会被清除。因此,需要在页面created时打开时定义当前步骤数,最大步骤数推荐从后端(或Redis)读取。下面会讨论在特殊情境下,created函数不执行的处理方式。
结合Vuex和Keep Alive实现缓存的动态建立和动态清除
实现背景
在该模块中,在实现根据步骤条跳转的基础上,期望实现在各个步骤之间跳转时,页面状态保留,因此这里考虑采用Vue的keep-alive插件。
Keep-Alive
-
Props:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
-
用法:
<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和<transition>
相似,<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。当组件在
<keep-alive>
内被切换,它的activated
和deactivated
这两个生命周期钩子函数将会被对应执行。 -
需要注意,使用了keep-alive进行缓存的组件在再次打开时,并不会执行created函数,而是执行activated函数,因此对于有数据更新的部分,需要在activated中声明。
主要用于保留组件状态或避免重新渲染。<!-- 基本 --> <keep-alive> <component :is="view"></component> </keep-alive> <!-- 多个条件判断的子组件 --> <keep-alive> <comp-a v-if="a > 1"></comp-a> <comp-b v-else></comp-b> </keep-alive> <!-- 和 `<transition>` 一起使用 --> <transition> <keep-alive> <component :is="view"></component> </keep-alive> </transition>
注意,
<keep-alive>
是用在其一个直属的子组件被开关的情形。如果你在其中有v-for
则不会工作。如果有上述的多个条件性的子元素,<keep-alive>
要求同时只有一个子元素被渲染。 -
include
andexclude
include
和exclude
prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:<!-- 逗号分隔字符串 --> <keep-alive include="a,b"> <component :is="view"></component> </keep-alive> <!-- 正则表达式 (使用 `v-bind`) --> <keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive> <!-- 数组 (使用 `v-bind`) --> <keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive>
匹配首先检查组件自身的
name
选项,如果name
选项不可用,则匹配它的局部注册名称 (父组件components
选项的键值)。匿名组件不能被匹配。
Vuex - Getter
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。
Getter 接受 state 作为其第一个参数:
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: (state) => {
return state.todos.filter(todo => todo.done)
}
}
})
通过属性访问
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作为第二个参数:
getters: {
// ...
doneTodosCount (state, getters) {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
Vuex - Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 increment
的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:
store.commit('increment')
实现方式
简单使用:结合Router,缓存部分页面
router/index.js
{
path: 'model',
name: 'guidance_model',
component: () => import('../views/Guidance_Model'),
meta: {
title: 'MindFall-创新引导',
keepAlive: true
},
},
App.vue
<template>
<div id="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
页面动态缓存
使用这样简单暴力的方法固然可以实现若干个页面的缓存处理,但是随之而来的便是缓存清理的问题。keep-alive并没有提供直接清理缓存的处理方式,这里采用vuex和include参数进行解决。
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
keepAlive: []
},
mutations: {
SET_KEEP_ALIVE: (state, keepAlive) => {
state.keepAlive = keepAlive
}
},
actions: {
},
modules: {
},
getters: {
keepAlive: state => state.keepAlive
}
})
App.vue或路由父页面
<template>
<div id="app">
<keep-alive :include="keepAlive" >
<router-view/>
</keep-alive>
</div>
</template>
<script>
export default {
data: () => ({
}),
computed: {
keepAlive () {
return this.$store.getters.keepAlive
}
},
}
</script>
在需要建立缓存的情境下:
this.$store.commit('SET_KEEP_ALIVE', ['guidance_questions', 'guidance_model', 'guidance_retrieval', 'solution_edit'])
注意,缓存组件列表中的参数为组件的name参数,并不是router/index.js中的name。
在需要清除某个组件缓存时,将该组件从队列中移除即可。
若要清空所有缓存:
this.$store.commit('SET_KEEP_ALIVE', [])
页面刷新问题
上面提到过,vuex的存储内容在页面刷新时会被清除。也就是说,若在使用了keep-alive的某个页面中进行了强制刷新,则会影响其他页面的缓存效果。这里给出一种简单粗暴的解决方法。
在在使用了keep-alive的页面(重要组件)中created函数(进行缓存的页面打开时不调用)和activated(进行缓存的页面打开时调用)函数中编写一样的操作语句,并同时加上缓存重置语句,可以在一定程度上减少各步骤之间缓存的影响。
created() {
this.$store.state.currentStep = 1
this.$store.commit('SET_KEEP_ALIVE', ['guidance_questions', 'guidance_model', 'guidance_retrieval', 'solution_edit'])
},
activated() {
this.$store.state.currentStep = 1
},
同一组件在多个使用了keep-alive的页面中多次调用的问题
前文提到的步骤条在各个页面之间多次使用,这些页面都进行了缓存处理。这时会出现进行步骤跳转时,跳转到新步骤页面后,由于缓存原因,vue保留了上个页面的状态,对当前页面的步骤数产生影响。
解决方式
这里采用v-if和钩子进行解决。
在离开某一页面时,销毁相同组件的渲染;在进入新页面时,重新渲染该相同组件。
beforeRouterEnter(to, from, next) {
next((vm) => {
vm.isShow = true;
});
},
beforeRouterLeave(to, from, next) {
this.isShow = false;
next();
},
博客中提到的解决方案并不十分理想,敬请指正。