浏览器对用户访问网页的记录
- 在聊如何管理
vue
组件滚动行为之前,先简单说说(毕竟深入了我也很模糊o(╯□╰)o)浏览器是如何对用户访问过的页面的保持,浏览器历史记录是对用户所访问的页面按时间顺序进行的记录和保存,以上是MDN对浏览器就如何跟踪用户访问过网页的解释性说明。 - 通常我们很少会对页面回退或前进进行操作,在浏览器用户界面上提供有前进、回退按钮,页面跳转到离开页面之前的位置,而不是重新刷新页面,这个功能是由浏览器引擎(与渲染引擎、解析引擎概念不同)来完成的。当用户进入一个页面的时候,会往 history 栈中放入当前的记录,对页面级别的操作通过操作内置对象
history
可以满足一些需求。
vue对访问记录的管理
- 进入正题,
vue
路由跳转就是通过对history.pushState()
和history.replaceState()
方法的模拟来实现,会往history
栈中存放一条记录,这也是为什么vue
的router.push
方法只能在支持history.pushState()
方法的浏览器中使用,当调用router.go()
或者router.back()
方法的时候就和history.go()
、history.back()
效果一样,都是对history
栈中的记录进行访问,上述行为与通过浏览器的回退和前进效果也是一样。
但是,在不加处理的情况下,组件的滚动行为会跟我们想象的不同。
vue组件滚动行为
- 设置三个路由
/home
、/list
、/about
,即对应三个不同的组件,
<ul class="tab">
<li>
<router-link to="/" >首页</router-link>
</li>
<li>
<router-link to="/list" >列表</router-link>
</li>
<li>
<router-link to="/about" >关于</router-link>
</li>
<li>
<a href="#" @click='() => { this.$router.back() }'>点击回退</a>
</li>
</ul>
<router-view></router-view>
- 每个组件的结构都是
ul>li
的结构
<!-- 以 home 组件为例 -->
<ul class="list_content home">
<li v-for='i in 10'>{{ i }}</li>
</ul>
长这样:
请注意,现在开始滚动首页位置至第 5 屏的位置,当切换到列表以及关于页面的时候,会发现这两个页面的滚动行为和首页滚动行为一致。
既不涉及组件的缓存,也不涉及组件的复用,我们不禁会疑惑为什么首页的滚动会影响到其他两个页面,如果我们有当切换组件的时候,需要让当前组件的内容是从
scrollTop = 0
的时候开始浏览,那这样的结果将会是一个绊脚石。原因如下,因为基于SPA模式开发,所以页面仅有一个,实现页面切换是利用哈希与组件的映射关系,
vue-router
是通过哈希来模拟完整的 url
,但是对于页面来说仍是一个 url
,所以在任何一个组件滚动页面,切换到其他组件的时候,页面仍保持滚动之前的状态,这就是出现上述现象的原因。
如何管理组件的滚动行为
- 如果你是想简单粗暴的在每次切换组件的时候让页面回到顶部,
router.beforeEach()
导航守卫会是一个不错的选择:
router.beforeEach((to, from, next) => {
// 让页面回到顶部
document.documentElement.scrollTop = 0
// 一定不要忘记调用 next()
next()
})
但这不是我们的主题,要借助 vue-router
提供的 scrollBehavior
,来管理组件滚动行为。
- 关于
scrollBehavior
,这里贴出官网对概念的介绍 传送门,当然,借助scrollBehavior
,你也能让页面在组件切换的时候回到顶部:
const scrollBehavior = function (to, from, savedPosition) {
// savedPosition 会在你使用浏览器前进或后退按钮时候生效
// 这个跟你使用 router.go() 或 router.back() 效果一致
// 这也是为什么我在 tab 栏结构中放入了一个 点击回退 的按钮
if (savedPosition) {
return savedPosition
} else {
// 如果不是通过上述行为切换组件,就会让页面回到顶部
return {x: 0, y: 0}
}
}
- 上述会定制所有组件的滚动行为,但有时候我们希望,当用户在浏览
home
页面到底部的时候,跳转到list
页面浏览,当浏览到中间的时候,跳转到about
页面浏览,当用户每次回退的时候,都希望保持离开之前页面的状态,即:从about
回退到list
页面的时候,页面仍是在中间,回退到home
页面的时候,仍是在底部,这就需要我们个性化定制。
定制不同组件的scrollBehavior
这里用到路由的元信息 meta
更细颗粒度控制滚动行为,这里以 home
组件为例说明:
const Home = {
template: `
<ul class="list_content home">
<li v-for='i in 10'>{{ i }}</li>
</ul>
`,
data () {
return {
timerId: ''
}
},
mounted () {
// 通过 addEventListener 方法注册事件的时候需要格外小心
// 如果在 destroyed 钩子函数中没有销毁 scroll 事件
// 在激活 home 组件的时候会再次绑定 scroll 事件
// window.addEventListener('scroll', this.justifyPos)
// 通过 on 方式绑定事件能够有效避免上述情况
window.onscroll = this.justifyPos
},
methods: {
justifyPos () {
// 节流;
if (this.timerId) clearTimeout(this.timerId)
this.timerId = setTimeout(() => {
// 获取页面滚动距离之后设置给当前路由的 元信息
this.$route.meta.y = window.pageYOffset
}, 300)
}
},
destroyed () {
// 当组件销毁的时候,移除滚动行为监听, 清空定时器;
// 该方法是绑定到 window 身上,即使跳转到其他组件,仍然会监听页面的滚动行为
// window.removeEventListener('scroll', this.justifyPos)
// clearTimeout(this.timerId)
}
}
const List = {
template: `
<ul class="list_content list">
<li v-for='i in 10'>{{ i }}</li>
</ul>
`
}
const About = {
template: `
<ul class="list_content about">
<li v-for='i in 10'>{{ i }}</li>
</ul>
`
}
const routes = [
// 设置 meta,细颗粒控制组件滚动
{path: '/', component: Home, meta: {x: 0, y: 0}},
{path: '/list', component: List, meta: {x: 0, y: 0}},
{path: '/about', component: About, meta: {x: 0, y: 0}}
]
const scrollBehavior = function (to, from, savedPosition) {
return to.meta
}
const router = new VueRouter({
routes,
scrollBehavior,
linkExactActiveClass: 'current'
})
上述会在 home
组件滚动停止的时候记录当前组件的滚动位置信息,并且存储到对应 home
组件的路由 meta
这个对象中,当切换到 list
或者 about
页面之后在回到 home
组件,仍会保留着离开之前的位置,而不是简单地让页面回到顶部。
但是,你会发现你只是针对 home
组件的滚动行为进行控制,list
和 about
组件的滚动行为也能够实现个性化定制,即也会将当前组件的滚动行为记录在对应的路由 meta
中。
这会让人疑惑,因为在 list
和 about
组件中并没有设置 justifyPos
方法,并且 window.onscroll = this.justifyPos
将 this
绑定到当前的上下文中。
vue
官网对于组件销毁介绍,会解绑所有的指令以及事件监听,但是对于方法的引用处理没有提到,个人觉得在这里应该抛出警告或者错误的,但是 vue
却没有提示,这也是令我困惑的一点。但是,这却为滚动行为监听提供了更好的处理方法,那就是绑定到 vue
根实例上,而不是某一个单一组件上,因为 this
会自动绑定到当前上下文:
new Vue({
router,
data: {
timerId: ''
},
mounted () {
window.addEventListener('scroll', this.justifyPos)
},
methods: {
justifyPos () {
if (this.timerId) clearTimeout(this.timerId)
this.timerId = setTimeout(() => {
this.$route.meta.y = window.pageYOffset
}, 300)
}
}
}).$mount('#app')
当better-scroll(以下简称bs)遇上vue,如何定制滚动行为
- 贴上传送门 better-scroll,感兴趣的可以看一下。
- 之所以会谈到
bs
,如果在项目中用到该插件,那么页面滚动行为跟组件滚动行为是完全不一样的,这是因为bs
特殊的结构要求,父容器需要有个固定的高度,所有的滚动行为是由子元素来产生的,在移动端应用bs
,通常会将父容器的高度设置为屏幕的高度,你的所有应用都应该放到这个父容器内。bs
在移动端性能很出色,但是这却为组件个性化定制scrollBehavior
带来了一些小麻烦。 - 原因就是应用
bs
插件的组件,一般会设置高度和屏幕高度一致,这样即使通过meta
来设置滚动记录,在vue-router
的scrollBehavior
中返回meta
也没有用处,因为高度是定死了,就不存在滚动,你所看到的滚动式是bs
插件所处理的。 - 这时候,就需要用到
bs
提供的一些事件和方法了,仍以 home 组件为例说明,看代码:
const Home = {
template: `
<div class="wrapper" ref="wrapper">
<ul class="list_content home">
<li v-for='i in 10' @click='goList'>{{ i }}</li>
</ul>
</div>
`,
mounted () {
this.$nextTick(() => {
// 初始化 BS
this._initScroll()
// 滚动监听
this.scroll.on('scrollEnd', (pos) => {
// 将滚动信息设置给当前路由元信息
this.$route.meta.y = pos.y
})
// 当前组件激活的时候,滚动到离开前位置
// 如果你想要滚动动画效果,可以在 scrollTo 方法中自定义
this.scroll.scrollTo(0, this.$route.meta.y, 0)
})
},
methods: {
_initScroll () {
if (!this.$refs.wrapper) return
this.scroll = new BScroll(this.$refs.wrapper, {
mouseWheel: {
speed: 20,
invert: false,
easeTime: 300
},
// 派发 click 事件;
click: true
})
},
// 跳转到列表页;
goList () {
this.$router.push({name: 'list'})
}
}
}
const List = {
template: `
<ul class="list_content list">
<li v-for='i in 10' @click='goHome'>{{ i }}</li>
</ul>
`,
methods: {
// 回跳到首页
goHome () {
this.$router.push({name: 'home'})
}
}
}
const routes = [
// 设置 meta
{path: '/', name: 'home', component: Home, meta: {x: 0, y: 0}},
{path: '/list', name: 'list', component: List, meta: {x: 0, y: 0}},
{path: '/about', component: About, meta: {x: 0, y: 0}}
]
// scrollBehavior 其实这里已经没有什么作用了,因为当前组件的高度被定死和整个屏幕一样高
// const scrollBehavior = function (to, from, savedPosition) {
// return to.meta
// }
// 设置路由
const router = new VueRouter({
routes,
scrollBehavior,
linkExactActiveClass: 'current'
})
// 挂载
new Vue({router}).$mount('#app')
- 通过
bs
提供的事件以及方法再结合路由的meta
,也能够实现细颗粒度控制滚动,如果对组件使用了keep-alive
,你应该在每次切换到该组件的时候在activated
钩子函数中初始化bs
、scrollEnd
事件以及scrollTo
方法;如果你页面有分页的功能,你可能需要在分页边界花费一些心思如何让滚动行为跨越分页,这里建议是使用组件缓存,关于组件如何清除缓存,可以参考我的另一篇文章 组件去缓存,当然如果你有更好的处理方式,也可以留言。