滚动文字指令 scrollText
概述
scrollText 是一个自定义 Vue 指令,用于实现文字内容超出容器宽度时的自动水平滚动效果。该指令常见于音频播放器的歌单标题、通知栏消息滚动等场景。本节从零实现该指令,并处理多种交互细节。
滇动场景分析
典型应用场景
| 场景 | 示例 |
|---|---|
| 音频播放器 | 歌曲标题过长时自动滚动 |
| 通知栏 | 系统公告、消息提醒横向滚动 |
| 标签展示 | 标签文本溢出时滚动显示 |
交互细节要求
| 交互 | 行为 |
|---|---|
| 鼠标悬停(PC端) | 暂停滚动 |
| 鼠标移出 | 恢复滚动 |
| 触摸按住(移动端) | 暂停滚动 |
| 触摸松开 | 恢复滚动 |
| 滚动到右端 | 停止并等待几秒后反向滚动 |
| 内容宽度未超出容器 | 不触发滚动 |
指令实现
核心 CSS 滚动原理
/* 容器:隐藏溢出内容 */
.scroll-text-container {
overflow: hidden;
white-space: nowrap;
position: relative;
}
/* 内容:通过 transform: translateX() 实现滚动 */
.scroll-text-content {
display: inline-block;
transition: none; /* JS 控制时不需要 CSS transition */
}
css
指令基础结构
// directives/scrollText.ts
import type { Directive, DirectiveBinding } from 'vue'
interface ScrollTextOptions {
speed?: number // 滚动速度(像素/帧),默认 1
delay?: number // 到达边界后暂停时间(ms),默认 2000
direction?: 'left' | 'right' // 初始滚动方向,默认 'left'
}
interface ScrollState {
animationId: number | null
isPaused: boolean
currentOffset: number
direction: 'left' | 'right'
maxScroll: number
delayTimer: ReturnType<typeof setTimeout> | null
}
/**
* v-scrollText 指令
* 用法:v-scrollText="{ speed: 1, delay: 2000 }"
*/
export const scrollText: Directive<HTMLElement, ScrollTextOptions> = {
mounted(el: HTMLElement, binding: DirectiveBinding<ScrollTextOptions>) {
const options: ScrollTextOptions = {
speed: 1,
delay: 2000,
direction: 'left',
...binding.value
}
// 检测是否需要滚动
const containerWidth = el.clientWidth
const contentWidth = el.scrollWidth
if (contentWidth <= containerWidth) {
// 内容未溢出,无需滚动
return
}
const state: ScrollState = {
animationId: null,
isPaused: false,
currentOffset: 0,
direction: options.direction as 'left' | 'right',
maxScroll: contentWidth - containerWidth,
delayTimer: null
}
// 存储状态到元素上
;(el as any)._scrollTextState = state
;(el as any)._scrollTextOptions = options
// 启动滚动动画
startScroll(el, state, options)
bindEvents(el, state)
},
unmounted(el: HTMLElement) {
const state = (el as any)._scrollTextState as ScrollState | undefined
if (state) {
if (state.animationId) cancelAnimationFrame(state.animationId)
if (state.delayTimer) clearTimeout(state.delayTimer)
}
unbindEvents(el)
}
}
typescript
动画循环实现
function startScroll(
el: HTMLElement,
state: ScrollState,
options: Required<ScrollTextOptions>
) {
const animate = () => {
if (state.isPaused) {
state.animationId = requestAnimationFrame(animate)
return
}
// 根据方向更新偏移量
if (state.direction === 'left') {
state.currentOffset -= options.speed
if (state.currentOffset <= -state.maxScroll) {
state.currentOffset = -state.maxScroll
pauseAndReverse(el, state, options)
}
} else {
state.currentOffset += options.speed
if (state.currentOffset >= 0) {
state.currentOffset = 0
pauseAndReverse(el, state, options)
}
}
el.style.transform = `translateX(${state.currentOffset}px)`
state.animationId = requestAnimationFrame(animate)
}
state.animationId = requestAnimationFrame(animate)
}
/**
* 到达边界后暂停指定时间,再反向滚动
*/
function pauseAndReverse(
el: HTMLElement,
state: ScrollState,
options: Required<ScrollTextOptions>
) {
state.isPaused = true
state.delayTimer = setTimeout(() => {
state.direction = state.direction === 'left' ? 'right' : 'left'
state.isPaused = false
}, options.delay)
}
typescript
事件绑定(PC端 + 移动端)
function bindEvents(el: HTMLElement, state: ScrollState) {
// PC 端:鼠标悬停暂停
el.addEventListener('mouseenter', () => {
state.isPaused = true
})
el.addEventListener('mouseleave', () => {
state.isPaused = false
})
// 移动端:触摸按住暂停
el.addEventListener('touchstart', () => {
state.isPaused = true
})
el.addEventListener('touchend', () => {
state.isPaused = false
})
}
function unbindEvents(el: HTMLElement) {
// Vue 指令 unmounted 中清理事件
// 这里通过 AbortController 或直接移除
}
typescript
使用方式
<template>
<!-- 基础用法 -->
<div class="song-title" v-scrollText>
这是一首非常非常非常非常长的歌曲标题名称
</div>
<!-- 自定义参数 -->
<div class="notification" v-scrollText="{ speed: 2, delay: 3000 }">
系统公告:今天下午三点进行系统维护升级...
</div>
</template>
<script setup lang="ts">
import { scrollText } from '@/directives/scrollText'
</script>
vue
关键技术要点
| 要点 | 说明 |
|---|---|
| 溢出检测 | 通过 el.scrollWidth > el.clientWidth 判断是否需要滚动 |
| 动画驱动 | 使用 requestAnimationFrame 实现 60fps 流畅滚动 |
| 方向切换 | 到达边界后通过 setTimeout 延迟反转方向 |
| 暂停机制 | PC 端用 mouseenter/mouseleave,移动端用 touchstart/touchend |
| 内存回收 | unmounted 钩子中必须清除 animationId 和 delayTimer |
性能优化建议
- 使用
transform: translateX()代替marginLeft/left,避免触发 layout 重排 requestAnimationFrame自动适配屏幕刷新率,避免使用setInterval- 内容未溢出时直接跳过,不创建动画循环
↑