有时候我们会碰到这么一个场景:我们可能希望点击页面上的某个按钮,然后就会滚动到页面的某一块上,也就是所谓的“滚动到锚点”的功能。这个其实很好实现,vue-router的官方文档里已经给出了写法:
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}
});
但是这么写存在一个问题。比如我在页面顶部有一个链接,点击之后就会跳转到底部(demo在这里):
<router-link to="#bottom">jump to bottom</router-link>
<!--...-->
<h1 id="bottom">This is bottom!</h1>
这应该是很常见的实现方式了。
但是这么实现有个问题,如果我先点击了这个链接跳转到了底部(此时URL为/#bottom
),然后我再手动从底部把页面滚动到顶部,此时再次点击链接,就不会跳转了。为什么?因为URL已经是/#bottom
了,当然不会再次跳转。
所以,我们就会有这么一个想法:能不能让目标组件离开可视范围之后就自动修改URL呢?这样我们就能反复跳转了。比如,当这个bottom不可见的时候,我们就自动把URL修改成/
,这样我们再次点击链接的时候,就能再次跳转。但是,摆在我们面前的是这样几个问题:
- 如何修改URL而不造成页面刷新、滚动?
- 如何尽可能让URL美观?
- 如何判断组件是否可见?
让URL改变而不刷新页面的API早就有了,就是H5的history API,而且兼容性也已经很好了。因为vue-router的history模式是基于history API的,所以,我们最直接的想法就是这么写:
this.$router.replace('/')
但是这样会报重复路由Avoided redundant navigation to current location: "/"
的错,而且可能会导致页面回到顶部,并不是我们想要的方案。
那么,直接用history API呢?比如,我们也许可以这么写:
history.replaceState(
null,
document.title,
location.pathname + location.search
);
但是这样并不会起到作用,因为vue-router内部的hash值并没有变化,我们只是修改了URL而已,仍然无法完成跳转。
我们可能还会想到直接修改URL的hash部分:
location.hash = '';
但是这样第一个是不美观(会让URL变成/#
),第二个是在相当多的浏览器上会直接跳转到页面顶部,因为/#
代表的就是页面的顶部。
我们可能还会想到直接修改location.href
,但这就会触发页面刷新,显然是不行的。
所以,综合一下,其实我们可以采用这样的办法:
location.hash = '_';
history.replaceState(
null,
document.title,
location.pathname + location.search
);
不用$router
的理由已经说过了,不再赘述。这一段是什么意思呢,第一个是先清除现有的锚点,设置到另一个不存在的锚点上,这样就不会触发页面滚动,同时也能确保正常跳转;然后再使用history API去修改URL,达到美观的目的。
完成了核心逻辑之后,下面就是如何判断组件的可见性。传统的方法想必大家都会,监听scroll事件,然后判断目标组件和顶部的距离;不过在现代浏览器里,有个新的API IntersectionObserver
,就是为了解决可见性的问题。具体的写法这篇文章里有提到,我就不再多说了。