关于这个问题,我就只假设一个场景:用户正在填写一个很长很长的表单,然后他填着填着就需要查看一下其他页面表格中的信息,这个时候他一定会去切换路由。好的,等他看完回来,他发现自己刚才用了一个小时填了一半的表单,居然被清!空!了!
当然这只是个笑话,应该没有人会设计一个这样反人类的表单,但是如果这个系统有路由缓存的功能,那么刚才的悲剧是不是就可以避免了呢?所以,路由缓存是一个很有必要的功能。
vue组件缓存:keep-alive => vue3.0官方文档 - keep-alive
路由缓存在vue中其实很容易实现,因为尤大大已经替我们写好了一个名叫keep-alive的组件,它作为一个vue的内置组件,可以在包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。这个描述是不是很符合我们想要实现的功能呢?说一下实现思路吧:
noKeepAlive: true
属性,然后在切换路由时跳过当前组件就这,是不是很简单?那么我们就去实现吧。
首先第一步是实现路由扁平化,由于我的所有子级菜单项,都依赖于一个名叫AppMain的组件(不明白我在说什么的可以参照项目源码wyb199877/multi-tabs),也就是说,所有路由模块都没有直接引用Layout组件(你的主布局组件),而是引用了一个只有router-view的AppMain组件。所以,我们只需要将引用AppMain的中间路由删掉,就可以达到路由扁平化的目的了。所以,我们可以在你router的入口文件中这样写:
// 路由扁平化,将二级以上的路由转换成二级路由
const handleKeepAlive = (to: RouteLocationNormalized) => {
// 判断目标路由记录是否大于2
if (to.matched && to.matched.length > 2) {
// 遍历路由记录,制作面包屑导航列表,并删除依赖于AppMain组件的中间路由,实现路由扁平化
for (let i = 0; i < to.matched.length; i++) {
const element = to.matched[i]
/* 从这里开始处理面包屑 */
if (to.meta.breadcrumb && element.name !== 'index') {
;(to.meta.breadcrumb as BreadcrumbItemProps[]).push({
name: element.name!,
label: element.meta.label as string,
type: element.meta.type as MenuItemType
})
}
if (!to.meta.breadcrumb) {
to.meta.breadcrumb = []
}
/* 从这里结束处理面包屑 */
if (element.components.default.name === 'AppMain') {
to.matched.splice(i, 1)
handleKeepAlive(to)
}
}
} else {
// 当路由已经扁平化完毕的时候,将目标路由自身加入面包屑中
/* 从这里开始处理面包屑 */
const toBreadcrumb = to.meta.breadcrumb as BreadcrumbItemProps[]
if (!toBreadcrumb) return
const isToInToBreadcrumb = toBreadcrumb.some(
(item: BreadcrumbItemProps) => item.name === to.name
)
if (!isToInToBreadcrumb) {
;(to.meta.breadcrumb as BreadcrumbItemProps[]).push({
name: to.name!,
label: to.meta.label as string,
type: to.meta.type as MenuItemType
})
}
/* 从这里结束处理面包屑 */
}
}
router.beforeEach((to, from, next) => {
handleKeepAlive(to)
next()
})
这样,我们就做好了路由扁平化,同时顺便处理了一下面包屑导航的定位(可以先忽略,等到面包屑章节的时候再回来看)。
我们需要在每次切换路由的时候,缓存路由对应的组件名,那么我们就可以在vuex中定义一个模块,专门用来处理标签页的增删和缓存。由于我在编写各个页面的时候,就规定组件名是对应路由名的首字母大写,所以这里的组件名只需要封装一个处理方法,将路由名的首字母大写即可。在src/store/modules
下创建文件tag.ts
:
import { Module } from 'vuex'
import { RouteRecord, RouteRecordName } from 'vue-router'
import { TagStateProps, RootStateProps } from '../typings'
const tagModule: Module<TagStateProps, RootStateProps> = {
namespaced: true,
state: {
cachedList: []
},
mutations: {
ADD_CACHED_VIEW(state, routeName: string | RouteRecordName) {
const bigName = Utils.getBigName(routeName as string)
// 判断路由是否已经存在
const isViewExist = state.cachedList.some((viewName) => viewName === bigName)
// 路由不存在则将路由加入cachedList
if (!isViewExist) {
state.cachedList.push(bigName)
}
},
REMOVE_CACHED_VIEW(state, routeName: string | RouteRecordName) {
const bigName = Utils.getBigName(routeName as string)
// 判断路由是否存在
const isViewExist = state.cachedList.some((viewName) => viewName === bigName)
if (!isViewExist) {
return
}
// 存在就删除
const viewIndex = state.cachedList.indexOf(bigName)
state.cachedList.splice(viewIndex, 1)
}
}
}
export default tagModule
然后去入口文件中处理数据持久化(关于数据持久化请查看同系列文章element-plus按需引入与动态换肤):
// tag模块只需运行时存储
const vuexSession = new VuexPersist({
storage: window.sessionStorage,
key: 'vue-session',
modules: ['tagModule']
})
const store = createStore<RootStateProps>({
modules,
plugins: [vuexLocal.plugin,vuexSession.plugin]
})
现在,我们需要在切换路由的时候,将页面加入缓存。我们回到router的入口文件,修改router.beforeEach
:
router.beforeEach((to, from, next) => {
if (!to.meta.noKeepAlive) {
// 将路由缓存
store.commit('tagModule/ADD_CACHED_VIEW', to.name)
}
// 路由扁平化
handleKeepAlive(to)
next()
})
这里解释一下上面的tagModule/ADD_VIEW
:由于我们之前在定义模块的时候使用了namespace: true
属性,所以当我们使用tag模块的mutation或action的时候,必须要加上tagModule作为命名空间。那么为什么非要是tagModule而不是tag呢?因为我在前面处理模块名的时候,规定了模块名必须是模块文件名加上Module,所以tag模块的模块名就是tagModule。
前面我们已经定义好了tagModule中的cacheList作为已经缓存的路由列表,现在我们只需将这个数组添加到keep-alive的include属性中就可以实现路由缓存了。
我们找到layout中的router-view,然后改成下面这样:
<router-view v-slot="{ Component }">
<keep-alive :include="cachedList">
<component :is="Component" :key="$route.name" />
keep-alive>
router-view>
const cachedList = computed(() => store.state.tagModule.cachedList)
return { cachedList }
至此,我们的多级路由缓存就做好了。
下一篇预告:vue3.0+ts+element-plus多页签应用模板:头部工具栏(上)