在现代 Web 应用开发中,单页应用(SPA)越来越流行。为了提升用户体验,我们常常需要实现"点击导航菜单 → 平滑滚动到页面对应区块"的功能。本文将基于 Vue 3 + Composition API + Element Plus,结合你提供的代码,深入剖析如何通过自定义事件总线(Event Bus)与 DOM 查询,实现跨组件的精准、平滑滚动,并兼顾 PC 端与移动端的差异化体验。
一、需求场景
在 index.vue(联系页面)中,顶部有三个 Tab:
- 联系总部
- 办公地址
- 在线留言
用户点击任一 Tab,页面应平滑滚动 至下方对应的 .headquarters、.address 或 .message 区块。
而在 App.vue(根布局组件)中,已经封装了全局的滚动容器(.layout)和返回顶部逻辑。

关键挑战:
- 两个组件无直接父子关系,如何通信?
- 滚动容器不是
window,而是自定义的.layout元素。 - 需要处理不同设备下的偏移量(如移动端需更大顶部留白)。
二、技术方案:事件总线 + DOM 定位 + 平滑滚动
1. 使用 mitt 创建轻量级事件总线
注:虽然 Vue 官方不推荐在大型项目中使用 Event Bus,但对于简单跨组件通信(如本例),它简洁高效。
// utils/eventbus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter
2. 发送滚动指令(index.vue)
当用户点击 Tab 时,通过 emitter.emit 发送一个包含目标标识的事件:
<!-- index.vue -->
<script setup>
import emitter from "@/utils/eventbus.js"
const handleChose = (val) => {
currentTab.value = val
// 发送滚动事件,val 为 1/2/3
emitter.emit("scroll", currentTab.value)
}
</script>
3. 监听并执行滚动(App.vue)
在 App.vue 的 onMounted 中监听 "scroll" 事件,并根据传入的 val 映射到具体的 DOM 选择器:
// App.vue
emitter.on("scroll", (val) => {
let targetSelector
switch (val) {
case 1: targetSelector = '.headquarters'; break
case 2: targetSelector = '.address'; break
case 3: targetSelector = '.message'; break
default: return
}
const targetElement = document.querySelector(targetSelector)
if (targetElement && layout.value) {
// 获取目标元素相对于视口的位置
const rect = targetElement.getBoundingClientRect()
// 获取滚动容器(.layout)相对于视口的位置
const layoutRect = layout.value.getBoundingClientRect()
// 计算目标元素相对于滚动容器的偏移量
const relativeTop = rect.top - layoutRect.top
// 当前已滚动的距离
const currentScrollTop = layout.value.scrollTop
// 根据设备类型设置顶部偏移(避免被固定头部遮挡)
const offset = isMobile.value ? 100 : 50
// 最终滚动位置 = 当前滚动位置 + 相对偏移 - 顶部留白
const targetPosition = currentScrollTop + relativeTop - offset
// 使用原生 smooth 滚动
layout.value.scrollTo({
top: targetPosition,
behavior: "smooth"
})
}
})
✅ 关键点解析:
getBoundingClientRect()返回元素相对于视口的位置。- 由于滚动发生在
.layout内部,而非整个窗口,因此必须计算 目标元素相对于滚动容器的位置。offset是为了防止内容被 Header 遮挡,在移动端预留更多空间(100px vs 50px)。
三、为什么不用 window.scrollTo?
因为你的页面结构是:
<div class="layout" style="overflow-y: auto; height: 100vh;">
<Header />
<div class="content"><RouterView /></div>
<Footer />
</div>
真正的滚动条属于 .layout 元素 ,而不是 body 或 window。所以必须操作 layout.value.scrollTop 或调用其 scrollTo 方法。
四、移动端适配处理
通过 useIsMobile Hook 判断设备类型:
// hooks/useIsMobile.js
import { ref, onMounted } from 'vue'
export function useIsMobile() {
const isMobile = ref(false)
onMounted(() => {
isMobile.value = window.innerWidth <= 767
})
return { isMobile }
}
然后在计算 offset 时动态调整:
const offset = isMobile.value ? 100 : 50
这样在手机上滚动后,内容不会紧贴顶部,留出足够空间供用户查看。
五、完整流程图
[用户点击 Tab]
↓
(index.vue) emit("scroll", 1)
↓
(App.vue) 监听到事件
↓
根据 val=1 → 选择器 '.headquarters'
↓
querySelector 获取 DOM 元素
↓
计算该元素在 .layout 中的相对位置
↓
加上设备适配偏移量(PC:50px, Mobile:100px)
↓
调用 layout.scrollTo({ top: ..., behavior: 'smooth' })
↓
页面平滑滚动到目标区域
六、总结
本文通过分析真实项目代码,展示了如何在 Vue 3 项目中实现跨组件、容器内、响应式的平滑滚动导航。核心在于:
- 事件解耦 :使用
mitt实现松耦合通信; - 精准定位 :利用
getBoundingClientRect计算相对位置; - 体验优先:区分 PC/Mobile 设置不同偏移量;
- 原生能力 :直接使用
element.scrollTo({ behavior: 'smooth' }),无需第三方库。
这种模式适用于任何需要"锚点跳转"但又不在 window 滚动的 SPA 场景,值得收藏复用!