滚动工具 (scroll.ts)
滚动相关工具函数,提供平滑滚动、位置控制、可见性检测等功能,让页面滚动体验更加流畅和用户友好。
📖 概述
滚动工具库包含以下核心功能:
- 滚动动画控制:提供动画缓动和平滑切换
- 滚动位置操作:获取和设置页面滚动位置
- 平滑滚动导航:滚动到指定位置或元素
- 可见性检测:检查元素是否在视口内可见
🎨 动画控制函数
easeInOutQuad
二次缓动函数,提供平滑的开始和结束动画效果。
typescript
easeInOutQuad(t: number, b: number, c: number, d: number): number
参数:
t
- 当前时间b
- 起始值c
- 变化量d
- 持续时间
返回值:
number
- 当前时间对应的值
特点:
- 动画开始时缓慢加速
- 中段快速运动
- 结束时平滑减速
- 提供自然的用户体验
requestAnimFrame
requestAnimationFrame
的跨浏览器兼容版本,用于智能动画控制。
typescript
const requestAnimFrame: (callback: FrameRequestCallback) => void
特点:
- 自动检测浏览器支持
- 提供降级方案(60fps setTimeout)
- 优化性能和电池使用
- 确保动画流畅运行
📍 位置操作
getScrollPosition
获取当前页面的滚动位置。
typescript
getScrollPosition(): number
返回值:
number
- 当前滚动位置(像素)
示例:
typescript
// 获取当前滚动位置
const currentPos = getScrollPosition()
console.log(`当前滚动到: ${currentPos}px`)
// 在滚动事件中使用
window.addEventListener('scroll', () => {
const scrollTop = getScrollPosition()
if (scrollTop > 100) {
// 显示回到顶部按钮
showBackToTopButton()
} else {
// 隐藏回到顶部按钮
hideBackToTopButton()
}
})
setScrollPosition
设置页面滚动位置。
typescript
setScrollPosition(position: number): void
参数:
position
- 滚动位置(像素)
示例:
typescript
// 立即滚动到指定位置
setScrollPosition(500)
// 保存和恢复滚动位置
const savedPosition = getScrollPosition()
// ... 进行其他操作
setScrollPosition(savedPosition)
特点:
- 兼容多种DOM结构
- 同时设置多个可能的滚动元素
- 确保在各种环境下都能正常工作
🚀 平滑滚动
scrollTo
平滑滚动到指定位置。
typescript
scrollTo(to: number, duration?: number, callback?: () => void): void
参数:
to
- 目标位置(像素)duration
- 动画持续时间(毫秒),默认为500mscallback
- 滚动完成后的回调函数
示例:
typescript
// 基本使用:滚动到500px位置
scrollTo(500)
// 自定义动画时间
scrollTo(1000, 1000) // 1秒内滚动到1000px
// 带回调函数
scrollTo(800, 600, () => {
console.log('滚动完成!')
// 可以在这里执行其他操作
highlightTargetElement()
})
// 实际应用:点击导航滚动到对应部分
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault()
const targetId = link.getAttribute('href').substring(1)
const targetElement = document.getElementById(targetId)
const targetPosition = targetElement.offsetTop
scrollTo(targetPosition, 800, () => {
// 更新URL但不触发页面跳转
history.pushState(null, '', `#${targetId}`)
})
})
})
scrollToTop
滚动到页面顶部的快捷方法。
typescript
scrollToTop(duration?: number, callback?: () => void): void
参数:
duration
- 动画持续时间(毫秒),默认为500mscallback
- 滚动完成后的回调函数
示例:
typescript
// 基本使用
scrollToTop()
// 慢速滚动到顶部
scrollToTop(1500)
// 带回调的回到顶部
scrollToTop(800, () => {
console.log('已回到页面顶部')
// 可以在这里隐藏"回到顶部"按钮
hideBackToTopButton()
})
// 回到顶部按钮实现
const backToTopBtn = document.querySelector('.back-to-top')
backToTopBtn.addEventListener('click', () => {
scrollToTop(600)
})
scrollToElement
滚动到指定元素位置。
typescript
scrollToElement(
element: HTMLElement | string,
offset?: number,
duration?: number,
callback?: () => void
): void
参数:
element
- 目标元素或元素选择器offset
- 偏移量(像素),默认为0duration
- 动画持续时间(毫秒),默认为500mscallback
- 滚动完成后的回调函数
示例:
typescript
// 滚动到指定元素
const targetElement = document.querySelector('.target-section')
scrollToElement(targetElement)
// 使用选择器
scrollToElement('.about-section')
// 带偏移量(考虑固定头部)
scrollToElement('.content', -80) // 向上偏移80px
// 完整配置
scrollToElement('.contact', -100, 1000, () => {
console.log('已滚动到联系我们部分')
// 高亮显示目标元素
targetElement.classList.add('highlight')
})
// 实际应用:锚点导航
class SmoothNavigation {
constructor() {
this.bindEvents()
}
bindEvents() {
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault()
const targetId = link.getAttribute('href')
scrollToElement(targetId, -60, 800, () => {
// 更新活动导航项
this.updateActiveNav(targetId)
})
})
})
}
updateActiveNav(targetId: string) {
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active')
})
const activeLink = document.querySelector(`[href="${targetId}"]`)
if (activeLink) {
activeLink.classList.add('active')
}
}
}
const smoothNav = new SmoothNavigation()
👁️ 可见性检测
isElementInViewport
检查元素是否在视口内可见。
typescript
isElementInViewport(element: HTMLElement, partiallyVisible?: boolean): boolean
参数:
element
- 要检查的元素partiallyVisible
- 是否计算部分可见,默认为false(完全可见)
返回值:
boolean
- 元素是否在视口内
示例:
typescript
const element = document.querySelector('.animate-on-scroll')
// 检查元素是否完全可见
if (isElementInViewport(element)) {
console.log('元素完全可见')
element.classList.add('animate')
}
// 检查元素是否部分可见
if (isElementInViewport(element, true)) {
console.log('元素至少部分可见')
element.classList.add('fade-in')
}
// 滚动监听实现懒加载
window.addEventListener('scroll', () => {
document.querySelectorAll('.lazy-load').forEach(img => {
if (isElementInViewport(img, true)) {
// 加载图片
img.src = img.dataset.src
img.classList.remove('lazy-load')
}
})
})
💡 实际应用场景
1. 平滑导航系统
typescript
class SmoothScrollNavigation {
private navItems: NodeListOf<HTMLElement>
private sections: NodeListOf<HTMLElement>
private headerHeight: number
constructor() {
this.navItems = document.querySelectorAll('.nav-item')
this.sections = document.querySelectorAll('.section')
this.headerHeight = 80
this.bindEvents()
this.updateActiveOnScroll()
}
bindEvents() {
this.navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault()
const targetId = item.getAttribute('href')?.substring(1)
const targetSection = document.getElementById(targetId)
if (targetSection) {
scrollToElement(targetSection, -this.headerHeight, 800, () => {
this.updateActiveNav(item)
})
}
})
})
}
updateActiveOnScroll() {
window.addEventListener('scroll', () => {
const scrollPos = getScrollPosition() + this.headerHeight + 50
this.sections.forEach((section, index) => {
const sectionTop = section.offsetTop
const sectionBottom = sectionTop + section.offsetHeight
if (scrollPos >= sectionTop && scrollPos < sectionBottom) {
this.updateActiveNav(this.navItems[index])
}
})
})
}
updateActiveNav(activeItem: HTMLElement) {
this.navItems.forEach(item => item.classList.remove('active'))
activeItem.classList.add('active')
}
}
2. 回到顶部按钮
typescript
class BackToTopButton {
private button: HTMLElement
private threshold: number
constructor(selector: string, threshold: number = 300) {
this.button = document.querySelector(selector)
this.threshold = threshold
this.bindEvents()
this.handleScroll()
}
bindEvents() {
this.button.addEventListener('click', () => {
scrollToTop(800, () => {
this.button.classList.add('success')
setTimeout(() => {
this.button.classList.remove('success')
}, 1000)
})
})
window.addEventListener('scroll', () => {
this.handleScroll()
})
}
handleScroll() {
const scrollPos = getScrollPosition()
if (scrollPos > this.threshold) {
this.button.classList.add('show')
} else {
this.button.classList.remove('show')
}
}
}
// 使用
const backToTop = new BackToTopButton('.back-to-top', 500)
3. 滚动动画触发器
typescript
class ScrollAnimationTrigger {
private elements: NodeListOf<HTMLElement>
private animatedElements: Set<HTMLElement>
constructor() {
this.elements = document.querySelectorAll('.scroll-animate')
this.animatedElements = new Set()
this.bindScrollEvent()
this.checkVisibility() // 初始检查
}
bindScrollEvent() {
let ticking = false
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
this.checkVisibility()
ticking = false
})
ticking = true
}
})
}
checkVisibility() {
this.elements.forEach(element => {
if (this.animatedElements.has(element)) return
if (isElementInViewport(element, true)) {
this.triggerAnimation(element)
this.animatedElements.add(element)
}
})
}
triggerAnimation(element: HTMLElement) {
const animationType = element.dataset.animation || 'fadeIn'
const delay = parseInt(element.dataset.delay || '0')
setTimeout(() => {
element.classList.add('animate', animationType)
}, delay)
}
}
// 使用
const scrollTrigger = new ScrollAnimationTrigger()
4. 无限滚动加载
typescript
class InfiniteScroll {
private container: HTMLElement
private loading: boolean = false
private page: number = 1
private hasMore: boolean = true
constructor(containerSelector: string) {
this.container = document.querySelector(containerSelector)
this.bindScrollEvent()
}
bindScrollEvent() {
window.addEventListener('scroll', () => {
if (this.shouldLoadMore()) {
this.loadMore()
}
})
}
shouldLoadMore(): boolean {
if (this.loading || !this.hasMore) return false
const scrollPos = getScrollPosition()
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
// 距离底部还有200px时开始加载
return scrollPos + windowHeight >= documentHeight - 200
}
async loadMore() {
this.loading = true
this.showLoading()
try {
const data = await this.fetchData(this.page)
if (data.length === 0) {
this.hasMore = false
} else {
this.renderData(data)
this.page++
}
} catch (error) {
console.error('加载失败:', error)
} finally {
this.loading = false
this.hideLoading()
}
}
async fetchData(page: number): Promise<any[]> {
// 模拟API调用
const response = await fetch(`/api/data?page=${page}`)
return response.json()
}
renderData(data: any[]) {
data.forEach(item => {
const element = this.createDataElement(item)
this.container.appendChild(element)
})
}
createDataElement(item: any): HTMLElement {
const div = document.createElement('div')
div.className = 'data-item scroll-animate'
div.innerHTML = `
<h3>${item.title}</h3>
<p>${item.content}</p>
`
return div
}
showLoading() {
const loader = document.querySelector('.loading')
if (loader) loader.classList.add('show')
}
hideLoading() {
const loader = document.querySelector('.loading')
if (loader) loader.classList.remove('show')
}
}
// 使用
const infiniteScroll = new InfiniteScroll('.content-container')
5. 滚动进度指示器
typescript
class ScrollProgressIndicator {
private progressBar: HTMLElement
private progressText: HTMLElement
constructor() {
this.createProgressBar()
this.bindScrollEvent()
}
createProgressBar() {
// 创建进度条
this.progressBar = document.createElement('div')
this.progressBar.className = 'scroll-progress-bar'
document.body.appendChild(this.progressBar)
// 创建进度文本(可选)
this.progressText = document.createElement('div')
this.progressText.className = 'scroll-progress-text'
document.body.appendChild(this.progressText)
}
bindScrollEvent() {
window.addEventListener('scroll', () => {
this.updateProgress()
})
}
updateProgress() {
const scrollTop = getScrollPosition()
const docHeight = document.documentElement.scrollHeight - window.innerHeight
const progress = Math.min((scrollTop / docHeight) * 100, 100)
// 更新进度条
this.progressBar.style.width = `${progress}%`
// 更新进度文本
this.progressText.textContent = `${Math.round(progress)}%`
// 根据进度改变样式
if (progress > 80) {
this.progressBar.classList.add('near-end')
} else {
this.progressBar.classList.remove('near-end')
}
}
}
// 使用
const progressIndicator = new ScrollProgressIndicator()
🎨 CSS 配合使用
这些滚动工具通常需要配合相应的CSS样式:
css
/* 平滑滚动(CSS方式,作为备选) */
html {
scroll-behavior: smooth;
}
/* 回到顶部按钮 */
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background: #007bff;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
z-index: 1000;
}
.back-to-top.show {
opacity: 1;
transform: translateY(0);
}
.back-to-top.success {
background: #28a745;
transform: scale(1.1);
}
/* 滚动动画 */
.scroll-animate {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s ease;
}
.scroll-animate.animate {
opacity: 1;
transform: translateY(0);
}
/* 滚动进度条 */
.scroll-progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #007bff, #28a745);
z-index: 9999;
transition: width 0.1s ease;
}
.scroll-progress-bar.near-end {
background: linear-gradient(90deg, #28a745, #ffc107);
}
/* 导航激活状态 */
.nav-item.active {
color: #007bff;
font-weight: bold;
}
/* 加载器 */
.loading {
text-align: center;
padding: 20px;
opacity: 0;
transition: opacity 0.3s ease;
}
.loading.show {
opacity: 1;
}
⚡ 性能优化建议
1. 节流滚动事件
typescript
import { throttle } from './function' // 假设有节流函数
// ❌ 不推荐:频繁触发
window.addEventListener('scroll', () => {
checkElementsVisibility()
})
// ✅ 推荐:节流处理
window.addEventListener('scroll', throttle(() => {
checkElementsVisibility()
}, 100))
2. 使用 Intersection Observer
typescript
// 现代浏览器推荐使用 Intersection Observer
class ModernScrollAnimationTrigger {
private observer: IntersectionObserver
constructor() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.triggerAnimation(entry.target as HTMLElement)
this.observer.unobserve(entry.target)
}
})
},
{ threshold: 0.1 }
)
document.querySelectorAll('.scroll-animate').forEach(el => {
this.observer.observe(el)
})
}
triggerAnimation(element: HTMLElement) {
element.classList.add('animate')
}
}
3. 缓存DOM查询
typescript
// ❌ 不推荐:重复查询DOM
function handleScroll() {
const elements = document.querySelectorAll('.animate-on-scroll') // 每次都查询
// ...
}
// ✅ 推荐:缓存DOM引用
class ScrollHandler {
private elements: NodeListOf<HTMLElement>
constructor() {
this.elements = document.querySelectorAll('.animate-on-scroll') // 只查询一次
}
handleScroll() {
this.elements.forEach(element => {
// 使用缓存的引用
})
}
}
⚠️ 注意事项
- 兼容性:某些功能在老旧浏览器中可能需要polyfill
- 性能影响:频繁的滚动事件监听可能影响性能,建议使用节流
- 内存泄漏:记得在组件销毁时移除事件监听器
- 移动端适配:在移动设备上测试滚动体验
- 可访问性:考虑用户的减少动画偏好设置