Vue Hash 路由与锚点导航区别及Hash路由下锚点导航的解决方式 Hash 路由用#标识页面改变的是「你要看哪一页」锚点导航用#标识位置改变的是「你要滚到哪里」Hash 路由 https://example.com/#/about ↑ 这是路由路径决定加载哪个组件 锚点导航 https://example.com/page.html#section-2 ↑ 这是页面内位置决定滚动到哪里两者看起来都用了#但背后的机制完全不同Hash 路由锚点导航作用模拟多页面切换视图组件同一页面内定位到某个元素#后面是什么路由路径/about、/user/1元素 IDsection-1、top谁在管理Vue Router 接管浏览器原生行为页面是否刷新否只替换组件否只是滚动URL 变化方式router.push()/router-linklocation.hash/a[href]/scrollIntoView监听机制hashchange事件被 Vue Router 拦截hashchange事件浏览器默认处理各自的工作原理Hash 路由用户点击 router-link → 路由匹配 → 加载对应组件 → 渲染到 router-view URL: /#/home → /#/about → /#/user/42 ↓ ↓ ↓ Home组件 About组件 User组件(42)// Vue Router 内部做的事简化 window.addEventListener(hashchange, () { // 解析 hash 里的路径/#/about → /about // 匹配路由表 // 渲染对应组件 })锚点导航用户点击 a href#section-2 → 浏览器查找 idsection-2 的元素 → 滚动过去 页面 DOM: div idsection-1.../div div idsection-2.../div ← 滚到这里 div idsection-3.../div// 浏览器原生行为没有任何框架参与 // 点击 a href#section-2 时 // 1. URL 变成 当前页面#section-2 // 2. 滚动到 idsection-2 的元素hash路由与锚点导航冲突在 Vue Hash 路由的应用中浏览器默认的锚点行为被 Vue Router 打破了!-- 你的 Vue 应用 -- a href#section-1跳到第一节/a !-- 你以为会发生的事 -- !-- ✅ 滚动到 idsection-1 的元素 -- !-- 实际发生的事 -- !-- ❌ Vue Router 把 #section-1 当成路由去匹配 -- !-- ❌ 路由表里没有 /section-1 这条规则 -- !-- ❌ 可能跳到 404 或者首页页面不会滚动 --根本原因Vue Router 启动后会劫持所有hashchange事件#后面的任何内容都会被当成路由路径解析浏览器的原生锚点滚动行为就失效了。如何让两者共存// router/index.js const router createRouter({ history: createWebHashHistory(), routes: [...], scrollBehavior(to, from, savedPosition) { // Vue Router 把路由的 # 和锚点的 # 统一放到 to.hash 里 // to.path → 路由路径 // to.hash → 锚点部分 if (to.hash) { return { el: to.hash, behavior: smooth, top: 80, } } return { top: 0 } }, })此时的URL拆解 https://example.com/#/docs#install Vue Router 解析为 ┌─────────────────────────────┐ │ to.path /docs │ → 加载 Docs 组件 │ to.hash #install │ → 滚动到 #install └─────────────────────────────┘template !-- 跨页面 锚点先路由跳转再滚动 -- router-link to/docs#install安装指南/router-link !-- 同页面锚点不涉及路由切换 -- router-link to/#features功能介绍/router-link /templateHash 路由 → 管「去哪一页」→ 切组件锚点导航 → 管「去哪个位置」→ 滚页面两者共用 # → 冲突 → 用 scrollBehavior 解决具体解决方案如下方案一scrollBehavior同页面 跨页面通用这是 Vue Router官方推荐的处理方式// router/index.js import { createRouter, createWebHashHistory } from vue-router const router createRouter({ history: createWebHashHistory(), routes: [ { path: /, component: () import(/views/Home.vue) }, { path: /about, component: () import(/views/About.vue) }, { path: /docs, component: () import(/views/Docs.vue) }, ], scrollBehavior(to, from, savedPosition) { // ① 有锚点 → 滚动到目标元素 if (to.hash) { return new Promise((resolve) { // 等待页面异步组件加载完成 setTimeout(() { resolve({ el: to.hash, // CSS 选择器如 #section-1 behavior: smooth, top: 80, // 偏移固定导航栏高度 }) }, 300) // 给异步组件加载留时间 }) } // ② 浏览器前进/后退 → 恢复上次滚动位置 if (savedPosition) { return savedPosition } // ③ 默认滚到顶部 return { top: 0, behavior: smooth } }, }) export default router模板写法template nav !-- 同页面锚点 -- router-link to/#intro简介/router-link router-link to/#features功能/router-link !-- 跨页面锚点 -- router-link to/about#team关于我们 - 团队/router-link router-link to/docs#install文档 - 安装/router-link /nav /template方案二同页面编程式导航更精细的控制同一个页面内的锚点跳转不涉及路由切换template div classpage nav classsidebar a v-fornav in navList :keynav.id :class[nav-item, { active: current nav.id }] clickgoTo(nav.id) {{ nav.label }} /a /nav main classcontent section v-fornav in navList :keynav.id :idnav.id classsection h2{{ nav.label }}/h2 p{{ nav.content }}/p /section /main /div /template script setup import { ref, onMounted, onUnmounted } from vue const navList [ { id: intro, label: 简介, content: ... }, { id: features, label: 功能, content: ... }, { id: pricing, label: 定价, content: ... }, { id: faq, label: 常见问题, content: ... }, ] const current ref(intro) // ---- 点击跳转 ---- function goTo(id) { const el document.getElementById(id) if (!el) return const offset 80 // 固定头部高度 const top el.getBoundingClientRect().top window.scrollY - offset window.scrollTo({ top, behavior: smooth }) // 手动同步更新 URL 的 hash但不触发路由跳转 history.replaceState(null, , #${id}) } // ---- 滚动监听自动高亮 ---- let ticking false function onScroll() { if (ticking) return ticking true requestAnimationFrame(() { const offset 120 for (let i navList.length - 1; i 0; i--) { const el document.getElementById(navList[i].id) if (el el.getBoundingClientRect().top offset) { current.value navList[i].id break } } ticking false }) } onMounted(() window.addEventListener(scroll, onScroll, { passive: true })) onUnmounted(() window.removeEventListener(scroll, onScroll)) /script style scoped .nav-item.active { color: var(--accent); font-weight: 600; } .section { scroll-margin-top: 80px; /* CSS 原生偏移兜底 */ min-height: 60vh; padding: 2rem 0; } /style方案三自定义指令复用场景多个页面都需要锚点导航时封装成指令// directives/anchor.js export const vAnchor { mounted(el, binding) { el.style.cursor pointer el.addEventListener(click, (e) { e.preventDefault() const targetId binding.value const target document.getElementById(targetId) if (!target) return target.scrollIntoView({ behavior: smooth, block: start }) // 用 replaceState 不触发 popstate history.replaceState(null, , #${targetId}) }) }, }// main.js import { vAnchor } from ./directives/anchor app.directive(anchor, vAnchor)template nav a v-anchorintro简介/a a v-anchorfeatures功能/a a v-anchorpricing定价/a /nav /template常见坑和解法坑 1固定导航栏遮挡目标区域/* 最优雅的解法CSS scroll-margin-top */ [id] { scroll-margin-top: 80px; }// JS 兼容方案 function scrollToWithOffset(id, offset 80) { const el document.getElementById(id) if (!el) return const y el.getBoundingClientRect().top window.scrollY - offset window.scrollTo({ top: y, behavior: smooth }) }坑 2scrollBehavior中元素还没渲染异步组件或数据驱动的页面目标元素可能还没挂载scrollBehavior(to) { if (to.hash) { return new Promise((resolve) { // 轮询等待目标元素出现 const tryScroll (retries 10) { const el document.querySelector(to.hash) if (el) { resolve({ el: to.hash, behavior: smooth, top: 80 }) } else if (retries 0) { setTimeout(() tryScroll(retries - 1), 100) } else { resolve({ top: 0 }) // 兜底 } } tryScroll() }) } return { top: 0 } }坑 3滚动监听与 Vue Router 导航冲突// 在滚动监听中避免在路由跳转过程中误触发 import { useRouter } from vue-router const router useRouter() const isNavigating ref(false) router.beforeEach(() { isNavigating.value true }) router.afterEach(() { setTimeout(() { isNavigating.value false }, 500) }) function onScroll() { if (isNavigating.value) return // 路由跳转期间不处理 // ... 正常的高亮逻辑 }