在前端开发中,我们经常会遇到需要实现横向滚动的场景,如图片画廊、时间轴、横向导航等。然而,浏览器默认的滚轮事件只支持垂直方向滚动,如何优雅地实现鼠标滚轮控制横向滚动成为了一个常见的需求。本文将介绍如何使用 Vue 3 的组合式 API 创建一个自定义 Hook 来解决这个问题。
问题分析
在实现横向滚动时,我们面临两个主要挑战:
- 如何区分需要横向滚动和垂直滚动的场景
- 如何兼容不同的 UI 组件库(如 Element Plus)和原生 DOM 元素
实现思路
我们的解决方案需要具备以下特性:
- 能够智能判断何时应该进行横向滚动
- 兼容 Element Plus 的 Scrollbar 组件和原生 DOM 元素
- 提供平滑的滚动体验
- 在无法横向滚动时回退到默认垂直滚动行为
代码实现
javascript
import { computed, nextTick } from 'vue';
/**
* 判断是否为数组
* @param {*} arr - 需要判断的值
* @returns {boolean} 是否为数组
*/
export const isArray = (arr) => {
return Array.isArray(arr);
};
/**
* 鼠标滚轮横向滚动处理 Hook
* @param {string} [refName] - 可选的 ref 名称,用于 Element Plus Scrollbar 组件
* @returns {Object} 包含 handleWheel 方法的对象
*/
export function useWheel(refName) {
// 获取模板引用
const scrollbarRef = refName ? useTemplateRef(refName) : ref(null);
// 计算目标引用,处理数组情况
const targetRef = computed(() => {
return isArray(scrollbarRef.value) ? scrollbarRef.value[0] : scrollbarRef.value;
});
/**
* 处理鼠标滚轮事件
* @param {Event} event - 滚轮事件对象
*/
const handleWheel = async (event) => {
await nextTick();
// 确定滚动容器
let scrollContainer;
if (targetRef.value) {
// Element Plus Scrollbar 组件
scrollContainer = targetRef.value.$el?.querySelector(".el-scrollbar__wrap");
} else {
// 原生 DOM 元素
scrollContainer = event.currentTarget;
}
if (!scrollContainer) return;
// 获取滚动容器信息
const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;
const maxScrollLeft = scrollWidth - clientWidth;
const delta = event.deltaY;
// 判断是否需要横向滚动
const canScrollRight = delta > 0 && scrollLeft < maxScrollLeft;
const canScrollLeft = delta < 0 && scrollLeft > 0;
// 如果可以横向滚动,则执行横向滚动并阻止默认行为
if (maxScrollLeft > 0 && (canScrollRight || canScrollLeft)) {
scrollContainer.scrollTo({
left: scrollLeft + delta * 2, // 乘以2增加滚动速度
behavior: "smooth" // 平滑滚动效果
});
event.preventDefault();
}
// 无法横向滚动时允许默认垂直滚动
};
return {
handleWheel,
};
}
使用示例
在 Element Plus Scrollbar 组件中使用
vue
<template>
<el-scrollbar ref="scrollbar" @wheel="handleWheel">
<!-- 横向内容 -->
<div class="horizontal-content">
<div v-for="item in 20" :key="item" class="card">
Card {{ item }}
</div>
</div>
</el-scrollbar>
</template>
<script setup>
import { useWheel } from "@/hooks/useWheel";
const { handleWheel } = useWheel("scrollbar");
</script>
<style scoped>
.horizontal-content {
display: flex;
width: max-content;
}
.card {
width: 200px;
height: 150px;
margin: 10px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
}
</style>
在原生 DOM 元素中使用
vue
<template>
<div class="custom-scroll-container" @wheel="handleWheel">
<!-- 横向内容 -->
<div class="horizontal-content">
<div v-for="item in 20" :key="item" class="card">
Card {{ item }
</div>
</div>
</div>
</template>
<script setup>
import { useWheel } from "@/hooks/useWheel";
const { handleWheel } = useWheel();
</script>
<style scoped>
.custom-scroll-container {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
}
.horizontal-content {
display: flex;
width: max-content;
}
.card {
width: 200px;
height: 150px;
margin: 10px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
}
</style>
实现原理详解
1. 引用处理
Hook 通过 useTemplateRef
获取对目标元素的引用,并处理了引用可能是数组的情况(Vue 3 中模板引用有时会是数组)。
2. 容器确定
根据是否提供了 ref 名称,Hook 能够智能确定要操作的滚动容器:
- 对于 Element Plus Scrollbar 组件,需要通过
$el.querySelector(".el-scrollbar__wrap")
找到实际的滚动容器 - 对于原生 DOM 元素,直接使用事件的目标元素
3. 滚动逻辑
通过比较容器的 scrollWidth
和 clientWidth
,判断是否存在横向滚动空间。然后根据滚轮方向和当前滚动位置,决定是否应该进行横向滚动。
4. 平滑体验
使用 scrollTo
方法而非直接修改 scrollLeft
,配合 behavior: "smooth"
选项,提供平滑的滚动体验。