非常感谢XGHeaven和jeff_one两位老哥,这个效果应该是页面堆栈,主要模拟原生APP应用导航,然后我的实现的只是这个效果的一部分功能,完整的功能还应该有“同一页面可以有多个实例,拥有不同的状态”,感兴趣的朋友可以去看看vue-navigation、vue-page-stack、vue-nav
场景
首先这个交互我也叫不出专业的名字,当时我的大致场景是这样的:
- 现在有一个小商城,从首页(Index)可以进入商品列表页面(List),这个List是一个无限列表,现在用户往下翻,看到某一个商品似乎比较喜欢,于是点击进入到商品详情页面(Info),看完后觉得比较满意。恩,但是货比三家嘛,于是用户返回商品列表页面,准备继续往下浏览,这时如果你将用户之前的列表刷新了会怎么样?要是我是非常不爽的,这意味着我需要重新操作一波,定位到刚刚中意的商品后,继续往下货比三家。
SomePage -> List: List重新加载
List -> Info -> List: List使用缓存
- 当用户确定好商品后,决定购买了,于是进入订单页(Form),订单页默认为用户输入了历史收货地址,用户似乎有想要提醒发货的商家,于是填写了备注,然后检查下订单有误否,发现地址可以再修改一下,于是点击收货地址进入到收货地址列表页(Address),选好后我们自动为用户返回到订单页并更新收货地址,如果此时备注没有了会怎么样?
SomePage -> Form: Form重新加载
Form -> Address -> Form: Form使用缓存
以上是简单举个例子,实际上这类似我在上家公司做移动端(SPA)时碰到的问题,只不过我们是商家列表,但交互类似,刚开始我是直接用路由方式解决的,类似将Info作为List的子页面,将Address作为Form的子页面。实现是实现了,但太麻烦了,维护麻烦啊😂,并且对组内其它伙伴不太友好。直到某一次我决定对其进行重构,重构前也没有什么好思路,然后Google了一番茅塞顿开:利用keep-alive的include实现,当时知道了思路,我就没往下看了,自己动手丰衣足食😁。
统一一下词汇,下文中说的类列表页
就是List,类详情页
就是Info
踩坑记
刚开始我还踩了一个坑,主要是Vue的keep-alive当时很少接触,都是Copy……
首先按照之前使用(copy)keep-alive
的套路是像这样的:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"/>
然后我将它改造成这样
<keep-alive>
<router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<keep-alive :include="include">
<router-view v-if="!$route.meta.keepAlive"/>
</keep-alive>
就是控制这个include
,具体的做法是,先在类列表页的路由选项meta
中增加一个cacheTo
字段,表示对指定页面缓存,值为一个数组,然后:
router.beforeEach((to, from, next) => {
if (isPageLikeList(from)) {
// 如果from是类列表页面
const fromCacheTo = from.meta.cacheTo
if (fromCacheTo.indexOf(to.name) > -1) {
// 如果to是类详情页面
// 将from对应的组件设置缓存
} else {
// 移除from缓存
}
}
// ...
})
但想的很美,写完发现Bug比较严重,类列表页缓存老失效,为什么呢?这就要从keep-alive的include机制说起了,于是看了一下keep-alive的源码,源码中有这么一段
// ...
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
// ...
然后继续往下找侦听include
的回调逻辑
function pruneCache (keepAliveInstance: any, filter: Function) {
// 对于include,filter逻辑为:include包含组件名时返回true,否则返回false
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
// 若组件不在include范围中
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
// 销毁组件
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
// 清除组件的缓存
cache[key] = null
remove(keys, key)
}
大致流程是,include
变化后(不管是新增了还是减少了),如果组件不在include
中并且当前的cache[组件name]
是有缓存的,就执行pruneCacheEntry,销毁组件并清除组件的缓存。
那么这对我上述的逻辑有什么影响呢?按照我之前的流程,动态控制include
,这样做删除组件缓存是没有问题,但是新增就会有问题了,因为新增操作发生在从类列表页离开进入到类详情页之前,此时类列表页已经存在了,然后源码中侦听include
的流程中也只有清除缓存的操作。
知道了问题,如何解决就很清晰了
实现
分析
我们可以将对include
的操作放到类列表页进入前(router.beforeEach),这样就能有效缓存类列表页了,其实这东西就是这么简单,先不管三七二十一,只要是类列表页,进入之前都将其缓存,离开时判断新路由是否是该类列表页指定的类详情页,如果不是就清除缓存。那么在实现之前,咱们先将场景再细化一下,之前简直比玩具还玩具……
SomePage -> List: List重新加载
// 第一种
List -> Info -> List: List使用缓存
// 第二种
List -> Info -> SomePage -> List: List重新加载
// 第三种,不限层级
List -> Info -> OtherList -> OtherInfo -> ... -> List: List、Info、OtherList...使用缓存
其中第三种,从OtherInfo返回OtherList时,OtherInfo应该是要被清除缓存的,依次类推。也就是说,返回时不保留之前的缓存
现在,我们根据例举的场景将大致逻辑确定一下。这里有一个点是很明确的,那就是: 我们只需要关心路由切换时的to
和from
,从这2个路由对象着手去解决。那么这里可以例举出4中情况:
to
和from
都不是类列表页to
和from
都是类列表页to
是类列表页from
是类列表页
现在根据这4种情况细化一下
- 第一种情况,
to
和from
都不是类列表页- 不需要缓存,并且可以将之前的缓存全部清除
- 第二种情况,
to
和from
都是类列表页- 若
to
不在from
的配置中,清空缓存,同时要新增to
缓存; - 否则,保留
from
的缓存,新增to
缓存
- 若
- 第三种情况,
to
是类列表页- 若
from
不在to
的配置中,清空缓存,新增to
缓存 - 否则,无需任何处理
- 若
- 第四种
- 若
to
不在from
的配置中,清空缓存
- 若
如何判断是否是类列表页?
[ { path: '/list', name: 'list', meta: { cacheTo: ['info']
}
// ...
},
{
path: '/info',
name: 'info',
// ...
}
]
如上,在路由中维护一个字段如cacheTo
,如果配置了组件名,就认为是类列表页面
具体实现
逻辑理清楚咯,接下来具体实现,我们将它做得通用一点(可拔插),并且尽量保证对原项目有较小的侵入性,我将它命名为VKeepAliveChain
首先,如果像踩坑记那样维护include
,不太具备可拔插的特性,我还得去搞一个store
,那么这个include
可以使用Vue.observable
处理
// VKeepAliveChain.js
const state = Vue.observable({
caches: []
})
const clearCache = () => {
if (state.caches.length > 0) {
state.caches = []
}
}
const addCache = name => state.caches.push(name)
为了避免像踩坑记那样直接使用<keep-alive :include="include">
,我们实现一个函数式组件来解决
// VKeepAliveChain.js
export const VKeepAliveChain = {
install (Vue, options = { key: 'cacheTo' }) {
const { key } = options
// 支持一下自定义key
if (key) {
cacheKey = key
}
// 直接透传children,所以会像keep-alive一样,只拿第一个组件节点
const component = {
name: 'VKeepAliveChain',
functional: true,
render (h, { children }) {
return h(
'keep-alive',
{ props: { include: state.caches } },
children
)
}
}
Vue.component('VKeepAliveChain', component)
}
}
现在我们来实现缓存控制的主要逻辑,由于要利用router.beforeEach
,约定了尽量小的侵入性,这里可以merge一下。
这个
mergeBeforeEachHook
方法有同学有疑问,我当时只是为了让后续维护的人知道这里有一个钩子(实际上后面也没关心过),vue-router的beforeEach
钩子是可以重复注册的,按照注册顺序执行,这里你可以在Vue.use时将router传人,然后在模块内直接注册一个beforeEach
钩子
// VKeepAliveChain.js
const defaultHook = (to, from, next) => next()
export const mergeBeforeEachHook = (hook = defaultHook) => {
return (to, from, next) => {
// 缓存控制逻辑
// 1. 都不是类列表页
// 清空缓存
// 2. 都是类列表页
// 若`to`不在`from`的配置中,清空缓存,同时要新增`to`缓存
// 保留`from`的缓存,新增`to`缓存
// 3. 新路由是类列表页
// 若`from`不在`to`的配置中,清空缓存,新增`to`缓存
// 否则,无需处理
// 4. 旧路由是类列表页
// 若`to`不在`from`的配置中,清空缓存
const toName = to.name
const toCacheTo = (to.meta || {})[cacheKey]
const isToPageLikeList = toCacheTo && toCacheTo.length > 0
const fromName = from.name
const fromCacheTo = (from.meta || {})[cacheKey]
const isFromPageLikeList = fromCacheTo && fromCacheTo.length > 0
if (!isToPageLikeList && !isFromPageLikeList) {
clearCache()
} else if (isToPageLikeList && isFromPageLikeList) {
if (fromCacheTo.indexOf(toName) === -1) {
clearCache()
}
addCache(toName)
} else if (isToPageLikeList) {
if (toCacheTo.indexOf(fromName) === -1) {
clearCache()
addCache(toName)
}
} else if (isFromPageLikeList) {
if (fromCacheTo.indexOf(toName) === -1) {
clearCache()
}
}
return hook(to, from, next)
}
}
那么整个功能就实现了,同时我将它发不到了npm v-keep-alive-chain上。
食用方式
首先引入并注册它
// main.js
import { mergeBeforeEachHook, VKeepAliveChain } from 'v-keep-alive-chain'
Vue.use(VKeepAliveChain, {
key: 'cacheTo' // 可选的 默认为cacheTo
})
// 如果你没有注册过beforeEach
router.beforeEach(mergeBeforeEachHook())
// 如果有注册beforeEach
router.beforeEach(mergeBeforeEachHook((to, from, next) => {
next()
}))
然后在App.vue(视你的情况而定)中
<keep-alive>
<router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<VKeepAliveChain>
<router-view v-if="!$route.meta.keepAlive"/>
</VKeepAliveChain>
接着在router中配置你的需求
[ { path: '/list', name: 'list', meta: { cacheTo: ['info']
}
// ...
},
{
path: '/info',
name: 'info',
// ...
}
]
然后就能愉快的玩耍了
注意事项
- 路由配置不能少了
name
属性,并且这个name
需要和组件name
一样 cacheTo
优先级小于keepAlive
,所以,处理这种需求的页面不要设置keepAlive
- 可以设置2个页面之前仅在相互切换时缓存,不过我还没发现可用的场景
- webpack、vue-cli一般我是拿来即用的,打包的脚手架是vue-cli4.0,很少深入研究这些东西,然后我看了下发布到npm包的源码,发现有很多无用的polyfill被打进去了,导致Gzip的包都有4Kb多,暂时还没有找到解决方法,知晓的朋友麻烦告知一下啊😂
文章写的比较快,如有什么错误,可以下方留言咯
朋友,看到这里,希望文章对你有启发,本人非常欢迎技术交流,如果你觉得文章对你有用,还请给老弟一个👍,平时我是不在乎这些个的,但由于我马上要投递简历了,需要点东西撑门面,没得办法,履历太差了,谢谢你,愿生活带给你美好!!!
今天的文章Vue前进刷新后退不刷新,简易页面堆栈实现分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20014.html