vue 组件的 scrollBehavior

浏览器对用户访问网页的记录
  • 在聊如何管理vue组件滚动行为之前,先简单说说(毕竟深入了我也很模糊o(╯□╰)o)浏览器是如何对用户访问过的页面的保持,浏览器历史记录是对用户所访问的页面按时间顺序进行的记录和保存,以上是MDN对浏览器就如何跟踪用户访问过网页的解释性说明。
  • 通常我们很少会对页面回退或前进进行操作,在浏览器用户界面上提供有前进、回退按钮,页面跳转到离开页面之前的位置,而不是重新刷新页面,这个功能是由浏览器引擎(与渲染引擎、解析引擎概念不同)来完成的。当用户进入一个页面的时候,会往 history 栈中放入当前的记录,对页面级别的操作通过操作内置对象 history 可以满足一些需求。
vue对访问记录的管理
  • 进入正题,vue 路由跳转就是通过对 history.pushState()history.replaceState() 方法的模拟来实现,会往 history 栈中存放一条记录,这也是为什么 vuerouter.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 屏的位置,当切换到列表以及关于页面的时候,会发现这两个页面的滚动行为和首页滚动行为一致。


既不涉及组件的缓存,也不涉及组件的复用,我们不禁会疑惑为什么首页的滚动会影响到其他两个页面,如果我们有当切换组件的时候,需要让当前组件的内容是从 scrollTop = 0 的时候开始浏览,那这样的结果将会是一个绊脚石。
原因如下,因为基于SPA模式开发,所以页面仅有一个,实现页面切换是利用哈希组件的映射关系,vue-router 是通过哈希来模拟完整的 url,但是对于页面来说仍是一个 url,所以在任何一个组件滚动页面,切换到其他组件的时候,页面仍保持滚动之前的状态,这就是出现上述现象的原因。

如何管理组件的滚动行为
  • 如果你是想简单粗暴的在每次切换组件的时候让页面回到顶部,router.beforeEach() 导航守卫会是一个不错的选择:
    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 页面浏览,当用户每次回退的时候,都希望保持离开之前页面的状态,即:从 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 () {
                    // 节流;
                    if (this.timerId) clearTimeout(this.timerId)
                    this.timerId = setTimeout(() => {
                        // 获取页面滚动距离之后设置给当前路由的 元信息
                        this.$route.meta.y = window.pageYOffset
                    }, 300)
                }
            },
          destroyed () {
                // 当组件销毁的时候,移除滚动行为监听, 清空定时器;
                // 该方法是绑定到 window 身上,即使跳转到其他组件,仍然会监听页面的滚动行为
                // 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 组件,仍会保留着离开之前的位置,而不是简单地让页面回到顶部。
但是,你会发现你只是针对 home 组件的滚动行为进行控制,listabout 组件的滚动行为也能够实现个性化定制,即也会将当前组件的滚动行为记录在对应的路由 meta 中。
这会让人疑惑,因为在 listabout 组件中并没有设置 justifyPos 方法,并且 window.onscroll = this.justifyPosthis 绑定到当前的上下文中。
vue 官网对于组件销毁介绍,会解绑所有的指令以及事件监听,但是对于方法的引用处理没有提到,个人觉得在这里应该抛出警告或者错误的,但是 vue 却没有提示,这也是令我困惑的一点。但是,这却为滚动行为监听提供了更好的处理方法,那就是绑定到 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-routerscrollBehavior 中返回 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()
                    // 滚动监听
                    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
                        },
                        // 派发 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 钩子函数中初始化 bsscrollEnd 事件以及 scrollTo 方法;如果你页面有分页的功能,你可能需要在分页边界花费一些心思如何让滚动行为跨越分页,这里建议是使用组件缓存,关于组件如何清除缓存,可以参考我的另一篇文章 组件去缓存,当然如果你有更好的处理方式,也可以留言。
写在最后
  • 上文中有个 节流 的概念,这里贴出参考文献,有兴趣的可以浏览一下,节流防抖 的不同在哪:节流与防抖
  • 本文为原创文章,如果需要转载,请注明出处,方便溯源,如有错误地方,可以在下方留言,欢迎校勘,源码已上传到我的GitHub
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容