V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
1340641314
V2EX  ›  分享创造

Vue.js 轻松实现页面后退时,还原滚动位置

  •  
  •   1340641314 ·
    lzxb · 2017-06-12 07:14:29 +08:00 · 6283 次点击
    这是一个创建于 1891 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    Vue.js 2.x发布之后,陆陆续续做了七八个项目,摸索出来了一套自己的状态管理模式,我将之称为Vuet。它以规则来驱动状态更新,它带来的是开发效率上的飙升,它就像草原,而你是野马,任你随意驰骋,总之它是为敏捷开发而诞生。

    缘由

    在大型的 Vue 应用程序开发中,多组件通信、多页面通信,往往是跨不过的坎,一个页面组件中往往参杂着页面获取数据的代码和响应用户操作的代码,稍有不慎,就使得代码混乱不堪。A、B、C 三个页面中,都需要同样的数据,然后每一个页面都写一次、发送一次请求,不久之后,代码就十分臃肿了。因此我们就需要vuex这样的第三方库来管理状态了

    Vuet 诞生初衷

    从列表点击进去到详情,从详情返回后,我们期望能显示回原来的位置,而不是整个页面重新初始化,重新请求数据,这样带来的是用户体验的极度糟糕的,我们期望能有一种规则来定义状态应该如何更新,这便是Vuet.js诞生的初衷。它以规则来定义状态的更新,它也是一种 Vue.js 全新的状态管理模式。天生的规则驱动,使得本次教程的主题,也将变得异常简单,因为我们只需要定义好页面更新的规则即可实现。

    有了 Vuex 还需要 Vuet 做什么?

    Vuex 和 Vuet 的出发点不一样,Vuex 不建议直接更新状态,而是通过提交mutation来更新状态,而 Vuet 则是允许的。因此 Vuex 和 Vuet 是可以配合使用的,并且有着不同的应用场景,该用Vuex 的地方就用 Vuex,可用Vuet 的地方,就可以使用 Vuet

    开始

    上面废话了那么久,也是因为Vuet.js才刚刚诞生,急需大家的支持。嗯,接下来我们开始本次的主题!

    目录结构

    |-- pages                 // 页面组件
    |   |-- topic             // 主题模块
    |       |-- Detail.vue    // 主题详情
    |       |-- List.vue      // 主题列表
    |-- router                // router 相关
    |   |-- index.js          // 入口文件
    |   |-- router.js         // 实例化 VueRouter
    |-- vuet                  // vuet 相关
    |   |-- index.js          // 入口文件
    |   |-- topic-detail.js   // 主题详情的状态
    |   |-- topic-list.js     // 主题列表的状态
    |   |-- vuet.js           // 实例化 Vuet
    |- index.html             // 程序页面入口文件
    |- main.js                // Vue 实例化入口文件
    

    上面是我们本次项目的基本目录结构

    安装模块

    npm install vue vue-router vuet --save
    

    这些都是基本的模块,想必不用多说,大家都知道的。

    route 规则

    先给出官方文档地址 本章的主题,核心就是在route规则身上,它能帮你获取、更新、重置页面的状态,配合v-route-scroll指令就能帮你处理页面的全局滚动条和 div 元素自身的滚动条

    code 社区 api 为例子

    • main.js
        import Vue from 'vue'
        import router from './router/'
        import vuet from './vuet/'
      
        export default new Vue({
          el: '#app',
          vuet,
          router,
          render (h) {
            return h('router-view')
          }
        })
      
    • vuet/index.js
        import vuet from './vuet'
      
        export default vuet
      
      
    • vuet/vuet.js
        import Vue from 'vue'
        import Vuet from 'vuet'
        import topicList from './topic-list'
        import topicDetail from './topic-detail'
      
        Vue.use(Vuet)
      
        const vuet = new Vuet({
          data () {
            return {
              loading: true, // 请求中
              loaderr: false // 请求失败
            }
          },
          pathJoin: '-', // 父子模块的连接路径
          modules: {
            topic: {
              list: topicList,
              detail: topicDetail
            }
          }
        })
      
        vuet.beforeEach(({ path, params, state }) => {
          state.loading = true
          state.loaderr = false
        })
      
        vuet.afterEach((err, { path, params, state }) => {
          state.loading = false
          state.loaderr = !!err
        })
      
        export default vuet
      
      
    • vuet/topic-list.js
        export default {
          routeWatch: 'query', // 定义页面的更新规则
          data () {
            return {
              data: [],
              tabs: [
                {
                  label: '全部',
                  value: 'all'
                },
                {
                  label: '精华',
                  value: 'good'
                },
                {
                  label: '分享',
                  value: 'share'
                },
                {
                  label: '问答',
                  value: 'ask'
                },
                {
                  label: '招聘',
                  value: 'job'
                }
              ]
            }
          },
          async fetch ({ route }) {
            const { tab = '' } = route.query
            const { data } = await window.fetch(`https://cnodejs.org/api/v1/topics?mdrender=false&tab=${tab}`).then(response => response.json())
            return {
              data
            }
          }
        }
      
      
    • vuet/topic-detail.js
        export default {
          routeWatch: 'params.id', // 定义页面的更新规则
          data () {
            return {
              data: {
                id: null,
                author_id: null,
                tab: null,
                content: null,
                title: null,
                last_reply_at: null,
                good: false,
                top: false,
                reply_count: 0,
                visit_count: 0,
                create_at: null,
                author: {
                  loginname: null,
                  avatar_url: null
                },
                replies: [],
                is_collect: false
              }
            }
          },
          async fetch ({ route }) {
            const { data } = await window.fetch(`https://cnodejs.org/api/v1/topic/${route.params.id}`).then(response => response.json())
            return {
              data
            }
          }
        }
      
      
    • router/index.js
        import router from './router'
      
        export default router
      
      
    • router/router.js
        import Vue from 'vue'
        import VueRouter from 'vue-router'
        import TopicList from '../pages/topic/List'
        import TopicDetail from '../pages/topic/Detail'
      
        Vue.use(VueRouter)
      
        const RouterView = {
          render (h) {
            return h('router-view')
          }
        }
      
        const router = new VueRouter({
          routes: [
            {
              path: '/',
              component: RouterView,
              children: [
                {
                  path: '',
                  name: 'topic-list',
                  component: TopicList
                },
                {
                  path: '/:id',
                  name: 'topic-detail',
                  component: TopicDetail
                }
              ]
            }
          ]
        })
      
        export default router
      
      
    • pages/topic/List.vue
      <template>
        <!-- 
            设置指令监听全局滚动条,
            注意了,光是设置指令可不行,还需要在组件中使用 route 规则,
            来处理页面滚动的操作,
            局部滚动条直接去掉.window 即可
            如果需要同时记录全局滚动条和 div 滚动条直接设置.window.self 即可
            它能做到 N 多个滚动位置记录,具体看官方文档喔!
            注:记录 div 滚动的话,需要设置一个 name 来识别
            v-route-scroll="{ path: 'topic-detail', name: 'xxx' }"
        -->
        <div v-route-scroll.window="{ path: 'topic-list' }">
          <header>
            <ul>
              <li v-for="item in list.tabs">
                <router-link :to="{ name: 'topic-list', query: { tab: item.value } }">{{ item.label }}</router-link>
              </li>
            </ul>
          </header>
          <ul class="list">
            <li v-for="item in list.data">
                <router-link :to="{ name: 'topic-detail', params: { id: item.id } }">{{ item.title }}</router-link>
            </li>
          </ul>
        </div>
      </template>
      <script>
        import { mapRules, mapModules } from 'vuet'
      
        export default {
          mixins: [
            // 设置模块的更新规则
            mapRules({
              route: 'topic-list'
            }),
            // 连接模块的状态
            mapModules({
              list: 'topic-list'
            })
          ]
        }
      </script>
      <style scoped>
      
      </style>
    
    • pages/topic/Detail.vue
      <template>
        <div v-route-scroll.window="{ path: 'topic-detail' }">
          <h3>{{ detail.data.title }}</h3>
          <div v-html="detail.data.content"></div>
        </div>  
      </template>
      <script>
        import { mapRules, mapModules } from 'vuet'
      
        export default {
          mixins: [
            // 设置模块的更新规则
            mapRules({
              route: 'topic-detail'
            }),
            // 连接模块的状态
            mapModules({
              detail: 'topic-detail'
            })
          ]
        }
      </script>
      <style scoped>
      
      </style>
    

    总结

    咋的一看,Vuet 看起来也不是很复杂,只需要定义好模块状态,然后在组件中设置对应的规则来更新模块的状态即可。其实 vuet 自带的 route 规则能够支持同时记录全局滚动条、div 自身的滚动条,这样就能大大的提升了我们的用户体验

    13 条回复    2017-09-09 21:26:42 +08:00
    yu7er
        1
    yu7er  
       2017-06-12 08:01:46 +08:00 via Android
    不错不错,vuex 应该是官方库。。
    1340641314
        2
    1340641314  
    OP
       2017-06-12 08:21:33 +08:00
    @yu7er 是的
    dnxbf321
        3
    dnxbf321  
       2017-06-12 09:37:22 +08:00
    可以用 vue-router 和 vuet 配合?
    1340641314
        4
    1340641314  
    OP
       2017-06-12 09:57:21 +08:00
    @dnxbf321 可以和 vue 的全家桶配合使用,和 vue-router 配合,才能发挥 vuet 的全部潜力
    del1214
        5
    del1214  
       2017-06-12 11:21:47 +08:00
    自己也实现过一次,不过远没有楼主思考的深入,已 star
    123s
        6
    123s  
       2017-06-12 11:54:36 +08:00
    单是你这个需求的话应用不大,刚刚才习惯 vuex 那一套,不想再搞新的。
    1340641314
        7
    1340641314  
    OP
       2017-06-12 11:59:06 +08:00
    1340641314
        8
    1340641314  
    OP
       2017-06-12 12:01:07 +08:00
    @123s 这个其实只是 vuet 中的一小部分功能,它是可以很好的和 vuex 来配合的,具体的话,就需要自己来权衡了。近期有时间的话,会陆陆续续发布一些相关的教程
    123s
        9
    123s  
       2017-06-12 13:05:41 +08:00
    @1340641314 必须有教程,现在前端一天就一个新的概念,累
    natforum
        10
    natforum  
       2017-06-12 13:57:14 +08:00
    贝爷打野归来已成大佬,而我还在原地踏步
    1340641314
        11
    1340641314  
    OP
       2017-06-12 14:00:38 +08:00
    @123s 确实是,其实我也是因为一直没有找到合适的状态管理模式,所以才自己造了一套,希望能减轻开发的负担
    1340641314
        12
    1340641314  
    OP
       2017-06-12 14:00:54 +08:00
    @natforum 哈哈,其实我也只是个小菜逼
    shuimuyian
        13
    shuimuyian  
       2017-09-09 21:26:42 +08:00
    楼主是 "狼族小贝" 吗
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1209 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 137ms · UTC 22:35 · PVG 06:35 · LAX 15:35 · JFK 18:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.