前言
学如逆水行舟,不进则退。
日常鼓励式开头,今天又是元气满满的一天哦,哈哈哈~
话不多说,进入正题。
用Vue
也有一段时间了,加上最近做的项目里面涉及了很多组件,所幸多花点时间整理整理,方便回顾和下次使用。本文实用的是Vue 2.x
版本,后面时机成熟再更新Vue 3
版本的封装。
千里之行始于足下,加油打工人~~
组件地址:github。
组件功能
具体功能如下:
- 打开新页面,缓存目标页面;
- 关闭已打开的页面,清空相应的缓存;
- 菜单右键实现:重置当前、关闭当前、关闭其他、关闭左侧、关闭右侧、全部关闭;
- 页面切换动画;
准备
初始化项目:
// @vue/cli 4.5.6
vue create vue-comp
安装必要的依赖:
// element-ui@2.14.1;sass-loader@10.1.0;node-sass@5.0.0;
npm install element-ui node-sass sass-loader --save-dev
// vuex@3.6.0;vue-router@3.4.9;
npm install vuex vue-router --save
简单布局:
准备几个页面并配置路由,大众化的中后台布局,这块就不贴代码了,有需要可参考github上的样例代码(本文样式文件几乎都不会贴出来哈,重要的是思路和逻辑嘛~)。
思路
使用vuex
和 keep-alive
实现中后台项目页面缓存(移动端类似)。
通过keep-alive
的include
属性控制哪些页面需要被缓存;
在vuex
中创建变量两个变量:
-
cachedViews
:用于存储需要缓存的页面的集合(需要特别注意的是,存到数组中的必须是组件的name
属性对应的值,缓存功能才能生效)。数据格式:
['Comp1', 'Comp2', ...]
-
visitedViews
:当前访问的所有页面的集合(存入的值为当前路由的信息,即:vm.$route
)。数据格式:
[{ path: '/comp1', name: 'Comp1', ... }, { path: '/comp2', name: 'Comp2', ... }, ...]
实现思路:
- 当访问页面时,将当前路由信息存入
visitedViews
,并判断该页面是否需要被缓存(在设置路由时,添加meta
对象,设置title
,有则表示需要缓存,否则反之。具体的判断条件可根据业务场景制定),如需要,则将当前路由的name
存入cachedViews
。 - 清空页面时,移除
cachedViews
和visitedViews
中对应的项。
基础实现
创建存储变量
在 vuex
中新建cache.js
:
const state = {
visitedViews: [],
cachedViews: []
}
const mutations = {}
const actions = {}
export default {
namespaced: true,
state,
mutations,
actions
}
设置 keep-alive
修改页面渲染的组件(我这里是创建了AppMain.vue
组件进行页面渲染),使其支持缓存功能。
在组件中设置了两个计算属性:
-
cachedViews
:当前已缓存的项。 -
key
:用于标识路由,值设置为当前路由的fullPath
。
这里做一下补充,关于router-view
的属性key
,不同设置之间的区别:
-
不设置
key
:同一组件之间跳转时(例如:
/comp/a => /comp/b
、/comp?param=a => /comp?param=b
),将不再执行created
、mounted
,获取数据的操作需要放到beforeRouteUpdate
方法中执行。钩子函数执行的顺序:
beforeRouteUpdate
。 -
设置
key
为$route.path
:-
/comp/a => /comp/b
:动态路由,$route.path
的值不一样,所以组件不会复用。钩子函数执行的顺序:
beforeRouteUpdate
=>created
=>mounted
。 -
/comp?param=a => /comp?param=b:
$route.path的值一样,所以此时和不设置
key`的情况一致。钩子函数执行的顺序:
beforeRouteUpdate
。
-
-
设置
key
为$route.fullPath
:/comp/a => /comp/b
、/comp?param=a => /comp?param=b
:对应的$route.fullPath
的值不一样,所以组件不会复用。钩子函数执行的顺序:
beforeRouteUpdate
=>created
=>mounted
。
<template>
<section class="app-main">
<div class="app-main-content">
<keep-alive :include="cachedViews">
<router-view :key="key"/>
</keep-alive>
</div>
</section>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'AppMain',
computed: {
...mapState('cache', ['cachedViews']),
key() {
return this.$route.fullPath
}
},
}
</script>
设置 tabs
作用:作为打开过的页面的快捷访问入口。
新建组件TabsView
,组件样式可以参照element-ui
里的“tabs
”标签页。我这里之所以自己写样式是因为我觉得这样组件的可控性更高,同时也方便以后业务方面的一些扩展。
<template>
<div class="tabs-view-container">
<div class="tabs-view-wrapper">
<router-link
v-for="tab in visitedViews"
:key="tab.name"
:class="`tabs-view-item ${isActive(tab) ? 'active' : ''}`"
ref="tab"
:to="tab.fullPath"
tag="span"
>
{{tab.title}}
<i class="el-icon-close" />
</router-link>
</div>
</div>
</template>
<script>
export default {
name: 'TabsView',
data() {
return {
visitedViews: [{title: '概览', name: 'Dashboard'}, {title: '测试'}]
}
},
methods: {
isActive(route) {
return route.name === this.$route.name
},
}
}
</script>
先来看一下这个组件。
这里先写死一个变量用来循环router-link
,中间显示的内容是路由信息里面设置的title
。通过函数 isActive
判断当前激活的项,并设置相应的类名(因为要传入参数,所以用的是函数而不是计算属性)。
此时系统的显示情况如下图所示:
添加缓存页面
前面的准备工作基本是已经做好了,下面进入核心阶段~
由于变量visitedViews
和cachedViews
是放到vuex
中进行管理的,所以数据的操作依赖对应的mutation
和action
。
在mutations
对象中添加两个函数:
ADD_VISITED_VIEW
:添加已访问的页面。ADD_CACHED_VIEW
:添加需要缓存的页面。
const mutations = {
// 添加
ADD_VISITED_VIEW(state, view) {
if (view.meta && view.meta.hidden) return
if (state.visitedViews.some(v => v.name === view.name)) return
state.visitedViews.push(Object.assign({}, view, {
title: view.meta.title || 'no-name'
}))
},
// 如果设置了 noCache,则不需要缓存(可自定义条件)。
ADD_CACHED_VIEW(state, view) {
if (view.meta && view.meta.hidden) return
if (state.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
state.cachedViews.push(view.name)
}
}
}
在actions
对象中添加三个函数:
addView
:分发addVisitedView
和addCachedView
。addVisitedView
:提交ADD_VISITED_VIEW
。addCachedView
:提交ADD_CACHED_VIEW
。
const actions = {
// 添加
addView({ dispatch }, view) {
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
addVisitedView({ commit }, view) {
commit('ADD_VISITED_VIEW', view)
},
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
}
}
修改TabsView
的script
:
简单分析一下:
修改的部分主要是将上面写死的visitedViews
的值改成了通过addTabs
函数动态添加的值。在初始化组件和路由切换的时候执行该函数,同时通过变量selectedTab
存储当前显示页面的路由信息(后面会用到哦)。
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'TabsView',
data() {
return {
selectedTab: {}, // 记录当前 tab
}
},
computed: {
...mapState('cache', ['visitedViews']),
},
watch: {
$route() {
this.addTabs()
}
},
created() {
this.addTabs()
},
methods: {
...mapActions('cache', ['addView']),
isActive(route) {
return route.name === this.$route.name
},
// 添加 缓存页
addTabs() {
if (this.$route.name) {
this.addView(this.$route)
this.selectedTab = this.$route
}
return false
},
}
}
</script>
测试一下:现在访问各个页面,tabs
标签栏会将未添加过的页面添加上;刷新页面,会清空tabs
标签栏并将当前页面作为初始页面(如果需要刷新的时候保留前面打开过的项,可以把数据同步存储在 sessionStorage
里,初始化的时候读取 sessionStorage
里的值。这个功能这里就不讲啦,逻辑还是很简单的)。
有了添加,下面就是删除了~
删除缓存页面
删除缓存页面的实现方式和上一节基本一致,但需要注意的是,删除的项如果是当前显示的页面,则需要进行页面跳转。
在 mutations
对象中添加两个函数:
DEL_VISITED_VIEW
:删除已访问的页面。DEL_CACHED_VIEW
:删除缓存中的记录。
const mutations = {
// ...
// 删除单个 [i, v] => [0, {name: 'xxx'}]
DEL_VISITED_VIEW(state, view) {
for (const [i, v] of state.visitedViews.entries()) {
if (v.name === view.name) {
state.visitedViews.splice(i, 1)
break
}
}
},
DEL_CACHED_VIEW(state, view) {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
}
}
在actions
对象中添加三个函数:
delView
:分发delVisitedView
和delCachedView
。delVisitedView
:提交DEL_VISITED_VIEW
。delCachedView
:提交DEL_CACHED_VIEW
。
const actions = {
// ...
// 删除
delView({ dispatch }, view) {
dispatch('delVisitedView', view)
dispatch('delCachedView', view)
},
delVisitedView({ commit }, view) {
commit('DEL_VISITED_VIEW', view)
},
delCachedView({ commit }, view) {
commit('DEL_CACHED_VIEW', view)
},
}
修改TabsView
的script
部分:
分析:
删除缓存页面我是从以下几个方面去思考的(执行删除动作后):
-
visitedViews
数组有值。两种情况:
- 触发删除的项不是当前显示的页面,正常删除即可。
- 触发删除的项是当前显示的页面,则删除后需要切换当前激活的页面(本文是切换为最后一项)。
-
visitedViews
数组没有值。两种情况:
- 触发删除的项不是概览页面,则跳转到概览页面。
- 触发删除的项是概览页面,则重新加载概览页面。
<template>
<!-- ... -->
<i class="el-icon-close" @click.prevent.stop="closeSelectedTab(tab)" />
<!-- ... -->
<template/>
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'TabsView',
// ...
methods: {
...mapActions('cache', ['addView', 'delView']),
// ...
closeSelectedTab(tab) {
// 删除 tab
this.delView(tab)
// 更新视图
this.toUpdateView(tab)
},
// 更新视图
toUpdateView(tab) {
const latestView = this.visitedViews.slice(-1)[0]
if (latestView) { // 当前还存在访问过的页面
// 删除项的name 和 当前路由的name 一致时
if (tab.name === this.$route.name) this.$router.push(latestView.fullPath)
} else {
if (tab.name === 'Dashboard') { // 当最后一个删除的项是概览页时,需要 "重新加载" 概览页
// TODO 临时方案
this.$router.push({
path: '/common',
query: {
test: Math.random() * 1000
}
})
} else this.$router.push('/common')
}
}
}
}
</script>
这里只贴了修改的部分(没有全贴出来是为了一方面突出修改的部分,另一方面是避免篇幅太长,所以小伙伴们就先凑合看吧~)。
这里预留了一个功能,在重新加载概览页面的地方。由于现在还没有做“重置当前”的功能,所以就先用了临时方案:页面跳转的方式,实现的“重新加载”功能。
根据上面的分析思路进行测试,当然,我们还需要测试一下页面切换是否可以实现缓存。
随便写个输入框进行测试,执行下面两条“用例”:
- 输入内容后跳转其他页面再返回,内容还在;
- 关闭页面再打开,内容清空;
至此,基本的添加、删除功能已经实现了~
下面我们来进行功能扩展。
功能扩展
添加tab
右键菜单,提供以下功能:重置当前、关闭当前、关闭其他、关闭左侧、关闭右侧、全部关闭。
右键菜单
实现这些扩展功能之前,要先在TabsView
组件中实现右键菜单,主要包括菜单的:样式和内容、显示/隐藏、位置、点击事件的功能。
样式和内容
-
menuVisible
:控制右键菜单显示/隐藏; -
menuPos
:控制菜单位置; -
menuConfig
:控制当前tab
是否有 关闭左侧和关闭右侧 的功能; -
menuMinWidth
:tab DOM
宽度的最小值(后面用于计算菜单的位置);
<template>
<div class="tabs-view-container">
<!-- ... -->
<ul v-show="menuVisible" :style="menuPos" class="contextmenu">
<li @click="refreshSelectedTab">重置当前</li>
<li @click="closeSelectedTab(selectedTab)">关闭当前</li>
<li @click="closeOthersTabs">关闭其他</li>
<li :class="{'not-allowed': menuConfig.left}" @click="closeDirTabs('left')">关闭左侧</li>
<li :class="{'not-allowed': menuConfig.right}" @click="closeDirTabs('right')">关闭右侧</li>
<li @click="closeAllTabs">全部关闭</li>
</ul>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'TabsView',
data() {
return {
selectedTab: {}, // 记录当前 tab
menuVisible: false,
menuPos: {},
menuConfig: {
left: true,
right: true,
},
menuMinWidth: 100,
}
},
// ...
methods: {
// ...
// 重置当前页
refreshSelectedTab() {},
// 关闭当前 缓存页
closeSelectedTab(tab) {},
// 关闭其他 缓存页
closeOthersTabs() {},
// 删除 左侧/右侧 缓存页
closeDirTabs() {},
// 关闭所有 缓存页
closeAllTabs() {}
}
}
</script>
显示和隐藏
实现步骤:
-
在
router-link
标签上添加右键事件:@contextmenu.prevent.native="openMenu($event, tab)"
-
实现打开、关闭方法:
-
当点击
tab
时,触发openMenu
,打开菜单并确定菜单的位置。 -
设置
watch
监听menuVisible
变量,当菜单显示时,给body
绑定关闭菜单的事件,触发点击后,关闭菜单。当菜单隐藏时,移除监听的事件。
-
<template>
<div class="tabs-view-container">
<div class="tabs-view-wrapper">
<router-link
v-for="tab in visitedViews"
:key="tab.name"
:class="`tabs-view-item ${isActive(tab) ? 'active' : ''}`"
ref="tab"
:to="tab"
tag="span"
@contextmenu.prevent.native="openMenu($event, tab)"
>
{{tab.title}}
<i class="el-icon-close" @click.prevent.stop="closeSelectedTab(tab)" />
</router-link>
</div>
<!-- ... -->
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
// ...
watch: {
// ...
menuVisible(boolean) {
if (boolean) document.body.addEventListener('click', this.closeMenu)
else document.body.removeEventListener('click', this.closeMenu)
}
},
methods: {
// ...
// 打开 缓存页签 右键菜单
openMenu(e, tag) {
this.selectedTab = tab
},
// 关闭 缓存页签 右键菜单
closeMenu() {
this.menuVisible = false
},
}
}
</script>
openMenu
函数的实现:
openMenu(e, tab) {
this.menuConfig = {
left: this.visitedViews[0].name === tab.name,
right: this.visitedViews[this.visitedViews.length - 1].name === tab.name
}
const offsetWidth = this.$el.offsetWidth
// 右键菜单 left 最大值
const maxLeft = offsetWidth - this.menuMinWidth
const left = e.clientX - 200
const top = e.offsetY
this.menuVisible = true
this.menuPos = {
left: `${left > maxLeft ? maxLeft : left}px`,
top: `${top}px`
}
this.selectedTab = tab
}
这个函数做了三个事情:
-
判断菜单功能(是否需要关闭左侧/关闭右侧)
当前触发右键事件的项的
name
等于visitedViews
第一项的name
时,表示触发项为第一项,则不需要“关闭左侧”的功能(左侧没有页面需要关闭);当前触发右键事件的项的
name
等于visitedViews
最后一项的name
时,表示触发项为最后一项,则不需要“关闭右侧”的功能(右侧没有页面需要关闭)。 -
控制菜单位置
这里控制位置的时候需要注意的是,计算过程会受样式的影响(菜单相对于谁来定位会影响计算方式。我这里是相对于
div.tabs-view-container
进行定位的)。offsetWidth
:div.tabs-view-container
的宽度。maxLeft
:设置菜单的left
的最大值。e.clientX
:事件发生时鼠标指针距离客户端左边的水平坐标。menuMinWidth
:菜单DOM的最小宽度(这里默认是100)(也可以理解成是:当菜单处于最右侧时,优化的偏移量)。结合下图,应该还是很容易理解菜单位置的计算逻辑的~
-
打开菜单
将变量
menuVisible
置为true
。
测试一下,现在菜单可以正常打开了。
下面开始完善菜单功能~
重置当前
实现思路是:当前页面 -> 进入中转页面 -> 调用路由的replace方法,从而实现页面刷新。
新建中转页面:Transfer.vue
:
<script>
export default {
created() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({
path: '/' + path,
query
})
},
render() {
return (<div></div>)
}
}
</script>
添加路由:
{
path: '/transfer/:path(.*)',
name: 'transfer',
component: Transfer
}
修改TabsView
组件:
- 添加重置页面函数
reloadPage
。 - 修改
toUpdateView
,将之前的临时跳转方案替换成调用reloadPage
。 - 修改
refreshSelectedTab
:先删除缓存项,再调用reloadPage
。
<script>
methods: {
...mapActions('tagsView', ['addView', 'delView', 'delCachedView']),
// ...
// 重新加载目标页
reloadPage(tab) {
this.$router.replace({
path: `/transfer${tab.fullPath}`,
query: {
...tab.params,
}
})
},
// 更新视图
toUpdateView(tab) {
const latestView = this.visitedViews.slice(-1)[0]
if (latestView) { // 当前还存在访问过的页面
// 删除项的name 和 当前路由的name 一致时,需要切换当前激活的 tab 为最后一项;反之,则不需要进行额外操作。
if (tab.name === this.$route.name) this.$router.push(latestView.fullPath)
} else {
// 当最后一个删除的项是概览页时,需要 "重新加载" 概览页,不能直接跳转
if (tab.name === 'Dashboard') this.reloadPage(tab) // 重新加载
else this.$router.push('/common')
}
},
// 重置当前页
refreshSelectedTab() {
let tab = this.selectedTab
this.delCachedView(tab)
this.reloadPage(tab)
},
// ...
}
</script>
关闭当前
直接调用 基础实现 -> 删除缓存页方法(closeSelectedTab
)。
关闭其他
分析两种情况:
- 触发项是当前页面:直接关闭其他页面。
- 触发项不是当前页面:关闭其他页面后,需要跳转到触发项对应的页面。
在 mutations
对象中添加两个函数:
DEL_OTHERS_VISITED_VIEWS
:过滤触发项的路由信息。DEL_OTHERS_CACHED_VIEWS
:过滤触发项的name
。
const mutations = {
// ...
// 删除其他
DEL_OTHERS_VISITED_VIEWS(state, view) {
state.visitedViews = state.visitedViews.filter(v => v.name === view.name)
},
DEL_OTHERS_CACHED_VIEWS(state, view) {
state.cachedViews = state.cachedViews.filter(name => name === view.name)
},
}
在actions
对象中添加三个函数:
delOthersViews
:分发delOthersVisitedViews
和delOthersCachedViews
。delOthersVisitedViews
:提交DEL_OTHERS_VISITED_VIEWS
。delOthersCachedViews
:提交DEL_OTHERS_CACHED_VIEWS
。
const actions = {
// ...
// 删除其他
delOthersViews({ dispatch }, view) {
dispatch('delOthersVisitedViews', view)
dispatch('delOthersCachedViews', view)
},
delOthersVisitedViews({ commit }, view) {
commit('DEL_OTHERS_VISITED_VIEWS', view)
},
delOthersCachedViews({ commit }, view) {
commit('DEL_OTHERS_CACHED_VIEWS', view)
},
}
修改TabsView
的closeOthersTags
方法:
<script>
methods: {
...mapActions('cache', ['addView', 'delView', 'delCachedView', 'delOthersViews']),
// ...
// 关闭其他 缓存页
closeOthersTabs() {
let tab = this.selectedTab
this.delOthersViews(tab)
if (this.$route.name !== tab.name) this.$router.push(tab.fullPath)
},
}
</script>
按照上面的两种情况进行测试,没问题咱们就继续(每一步的测试还是挺重要的,确保每个小功能没问题,有问题也能及时定位)~
关闭全部
“关闭全部”功能还是很简单的,直接清空visitedViews
和cachedViews
,然后重新加载概览页面。
在 mutations
对象中添加两个函数:
DEL_ALL_VISITED_VIEWS
:清空已访问页面。DEL_ALL_CACHED_VIEWS
:清空已缓存页面的name
。
const mutations = {
// ...
// 删除所有
DEL_ALL_VISITED_VIEWS(state) {
state.visitedViews = []
},
DEL_ALL_CACHED_VIEWS(state) {
state.cachedViews = []
},
}
在actions
对象中添加三个函数:
delAllViews
:分发delAllVisitedViews
和delAllCachedViews
。delAllVisitedViews
:提交DEL_ALL_VISITED_VIEWS
。delAllCachedViews
:提交DEL_ALL_CACHED_VIEWS
。
const actions = {
// ...
// 删除所有
delAllViews({ dispatch }, view) {
dispatch('delAllVisitedViews', view)
dispatch('delAllCachedViews', view)
},
delAllVisitedViews({ commit }) {
commit('DEL_ALL_VISITED_VIEWS')
},
delAllCachedViews({ commit }) {
commit('DEL_ALL_CACHED_VIEWS')
},
}
修改TabsView
的closeAllTabs
:
<script>
methods: {
...mapActions('tagsView', ['addView', 'delView', 'delCachedView', 'delOthersViews', 'delAllViews']),
// ...
// 关闭所有 缓存页
closeAllTags() {
this.delAllViews()
this.reloadPage({ fullPath: '/common' })
},
}
</script>
继续测试~
一切正常~
关闭左侧/关闭右侧
分析:
关闭左侧/关闭右侧,最关键的就是拿到当前项的下标(index
),然后处理数组。
- 关闭左侧:通过数组
slice
方法,截取从index
到数组末尾的元素,然后重新赋值。 - 关闭右侧:通过数组
splice
方法,删除从index
到数组末尾的元素。
在 mutations
对象中添加两个函数:
DEL_DIR_VISITED_VIEWS
:删除触发项左侧或右侧的已访问页面。DEL_DIR_CACHED_VIEWS
:删除触发项左侧或右侧的已缓存页面的name
。
const mutations = {
// ...
// 删除 左侧/右侧
DEL_DIR_VISITED_VIEWS(state, { view, dir }) {
let visitedViews = [...state.visitedViews]
for (const [i, v] of visitedViews.entries()) {
if (v.name === view.name) {
if (dir === 'right') {
let len = visitedViews.length - (i + 1) // 右边还有 len 个项
visitedViews.splice(i + 1, len)
} else visitedViews = visitedViews.slice(i)
break
}
}
state.visitedViews = visitedViews
},
DEL_DIR_CACHED_VIEWS(state, { view, dir }) {
const index = state.cachedViews.indexOf(view.name)
let cachedViews = [...state.cachedViews]
if (index > -1) {
if (dir === 'right') {
let len = cachedViews.length - (index + 1) // 右边还有 len 个项
cachedViews.splice(index + 1, len)
} else cachedViews = cachedViews.slice(index)
}
state.cachedViews = cachedViews
},
}
在actions
对象中添加三个函数:
delDirViews
:分发delDirVisitedViews
和delDirCachedViews
。delDirVisitedViews
:提交DEL_DIR_VISITED_VIEWS
。delDirCachedViews
:提交DEL_DIR_CACHED_VIEWS
。
const actions = {
// ...
// 删除 左侧/右侧
delDirViews({ dispatch }, { view, dir }) {
dispatch('delDirVisitedViews', { view, dir })
dispatch('delDirCachedViews', { view, dir })
},
delDirVisitedViews({ commit }, { view, dir }) {
commit('DEL_DIR_VISITED_VIEWS', { view, dir })
},
delDirCachedViews({ commit }, { view, dir }) {
commit('DEL_DIR_CACHED_VIEWS', { view, dir })
},
}
修改TabsView
的closeDirTabs
方法:
-
判断是否有可删除的“左侧/右侧”,有则执行删除函数,反之则直接
return
。 -
判断当前路由对应的页面是否包含在删除项内(执行循环,拿当前路由的
name
进行匹配),如果包含,则跳转到最后一项对应的页面。
<script>
methods: {
...mapActions('tagsView', ['addView', 'delView', 'delCachedView', 'delOthersViews', 'delAllViews', 'delDirViews']),
// ...
// 删除 左侧/右侧 缓存页
closeDirTabs(dir) {
if (this.menuConfig[dir]) return
let tab = this.selectedTab
this.delDirViews({
view: tab,
dir: dir
})
let needUpdateVisited = true
for (const [, v] of this.visitedViews.entries()) {
if (this.$route.name === v.name) {
needUpdateVisited = false
break
}
}
if (needUpdateVisited) this.$router.push(this.visitedViews.slice(-1)[0].fullPath)
},
}
</script>
测试~
“小”功告成~
至此整个缓存组件的既定功能已经完成了,但是还有两个需要优化的地方:
- 页面切换动画:现在的页面切换太过生硬,需要加上过渡动画。
- 标签栏滚动:当打开的页面较多时,可能会出现换行,所以我们需要在外层添加一个可滚动的“盒子”,防止样式错乱。
页面过渡动画
还得记得之前一开始的AppMain
组件吗,我们稍微对它进行一些修改:
添加transition
标签,并指定name
为fade
,过渡模式mode
为out-in
(当前元素先进行过渡,完成之后新元素过渡进入)。
<template>
<section class="app-main">
<div class="app-main-content">
<transition name="fade" mode="out-in">
<keep-alive :include="cachedViews">
<router-view :key="key"/>
</keep-alive>
</transition>
</div>
</section>
</template>
新建样式文件:transition.scss
,并添加过渡样式:
/* global transition css */
/* fade */
.fade-leave-active,
.fade-enter-active {
transition: all .3s;
}
.fade-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-leave-to {
opacity: 0;
transform: translateX(30px);
}
在入口文件main.js
中引入transition.scss
:
import '@/style/transition'
实现滚动
为什么要单独开一个菜单写滚动呢?
是因为我今天无意中看到了el-scrollbar
这个组件(由element-ui
提供),虽然文档里面没有相关的说明,但是可以直接使用,所以我就在这里进行了一下测试(主要是对滚动条的样式做了统一处理)~
修改TabsView
:
<template>
<div class="tabs-view-container">
<el-scrollbar class="tabs-view-scroll">
<div class="tabs-view-wrapper">
<router-link
v-for="tab in visitedViews"
:key="tab.name"
:class="`tabs-view-item ${isActive(tab) ? 'active' : ''}`"
ref="tab"
:to="tab"
tag="span"
@contextmenu.prevent.native="openMenu($event, tab)"
>
{{tab.title}}
<i class="el-icon-close" @click.prevent.stop="closeSelectedTab(tab)" />
</router-link>
</div>
</el-scrollbar>
<!-- ... -->
</div>
</template>
效果图:
效果还是棒棒哒💯(感谢element
团队)!
“大”功告成!
总结
本文的技术栈主要涉及了vue
、vuex
、vue-router
、keep-alive
、transition
、element-ui
,在组件的实现过程中,也算是回顾了它们的一些基础知识和用法。当然,有时候会有更复杂的业务场景,但是我觉得大部分都可以在路由里面配置不同的参数,然后进行相应的处理(emmm...目前我还没遇到很特别的情况,有的话欢迎一起讨论~)。
另外再聊聊为什么会想到写这个组件。
最近在改一个重构的项目,看了之前同事封装的一些组件,有所收获。所以结合自己的想法把一部分组件进行了重写,同时计划 把思考的过程记录下来。不足之处还请指教~
最后,祝看官大大们周末愉快,顺便给个👍(赞)吧~