各位靓仔靓女,今天咱们聊聊 Vue Router 里的一个神奇小精灵:scrollBehavior
。它就像个贴心的管家,负责在你切换路由的时候,帮你把页面滚动到指定的位置,让你的用户体验丝滑流畅,告别手动滚动的尴尬。
大家好!现在开始咱们的 Vue Router scrollBehavior
源码探秘之旅!
一、scrollBehavior
是个啥?它的使命是啥?
简单来说,scrollBehavior
是 Vue Router 提供的一个配置选项,允许你自定义路由切换时的页面滚动行为。默认情况下,每次路由切换,浏览器都会尝试恢复到之前的滚动位置。但有时候,我们希望页面滚动到顶部,或者滚动到特定的元素位置,或者干脆保持原样。这时候,scrollBehavior
就派上用场了。
它的使命就是:控制路由切换时的页面滚动位置,提升用户体验。
二、scrollBehavior
的配置方式:三种姿势任你选
scrollBehavior
可以是一个函数,这个函数接收两个参数,返回一个描述滚动位置的对象。这个函数会在每次路由切换后被调用。
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// to: 即将要进入的目标路由对象
// from: 当前导航正要离开的路由对象
// savedPosition: 只有在使用 popstate 导航时才可用 (比如点击浏览器的后退/前进按钮)。
// 返回期望滚动到的位置
// 可以是以下形式:
// - { x: number, y: number }
// - { selector: string }
// - { x: number, y: number, behavior: 'auto' | 'smooth' }
// - 异步返回 Promise<PositionResult>
}
})
让我们仔细看看这三个参数:
to
: 目标路由对象,包含了你将要导航到的路由的所有信息,比如路径、参数、查询参数等等。from
: 当前路由对象,包含了你当前所在路由的所有信息。savedPosition
: 这个参数只有在使用popstate
导航时才可用。啥是popstate
导航呢?简单来说,就是用户点击浏览器的前进/后退按钮时触发的导航。这个savedPosition
包含了上一次页面的滚动位置,你可以利用它来实现“记忆滚动位置”的功能。
返回值类型:
{ x: number, y: number }
: 直接指定滚动条的横纵坐标。比如{ x: 0, y: 0 }
表示滚动到页面顶部。{ selector: string }
: 指定一个 CSS 选择器,页面会滚动到该选择器对应的元素的位置。比如{ selector: '#app' }
表示滚动到 id 为app
的元素。{ x: number, y: number, behavior: 'auto' | 'smooth' }
: 指定滚动条的横纵坐标,并指定滚动行为。behavior
可以是'auto'
(立即滚动) 或者'smooth'
(平滑滚动)。Promise<PositionResult>
: 异步返回一个描述滚动位置的对象。这个允许你执行一些异步操作,比如等待某个元素加载完成,然后再滚动到指定位置。
三、源码剖析:scrollBehavior
是如何工作的?
要理解 scrollBehavior
的工作原理,我们需要深入 Vue Router 的源码。这里我们关注的核心代码片段位于 src/util/scroll.js
和 src/history/base.js
中。
-
src/util/scroll.js
: 包含一些辅助函数,用于获取当前页面的滚动位置,以及执行滚动操作。// src/util/scroll.js const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent // detect android 4.0+ const isAndroid = ua.indexOf('Android') > -1 const isNewerAndroid = ua.indexOf('Chrome') > -1 if (isAndroid && !isNewerAndroid) { return false } return window.history && typeof window.history.pushState === 'function' })() function getScrollPosition (): { x: number, y: number } { const supportPageOffset = window.pageXOffset !== undefined const x = supportPageOffset ? window.pageXOffset : document.documentElement.scrollLeft const y = supportPageOffset ? window.pageYOffset : document.documentElement.scrollTop return { x: x, y: y } } function getElementPosition (el: Element): { x: number, y: number } { const docRect = document.documentElement.getBoundingClientRect() const elRect = el.getBoundingClientRect() return { x: elRect.left - docRect.left, y: elRect.top - docRect.top } } function isValidPosition (obj: any): boolean { return typeof obj === 'object' && typeof obj.x === 'number' && typeof obj.y === 'number' } function scrollLeft (x: number): void { window.scrollTo(x, getScrollPosition().y) } function scrollTop (y: number): void { window.scrollTo(getScrollPosition().x, y) } function scrollTo (x: number, y: number): void { window.scrollTo(x, y) } export { supportsPushState, getScrollPosition, getElementPosition, isValidPosition, scrollLeft, scrollTop, scrollTo }
getScrollPosition()
: 获取当前页面的滚动位置 (x, y)。getElementPosition(el)
: 获取元素相对于文档的坐标。scrollTo(x, y)
: 滚动到指定的坐标。
-
src/history/base.js
: 这是 Vue Router 历史模式的基础类,负责处理路由切换和滚动行为。核心代码如下:// src/history/base.js export class History { constructor (router: Router, base: ?string) { this.router = router this.base = normalizeBase(base) this.current = START // installed by Router.use(VueRouter) this.pending = null this.ready = false this.readyCbs = [] this.readyErrorCbs = [] this.errorCbs = [] } listen (cb: Function) { this.cb = cb } onReady (cb: Function, errorCb: ?Function) { if (this.ready) { cb() } else { this.readyCbs.push(cb) if (errorCb) { this.readyErrorCbs.push(errorCb) } } } onError (cb: Function) { this.errorCbs.push(cb) } transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { let route try { route = this.router.match(location, this.current) } catch (e) { this.errorCbs.forEach(cb => { cb(e) }) // Exception should still be thrown after executing the error callbacks. throw e } const prev = this.current this.confirmTransition( route, () => { this.updateRoute(route) onComplete && onComplete(route) this.ensureURL() // 调用 scrollBehavior if (this.router.options.scrollBehavior) { this.handleScroll(route, prev, savedPosition) // 调用 handleScroll } if (this.pending) { this.pending = null } }, err => { if (onAbort) { onAbort(err) } if (err && typeof err === 'object' && err.isCancelled) { return } this.ensureURL(true) } ) } updateRoute (route: Route) { const prev = this.current this.current = route this.cb && this.cb(route) this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) } handleScroll (to: Route, from: Route, isPop?: boolean) { const router = this.router if (!router.app) { return } const behavior = router.options.scrollBehavior if (!behavior) { return } if (isPop && savedPosition) { return setTimeout(() => { this.scrollToPosition(savedPosition) }, supportsPushState ? 0 : 1) } let position = false try { position = behavior.call(router, to, from, savedPosition) } catch (e) { return console.error(e) } // 异步处理 if (!position) { return } if (typeof position.then === 'function') { position.then(position => { this.scrollToPosition(position) }).catch(err => { console.log(err) }) } else { this.scrollToPosition(position) } } scrollToPosition (position) { if (!position) return if (typeof position.selector === 'string') { const el = document.querySelector(position.selector) if (el) { let offset = getElementPosition(el) scrollTo(offset.x, offset.y) } } else if (isValidPosition(position)) { scrollTo(position.x, position.y) } } }
transitionTo(location, onComplete, onAbort)
: 这个方法负责处理路由切换的核心逻辑。它首先匹配路由,然后确认过渡,最后调用updateRoute
更新当前路由。在updateRoute
之后,会调用handleScroll
来处理滚动行为。handleScroll(to, from, isPop)
: 这个方法是scrollBehavior
的核心执行者。它首先判断是否需要处理滚动,然后调用用户配置的scrollBehavior
函数,获取期望的滚动位置。如果scrollBehavior
返回的是一个 Promise,它会等待 Promise resolve 后再进行滚动。scrollToPosition(position)
: 这个方法接收一个描述滚动位置的对象,然后根据对象的类型 ({ x, y }
或{ selector }
) 调用相应的滚动方法。
流程总结:
- 路由切换触发
transitionTo
方法。 transitionTo
方法调用handleScroll
方法。handleScroll
方法调用用户配置的scrollBehavior
函数,获取期望的滚动位置。handleScroll
方法调用scrollToPosition
方法。scrollToPosition
方法根据滚动位置的类型,调用scrollTo
或getElementPosition
方法来执行滚动操作。
四、实战演练:几种常见的 scrollBehavior
用法
-
滚动到页面顶部:
scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 } }
这个是最简单的用法,每次路由切换都会滚动到页面顶部。
-
记忆滚动位置:
scrollBehavior (to, from, savedPosition) { if (savedPosition) { return savedPosition } else { return { x: 0, y: 0 } } }
这个用法会尝试恢复之前的滚动位置 (如果存在),否则滚动到页面顶部。
-
滚动到指定元素:
scrollBehavior (to, from, savedPosition) { if (to.hash) { return { selector: to.hash, behavior: 'smooth', } } else { return { x: 0, y: 0 } } }
这个用法会根据路由的 hash 值滚动到对应的元素。比如,如果路由是
/about#team
,它会滚动到 id 为team
的元素。 -
异步滚动:
scrollBehavior (to, from, savedPosition) { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ x: 0, y: 100 }) }, 500) }) }
这个用法会在 500 毫秒后滚动到
x: 0, y: 100
的位置。 -
根据 meta 信息动态滚动
// 路由配置 const routes = [ { path: '/page1', component: Page1, meta: { scrollToTop: true } }, { path: '/page2', component: Page2, meta: { scrollToElement: '#section2' } } ] // scrollBehavior 函数 scrollBehavior (to, from, savedPosition) { if (to.meta.scrollToTop) { return { x: 0, y: 0 } } if (to.meta.scrollToElement) { return { selector: to.meta.scrollToElement, behavior: 'smooth' } } return savedPosition || { x: 0, y: 0 } }
这种方式可以根据路由元信息动态地设置滚动行为,非常灵活。
五、注意事项:
scrollBehavior
函数是在路由切换后执行的,所以页面可能已经渲染完毕。- 如果你的页面使用了异步组件,可能需要在
scrollBehavior
函数中使用 Promise 来等待组件加载完成。 savedPosition
只在使用popstate
导航时可用。- 如果你的
scrollBehavior
函数返回null
或undefined
,则不会执行滚动操作。 behavior: 'smooth'
属性的兼容性可能存在问题,需要根据你的目标浏览器进行测试。
六、总结:
scrollBehavior
是 Vue Router 提供的一个非常强大的工具,可以让你轻松控制路由切换时的页面滚动行为。通过深入理解 scrollBehavior
的配置方式和源码实现,你可以更好地利用它来提升用户体验。希望今天的讲解能够帮助你更好地掌握这个神奇的小精灵,让你的 Vue 应用更加出色!
最后的彩蛋:一个高级用法示例
假设你的应用有一个侧边栏,点击侧边栏的链接会切换到不同的内容区域。你希望在切换内容区域时,页面滚动到对应的标题位置,并且使用平滑滚动效果。同时,你也希望记住用户的滚动位置,以便在点击浏览器的后退/前进按钮时能够恢复之前的滚动状态。
<template>
<div class="app">
<aside>
<ul>
<li><router-link to="/section1">Section 1</router-link></li>
<li><router-link to="/section2">Section 2</router-link></li>
<li><router-link to="/section3">Section 3</router-link></li>
</ul>
</aside>
<main>
<section id="section1">
<h2>Section 1</h2>
<p>...</p>
</section>
<section id="section2">
<h2>Section 2</h2>
<p>...</p>
</section>
<section id="section3">
<h2>Section 3</h2>
<p>...</p>
</section>
</main>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
.app {
display: flex;
}
aside {
width: 200px;
padding: 20px;
}
main {
flex: 1;
padding: 20px;
}
section {
margin-bottom: 50px;
}
</style>
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
Vue.use(VueRouter)
const routes = [
{ path: '/section1', component: App },
{ path: '/section2', component: App },
{ path: '/section3', component: App }
]
const router = new VueRouter({
routes,
mode: 'history',
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth'
}
}
if (savedPosition) {
return savedPosition
}
return { x: 0, y: 0 }
}
})
export default router
这个例子结合了 hash 锚点滚动和记忆滚动位置的功能,可以提供非常流畅的用户体验。
希望这个彩蛋能让你对 scrollBehavior
的应用有更深入的理解!
好了,今天的讲座就到这里。希望大家有所收获,下次再见!