瀏覽器對用戶訪問網頁的記錄
- 在聊如何管理
vue
組件滾動行為之前诵冒,先簡單說說(畢竟深入了我也很模糊o(╯□╰)o)瀏覽器是如何對用戶訪問過的頁面的保持劝篷,瀏覽器歷史記錄是對用戶所訪問的頁面按時間順序進行的記錄和保存,以上是MDN對瀏覽器就如何跟蹤用戶訪問過網頁的解釋性說明。 - 通常我們很少會對頁面回退或前進進行操作,在瀏覽器用戶界面上提供有前進崖疤、回退按鈕嗜闻,頁面跳轉到離開頁面之前的位置蜕依,而不是重新刷新頁面,這個功能是由瀏覽器引擎(與渲染引擎琉雳、解析引擎概念不同)來完成的样眠。當用戶進入一個頁面的時候,會往 history 棧中放入當前的記錄翠肘,對頁面級別的操作通過操作內置對象
history
可以滿足一些需求檐束。
vue對訪問記錄的管理
- 進入正題,
vue
路由跳轉就是通過對history.pushState()
和history.replaceState()
方法的模擬來實現束倍,會往history
棧中存放一條記錄被丧,這也是為什么vue
的router.push
方法只能在支持history.pushState()
方法的瀏覽器中使用,當調用router.go()
或者router.back()
方法的時候就和history.go()
肌幽、history.back()
效果一樣晚碾,都是對history
棧中的記錄進行訪問,上述行為與通過瀏覽器的回退和前進效果也是一樣喂急。
但是格嘁,在不加處理的情況下,組件的滾動行為會跟我們想象的不同廊移。
vue組件滾動行為
- 設置三個路由
/home
糕簿、/list
探入、/about
,即對應三個不同的組件懂诗,
<ul class="tab">
<li>
<router-link to="/" >首頁</router-link>
</li>
<li>
<router-link to="/list" >列表</router-link>
</li>
<li>
<router-link to="/about" >關于</router-link>
</li>
<li>
<a href="#" @click='() => { this.$router.back() }'>點擊回退</a>
</li>
</ul>
<router-view></router-view>
- 每個組件的結構都是
ul>li
的結構
<!-- 以 home 組件為例 -->
<ul class="list_content home">
<li v-for='i in 10'>{{ i }}</li>
</ul>
長這樣:
請注意蜂嗽,現在開始滾動首頁位置至第 5 屏的位置,當切換到列表以及關于頁面的時候殃恒,會發(fā)現這兩個頁面的滾動行為和首頁滾動行為一致植旧。
既不涉及組件的緩存,也不涉及組件的復用离唐,我們不禁會疑惑為什么首頁的滾動會影響到其他兩個頁面病附,如果我們有當切換組件的時候,需要讓當前組件的內容是從
scrollTop = 0
的時候開始瀏覽亥鬓,那這樣的結果將會是一個絆腳石完沪。原因如下,因為基于SPA模式開發(fā)嵌戈,所以頁面僅有一個覆积,實現頁面切換是利用哈希與組件的映射關系,
vue-router
是通過哈希來模擬完整的 url
熟呛,但是對于頁面來說仍是一個 url
宽档,所以在任何一個組件滾動頁面,切換到其他組件的時候惰拱,頁面仍保持滾動之前的狀態(tài)雌贱,這就是出現上述現象的原因。
如何管理組件的滾動行為
- 如果你是想簡單粗暴的在每次切換組件的時候讓頁面回到頂部偿短,
router.beforeEach()
導航守衛(wèi)會是一個不錯的選擇:
router.beforeEach((to, from, next) => {
// 讓頁面回到頂部
document.documentElement.scrollTop = 0
// 一定不要忘記調用 next()
next()
})
但這不是我們的主題,要借助 vue-router
提供的 scrollBehavior
馋没,來管理組件滾動行為昔逗。
- 關于
scrollBehavior
,這里貼出官網對概念的介紹 傳送門篷朵,當然勾怒,借助scrollBehavior
,你也能讓頁面在組件切換的時候回到頂部:
const scrollBehavior = function (to, from, savedPosition) {
// savedPosition 會在你使用瀏覽器前進或后退按鈕時候生效
// 這個跟你使用 router.go() 或 router.back() 效果一致
// 這也是為什么我在 tab 欄結構中放入了一個 點擊回退 的按鈕
if (savedPosition) {
return savedPosition
} else {
// 如果不是通過上述行為切換組件声旺,就會讓頁面回到頂部
return {x: 0, y: 0}
}
}
- 上述會定制所有組件的滾動行為笔链,但有時候我們希望,當用戶在瀏覽
home
頁面到底部的時候腮猖,跳轉到list
頁面瀏覽鉴扫,當瀏覽到中間的時候,跳轉到about
頁面瀏覽澈缺,當用戶每次回退的時候坪创,都希望保持離開之前頁面的狀態(tài)炕婶,即:從about
回退到list
頁面的時候,頁面仍是在中間莱预,回退到home
頁面的時候柠掂,仍是在底部,這就需要我們個性化定制依沮。
定制不同組件的scrollBehavior
這里用到路由的元信息 meta
更細顆粒度控制滾動行為涯贞,這里以 home
組件為例說明:
const Home = {
template: `
<ul class="list_content home">
<li v-for='i in 10'>{{ i }}</li>
</ul>
`,
data () {
return {
timerId: ''
}
},
mounted () {
// 通過 addEventListener 方法注冊事件的時候需要格外小心
// 如果在 destroyed 鉤子函數中沒有銷毀 scroll 事件
// 在激活 home 組件的時候會再次綁定 scroll 事件
// window.addEventListener('scroll', this.justifyPos)
// 通過 on 方式綁定事件能夠有效避免上述情況
window.onscroll = this.justifyPos
},
methods: {
justifyPos () {
// 節(jié)流;
if (this.timerId) clearTimeout(this.timerId)
this.timerId = setTimeout(() => {
// 獲取頁面滾動距離之后設置給當前路由的 元信息
this.$route.meta.y = window.pageYOffset
}, 300)
}
},
destroyed () {
// 當組件銷毀的時候危喉,移除滾動行為監(jiān)聽, 清空定時器肩狂;
// 該方法是綁定到 window 身上,即使跳轉到其他組件姥饰,仍然會監(jiān)聽頁面的滾動行為
// window.removeEventListener('scroll', this.justifyPos)
// clearTimeout(this.timerId)
}
}
const List = {
template: `
<ul class="list_content list">
<li v-for='i in 10'>{{ i }}</li>
</ul>
`
}
const About = {
template: `
<ul class="list_content about">
<li v-for='i in 10'>{{ i }}</li>
</ul>
`
}
const routes = [
// 設置 meta傻谁,細顆粒控制組件滾動
{path: '/', component: Home, meta: {x: 0, y: 0}},
{path: '/list', component: List, meta: {x: 0, y: 0}},
{path: '/about', component: About, meta: {x: 0, y: 0}}
]
const scrollBehavior = function (to, from, savedPosition) {
return to.meta
}
const router = new VueRouter({
routes,
scrollBehavior,
linkExactActiveClass: 'current'
})
上述會在 home
組件滾動停止的時候記錄當前組件的滾動位置信息列粪,并且存儲到對應 home
組件的路由 meta
這個對象中审磁,當切換到 list
或者 about
頁面之后在回到 home
組件,仍會保留著離開之前的位置岂座,而不是簡單地讓頁面回到頂部态蒂。
但是,你會發(fā)現你只是針對 home
組件的滾動行為進行控制费什,list
和 about
組件的滾動行為也能夠實現個性化定制钾恢,即也會將當前組件的滾動行為記錄在對應的路由 meta
中。
這會讓人疑惑鸳址,因為在 list
和 about
組件中并沒有設置 justifyPos
方法瘩蚪,并且 window.onscroll = this.justifyPos
將 this
綁定到當前的上下文中。
vue
官網對于組件銷毀介紹稿黍,會解綁所有的指令以及事件監(jiān)聽疹瘦,但是對于方法的引用處理沒有提到,個人覺得在這里應該拋出警告或者錯誤的巡球,但是 vue
卻沒有提示言沐,這也是令我困惑的一點。但是酣栈,這卻為滾動行為監(jiān)聽提供了更好的處理方法险胰,那就是綁定到 vue
根實例上,而不是某一個單一組件上矿筝,因為 this
會自動綁定到當前上下文:
new Vue({
router,
data: {
timerId: ''
},
mounted () {
window.addEventListener('scroll', this.justifyPos)
},
methods: {
justifyPos () {
if (this.timerId) clearTimeout(this.timerId)
this.timerId = setTimeout(() => {
this.$route.meta.y = window.pageYOffset
}, 300)
}
}
}).$mount('#app')
當better-scroll(以下簡稱bs)遇上vue起便,如何定制滾動行為
- 貼上傳送門 better-scroll,感興趣的可以看一下。
- 之所以會談到
bs
缨睡,如果在項目中用到該插件鸟悴,那么頁面滾動行為跟組件滾動行為是完全不一樣的,這是因為bs
特殊的結構要求奖年,父容器需要有個固定的高度细诸,所有的滾動行為是由子元素來產生的,在移動端應用bs
陋守,通常會將父容器的高度設置為屏幕的高度震贵,你的所有應用都應該放到這個父容器內。bs
在移動端性能很出色水评,但是這卻為組件個性化定制scrollBehavior
帶來了一些小麻煩猩系。 - 原因就是應用
bs
插件的組件,一般會設置高度和屏幕高度一致中燥,這樣即使通過meta
來設置滾動記錄寇甸,在vue-router
的scrollBehavior
中返回meta
也沒有用處,因為高度是定死了疗涉,就不存在滾動拿霉,你所看到的滾動式是bs
插件所處理的。 - 這時候咱扣,就需要用到
bs
提供的一些事件和方法了绽淘,仍以 home 組件為例說明,看代碼:
const Home = {
template: `
<div class="wrapper" ref="wrapper">
<ul class="list_content home">
<li v-for='i in 10' @click='goList'>{{ i }}</li>
</ul>
</div>
`,
mounted () {
this.$nextTick(() => {
// 初始化 BS
this._initScroll()
// 滾動監(jiān)聽
this.scroll.on('scrollEnd', (pos) => {
// 將滾動信息設置給當前路由元信息
this.$route.meta.y = pos.y
})
// 當前組件激活的時候闹伪,滾動到離開前位置
// 如果你想要滾動動畫效果沪铭,可以在 scrollTo 方法中自定義
this.scroll.scrollTo(0, this.$route.meta.y, 0)
})
},
methods: {
_initScroll () {
if (!this.$refs.wrapper) return
this.scroll = new BScroll(this.$refs.wrapper, {
mouseWheel: {
speed: 20,
invert: false,
easeTime: 300
},
// 派發(fā) click 事件;
click: true
})
},
// 跳轉到列表頁偏瓤;
goList () {
this.$router.push({name: 'list'})
}
}
}
const List = {
template: `
<ul class="list_content list">
<li v-for='i in 10' @click='goHome'>{{ i }}</li>
</ul>
`,
methods: {
// 回跳到首頁
goHome () {
this.$router.push({name: 'home'})
}
}
}
const routes = [
// 設置 meta
{path: '/', name: 'home', component: Home, meta: {x: 0, y: 0}},
{path: '/list', name: 'list', component: List, meta: {x: 0, y: 0}},
{path: '/about', component: About, meta: {x: 0, y: 0}}
]
// scrollBehavior 其實這里已經沒有什么作用了杀怠,因為當前組件的高度被定死和整個屏幕一樣高
// const scrollBehavior = function (to, from, savedPosition) {
// return to.meta
// }
// 設置路由
const router = new VueRouter({
routes,
scrollBehavior,
linkExactActiveClass: 'current'
})
// 掛載
new Vue({router}).$mount('#app')
- 通過
bs
提供的事件以及方法再結合路由的meta
,也能夠實現細顆粒度控制滾動硼补,如果對組件使用了keep-alive
驮肉,你應該在每次切換到該組件的時候在activated
鉤子函數中初始化bs
、scrollEnd
事件以及scrollTo
方法已骇;如果你頁面有分頁的功能,你可能需要在分頁邊界花費一些心思如何讓滾動行為跨越分頁票编,這里建議是使用組件緩存褪储,關于組件如何清除緩存,可以參考我的另一篇文章 組件去緩存慧域,當然如果你有更好的處理方式鲤竹,也可以留言。
寫在最后
- 上文中有個
節(jié)流
的概念,這里貼出參考文獻辛藻,有興趣的可以瀏覽一下碘橘,節(jié)流
和防抖
的不同在哪:節(jié)流與防抖 - 本文為原創(chuàng)文章,如果需要轉載吱肌,請注明出處痘拆,方便溯源,如有錯誤地方氮墨,可以在下方留言纺蛆,歡迎校勘规揪,源碼已上傳到我的GitHub桥氏。