写一个锚点-固钉Anchor
Ant Design 中有一个“固钉”组件,使用它能够做一个锚点滚动导航。当点击文字时页面滚动到相应位置,当滚动页面时导航高亮跟随,当复制链接在新窗口访问时能滚动到相应地方。
<a-anchor-link href="#components-anchor-demo-basic" title="Basic demo" />
<a-anchor-link href="#components-anchor-demo-static" title="Static demo" />
锚点
实现锚点方式有多种,可以使用a标签href
或者name
,可以使用id
。使用a标签锚点,目标内容会直接滚动到页面顶部贴着浏览器,考虑到不同的使用场景可能需要距离浏览器顶部一些距离,所以这里选择手动计算。
丝滑滚动
可以注意到ant的锚点导航点击滚动时很丝滑,不像普通写a标签锚点那样直接“啪”就跳到目标内容。我们可以给容器加CSS动画,但是也有直接处理滚动动画的样式属性:scroll-behavior
。在需要滚动的容器加上scroll-behavior: smooth
,查看效果————真的有那么丝滑~
smooth值:滚动框通过一个用户代理预定义的时长、使用预定义的时间函数,来实现平稳的滚动。scroll-behavior
除了css设置平滑滚动,js方法也可以设置。比如我们要平滑精确地滚动到某个坐标:scrollTo:
element.scrollTo({
behavior: 'smooth'
})
比如我们要使某个元素平滑滚动到可视区域:scrollIntoView:
element.scrollIntoView({behavior: "smooth", block: "center"})
position
我们选择锚点导航一般是页面内容很多需要快速定位到目标内容的时候,所以导航需要固定在页面的某个区域方便用户点击,而不是让导航跟着页面滚动,如果是这样做锚点就无意义了。
使元素固定在视窗不随页面滚动,我们可以使用position: fixed;
。fixed是相对viewport 视口固定住不动的属性值。视口概念。
从始至终保持在距顶80距左10的位置:
XXX {
position: fixed;
top: 80px;
left: 10px;
}
仔细感受ant的固钉,是滚动到距顶部的某个距离才固定住,而不是一开始就固定死在一个地方。如果继续使用fixed
的话就需要使用js计算滚动到距顶部多少距离的时候就给html元素加上fixed样式。
但是position
还有一个值在很多文档资料中没有提到:sticky
粘性定位。粘性定位可以被认为是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。它可以实现元素随内容一起滚动后在距离视口某段距离或小于距视口某段距离时固定住。
元素先相对定位随内容一起滚动,在距顶部10的时候固定住:
XXX {
position: sticky;
top: 10px;
}
开始写一个my-anchor
新建文件MyAnchor.vue:
<template>
<div class="sticky-box" :style="{ left: `${stickyLeft}px`, top: `${stickyTop}px` }">
<div class="link-container">
<span
v-for="(item, index) in linkArr"
:key="index"
@click="herfTo(item.id, index)"
:class="index === currentIndex ? 'active-link' : 'link'"
>{{ item.name }}
<span class="split"></span>
</span>
</div>
</div>
</template>
<script>
import throttle from 'lodash/throttle'
export default {
name: 'anchor',
props: {
linkArr: {
type: Array,
default: () => []
},
stickyLeft: {
type: String,
default: ''
},
stickyTop: {
type: String,
default: ''
},
// 元素滚动到距顶部多少距离时,锚点激活
offsetTop: {
type: String,
default: ''
}
},
data() {
return {
currentIndex: 0,
scrollTops: {}
}
},
watch: {
currentIndex: {
handler(index) {
if (history.replaceState) {
history.replaceState(
null,
null,
`#${this.linkArr[index]?.name || ''}`
)
} else {
location.hash = '#myhash'
}
}
}
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
init() {
this.linkArr.forEach(i => {
this.scrollTops[i.id] =
document.querySelector('#' + i.id)?.offsetTop - Number(this.offsetTop)
})
document.addEventListener('scroll', this.scrollFu, false)
let linkName = decodeURI(location.hash).split('#')[1]
let minkId = this.linkArr.filter(item => item.name === linkName)[0]?.id
let index = this.linkArr.findIndex(item => item.name === linkName)
this.herfTo(minkId, index)
},
herfTo(id, index) {
if (!id) {
return
}
document.removeEventListener('scroll', this.scrollFu, false)
this.currentIndex = index
this.$nextTick(() => {
window.scrollTo({
top: document.getElementById(id).offsetTop - Number(this.offsetTop),
behavior: 'smooth'
})
setTimeout(() => {
document.addEventListener('scroll', this.scrollFu, false)
}, 500)
})
},
scrollFu() {
let scrollTop =
document.scrollTop || window.pageYOffset || document.body.scrollTop
this.getScrollTops()
if (scrollTop == 0) {
this.currentIndex = 0
return
}
if (this.isScrollToBottom()) {
this.currentIndex = this.linkArr.length - 1
return
}
let scrollTops = Object.values(this.scrollTops)
scrollTops.forEach((v, i) => {
if (v <= scrollTop) {
this.currentIndex = i
}
})
},
isScrollToBottom() {
let marginBottom = 0
if (document.documentElement.scrollTop) {
marginBottom =
document.documentElement.scrollHeight -
(document.documentElement.scrollTop + document.body.scrollTop) -
document.documentElement.clientHeight
} else {
marginBottom =
document.body.scrollHeight -
document.body.scrollTop -
document.body.clientHeight
}
if (marginBottom <= 0) {
return true
} else {
return false
}
},
getScrollTops: throttle(function() {
this.linkArr.forEach(i => {
this.scrollTops[i.id] =
document.querySelector('#' + i.id)?.offsetTop - Number(this.offsetTop)
})
}, 800)
}
}
</script>
<style lang="scss" scoped>
.sticky-box {
z-index: 999;
position: sticky;
width: 200px;
left: 100px;
}
.link-container {
width: 200px;
display: flex;
text-align: left;
flex-direction: column;
line-height: 30px;
border-left: 1px solid #e0e0e0;
.link {
cursor: pointer;
padding-left: 10px;
border-left: 1px solid transparent;
}
.active-link {
position: relative;
padding-left: 11px;
color: #0672ff;
.split {
position: absolute;
height: 100%;
width: 2px;
left: -1px;
background-color: #0672ff;
}
}
}
</style>
使用:
<MyAnchor
:linkArr="[
{
id: 'my-anchor-1',
name: '1'
},
{
id: 'my-anchor-2',
name: '2'
},
{
id: 'my-anchor-3',
name: '3'
}
]"
stickyLeft="100"
stickyTop="20"
offsetTop="70"
></MyAnchor>
....
....
<div id="my-anchor-1">123123</div>
可以根据自己需要进行优化和改进来满足自己的需求。