Vue.js项目重构,轻松实现上拉加载滚动位置还原

栏目: 编程语言 · 发布时间: 7年前

内容简介:Vue.js项目重构,轻松实现上拉加载滚动位置还原

前言

上一篇《 Vue.js轻松实现页面后退时,还原滚动位置 》只是简单的实现了路由切换时进行的滚动位置还原,很多朋友就来问上拉加载怎么实现啊!于是我想起了以前做过一个叫 vue-cnode 的项目,于是花了两天时间进行了重构,完全的移除了Vuex,使用了 Vuet 来做为状态的管理工具。如果关注 Vuet 的朋友就会发现,版本更新得好快,简直就是版本帝啊!!!其实 Vuet 的版本升级,都是向下兼容的,每次的版本发布都会经过完整的单元测试和e2e测试,极大的保证了发布版本的稳定性。

项目源码

需求分析

  • 记录上拉请求时的页数
  • 页面后退时,还原之前列表页面的状态
  • 列表分类切换时,进行状态重置
  • 从列表A点击详情A,页面后退,重新打开详情A,还原之前访问详情A状态
  • 从列表A点击详情A,页面后退,重新打开详情B,清除详情A的状态,初始化详情B的状态

安装

npm install --save vuet

Vuet实例

import Vue from 'vue'
import Vuet from 'vuet'
import utils from 'utils'
import http from 'http'

Vue.use(Vuet)

export default new Vuet({
  pathJoin: '-', // 定义模块的连接符
  modules: {
    topic: {
      create: {
        data () {
          return {
            title: '', // 标题
            tab: '', // 发表的板块
            content: '' // 发表的内容
          }
        },
        manuals: {
          async create ({ state }) {
            if (!state.title) {
              return utils.toast('标题不能为空')
            } else if (!state.tab) {
              return utils.toast('选项不能为空')
            } else if (!state.content) {
              return utils.toast('内容不能为空')
            }
            const res = await http.post(`/topics`, {
              ...state
            })
            if (res.success) {
              this.reset()
            } else {
              utils.toast(res.error_msg)
            }
            return res
          }
        }
      },
      /********* 实现列表上拉加载滚动位置还原的核心代码开始 *************/
      list: {
        data () {
          return {
            data: [], // 列表存储的数据
            loading: true, // 数据正在加载中
            done: false, // 数据是否已经全部加载完成
            page: 1 // 加载的页数
          }
        },
        async fetch ({ state, route, params, path }) {
          // 注,在vuet 0.1.2以上版本,会多带一个params.routeWatch参数,我们可以根据这个来判断页面是否发生了变化
          if (params.routeWatch === true) { // 路由发生了变化,重置模块状态
            this.reset(path)
          } else if (params.routeWatch === false) { // 路由没有变化触发的请求,可能是从详情返回到列表
            return {}
          }
          // params.routeWatch 没有参数,则是上拉加载触发的调用
          const { tab = '' } = route.query
          const query = {
            tab,
            mdrender: false,
            limit: 20,
            page: state.page
          }
          const res = await http.get('/topics', query)
          const data = params.routeWatch ? res.data : [...state.data, ...res.data]
          return {
            data, // 更新模块的列表数据
            page: ++state.page, // 每次请求成功后,页数+1
            loading: false, // 数据加载完成
            done: res.data.length < 20 // 判断列表的页数是否全部加载完成
          }
        }
      },
      /********* 实现列表上拉加载滚动位置还原的核心代码结束 *************/
      detail: {
        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
            },
            existence: true,
            loading: true,
            commentId: null
          }
        },
        async fetch ({ route }) {
          const { data } = await http.get(`/topic/${route.params.id}`)
          if (data) {
            return {
              data,
              loading: false
            }
          }
          return {
            existence: false,
            loading: false
          }
        }
      }
    },
    user: { // 登录用户的模块
      self: {
        data () {
          return {
            data: JSON.parse(localStorage.getItem('vue_cnode_self')) || {
              avatar_url: null,
              id: null,
              loginname: null,
              success: false
            }
          }
        },
        manuals: {
          async login ({ state }, accesstoken) { // 用户登录方法
            const res = await http.post(`/accesstoken`, { accesstoken })
            if (typeof res === 'object' && res.success) {
              state.data = res
              localStorage.setItem('vue_cnode_self', JSON.stringify(res))
              localStorage.setItem('vue_cnode_accesstoken', accesstoken)
            }
            return res
          },
          signout () { // 用户退出方法
            localStorage.removeItem('vue_cnode_self')
            localStorage.removeItem('vue_cnode_accesstoken')
            this.reset()
          }
        }
      },
      detail: {
        data () {
          return {
            data: {
              loginname: null,
              avatar_url: null,
              githubUsername: null,
              create_at: null,
              score: 0,
              recent_topics: [],
              recent_replies: []
            },
            existence: true,
            loading: true,
            tabIndex: 0
          }
        },
        async fetch ({ route }) {
          const { data } = await http.get(`/user/${route.params.username}`)
          if (data) {
            return {
              data,
              loading: false
            }
          }
          return {
            existence: false,
            loading: false
          }
        }
      },
      messages: {
        data () {
          return {
            data: {
              has_read_messages: [],
              hasnot_read_messages: []
            },
            loading: true
          }
        },
        async fetch () {
            // 用户未登录,拦截请求
          if (!this.getState('user-self').data.id) return
          const { data } = await http.get(`/messages`, { mdrender: true })
          return {
            data
          }
        },
        count: {
          data () {
            return {
              data: 0
            }
          },
          async fetch () {
            // 用户未登录,拦截请求
            if (!this.getState('user-self').data.id) return
            const res = await http.get('/message/count')
            if (!res.data) return
            return {
              data: res.data
            }
          }
        }
      }
    }
  }
})

Vuet 实例创建完成后,我们就可以在组件中连接我们的 Vuet 了。

  • 首页列表

    <template>
    <div>
      <nav class="nav">
        <ul flex="box:mean">
    
          <li v-for="item in tabs" :class="{ active: item.tab === ($route.query.tab || '') }">
            <router-link :to="{ name: 'index', query: { tab: item.tab } }">{{ item.title }}</router-link>
          </li>
        </ul>
      </nav>
      <!-- 
          注意了,由于我的页面布局是一个局部滚动条,所以需要指定一个name
          如果你的页面是全局滚动条,设置指令为
          v-route-scroll.window="{ path: 'topic-list' }"
      -->
      <v-content v-route-scroll="{ path: 'topic-list', name: 'content' }">
        <ul class="list">
          <li v-for="item in list.data" key="item.id">
            <router-link :to="{ name: 'topic-detail', params: { id: item.id } }">
              <div class="top" flex="box:first">
                <div class="headimg" :style="{ backgroundImage: 'url(' + item.author.avatar_url + ')' }"></div>
                <div class="box" flex="dir:top">
                  <strong>{{ item.author.loginname }}</strong>
                  <div flex>
                    <time>{{ item.create_at | formatDate }}</time>
                    <span class="tag">#分享#</span>
                  </div>
                </div>
              </div>
              <div class="common-typeicon" flex v-if="item.top || item.good">
                <div class="icon" v-if="item.good">
                  <i class="iconfont icon-topic-good"></i>
                </div>
                <div class="icon" v-if="item.top">
                  <i class="iconfont icon-topic-top"></i>
                </div>
              </div>
              <div class="tit">{{ item.title }}</div>
              <div class="expand" flex="box:mean">
                <div class="item click" flex="main:center cross:center">
                  <i class="iconfont icon-click"></i>
                  <div class="num">{{ item.visit_count > 0 ? item.visit_count : '暂无阅读' }}</div>
                </div>
                <div class="item reply" flex="main:center cross:center">
                  <i class="iconfont icon-comment"></i>
                  <div class="num">{{ item.reply_count > 0 ? item.reply_count : '暂无评论' }}</div>
                </div>
                <div class="item last-reply" flex="main:center cross:center">
                  <time class="time">{{ item.last_reply_at | formatDate }}</time>
                </div>
              </div>
            </router-link>
          </li>
        </ul>
        <v-loading :done="list.done" :loading="list.loading" @seeing="$vuet.fetch('topic-list')"></v-loading>
      </v-content>
      <v-footer></v-footer>
    </div>
    </template>
    <script>
    import { mapModules, mapRules } from 'vuet'
    
    export default {
      mixins: [
        mapModules({ list: 'topic-list' }), // 连接我们定义的Vuet.js的状态
        mapRules({ route: 'topic-list' }) // 使用Vuet.js内置的route规则来对页面数据和滚动位置进行管理
      ],
      data () {
        return {
          tabs: [
            {
              title: '全部',
              tab: ''
            },
            {
              title: '精华',
              tab: 'good'
            },
            {
              title: '分享',
              tab: 'share'
            },
            {
              title: '问答',
              tab: 'ask'
            },
            {
              title: '招聘',
              tab: 'job'
            }
          ]
        }
      }
    }
    </script>
  • 页面详情

<template>
  <div>
    <v-header title="主题">
      <div slot="left" class="item" flex="main:center cross:center" v-on:click="$router.go(-1)">
        <i class="iconfont icon-back"></i>
      </div>
    </v-header>
    <!--
        设置详情的局部滚动条
    -->
    <v-content style="bottom: 0;" v-route-scroll="{ path: 'topic-detail', name: 'content' }">
      <v-loading v-if="detail.loading"></v-loading>
      <v-data-null v-if="!detail.existence" msg="话题不存在"></v-data-null>
      <template v-if="!detail.loading && detail.existence">
        <div class="common-typeicon" flex v-if="data.top || data.good">
          <div class="icon" v-if="data.good">
            <i class="iconfont icon-topic-good"></i>
          </div>
          <div class="icon" v-if="data.top">
            <i class="iconfont icon-topic-top"></i>
          </div>
        </div>

        <ul class="re-list">
          <!-- 楼主信息 start -->
          <li flex="box:first">
            <div class="headimg">
              <router-link class="pic" :to="{ name: 'user-detail', params: { username: author.loginname } }" :style="{ backgroundImage: 'url(' + author.avatar_url + ')' }"></router-link>
            </div>
            <div class="bd">
              <div flex>
                <router-link flex-box="0" :to="{ name: 'user-detail', params: { username: author.loginname } }">{{ author.loginname }}</router-link>
                <time flex-box="1">{{ data.create_at | formatDate }}</time>
                <div flex-box="0" class="num">#楼主</div>
              </div>
            </div>
          </li>
          <!-- 楼主信息 end -->
          <!-- 主题信息 start -->
          <li>
            <div class="datas">
              <div class="tit">{{ data.title }}</div>
              <div class="bottom" flex="main:center">
                <div class="item click" flex="main:center cross:center">
                  <i class="iconfont icon-click"></i>
                  <div class="num">{{ data.visit_count }}</div>
                </div>
                <div class="item reply" flex="main:center cross:center">
                  <i class="iconfont icon-comment"></i>
                  <div class="num">{{ data.reply_count }}</div>
                </div>
              </div>
            </div>
            <div class="markdown-body" v-html="data.content"></div>
          </li>
          <!-- 主题信息 end -->
          <li class="replies-count" v-if="replies.length">
            共(<em>{{ replies.length }}</em>)条回复
          </li>
          <!-- 主题评论 start -->
          <li v-for="(item, $index) in replies">
            <div flex="box:first">
              <div class="headimg">
                <router-link class="pic" :to="{ name: 'user-detail', params: { username: item.author.loginname } }" :style="{ backgroundImage: 'url(' + item.author.avatar_url + ')' }"></router-link>
              </div>
              <div class="bd">
                <div flex>
                  <router-link flex-box="0" :to="{ name: 'user-detail', params: { username: item.author.loginname } }">{{ item.author.loginname }}</router-link>
                  <time flex-box="1">{{ item.create_at | formatDate }}</time>
                  <div flex-box="0" class="num">#{{ $index + 1 }}</div>
                </div>
                <div class="markdown-body" v-html="item.content"></div>
                <div class="bottom" flex="dir:right cross:center">
                  <div class="icon" @click="commentShow(item, $index)">
                    <i class="iconfont icon-comment-topic"></i>
                  </div>
                  <div class="icon" :class="{ fabulous: testThing(item.ups) }" v-if="item.author.loginname !== user.data.loginname" @click="fabulousItem(item)">
                    <i class="iconfont icon-comment-fabulous"></i>
                    <em v-if="item.ups.length">{{ item.ups.length }}</em>
                  </div>
                </div>
              </div>
            </div>
            <reply-box v-if="detail.commentId === item.id" :loginname="item.author.loginname" :replyId="item.id"></reply-box>
          </li>
          <!-- 主题评论 end -->
        </ul>
        <div class="reply" v-if="user.data.id">
          <reply-box @success="$vuet.fetch('topic-detail')"></reply-box>
        </div>
        <div class="tip-login" v-if="!user.data.id">
          你还未登录,请先
          <router-link to="/login">登录</router-link>
        </div>
      </template>
    </v-content>
  </div>
</template>
<script>
  import http from 'http'
  import replyBox from './reply-box'
  import { mapModules, mapRules } from 'vuet'

  export default {
    mixins: [
      // 连接详情和登录用户模块
      mapModules({ detail: 'topic-detail', user: 'user-self' }),
      // 一样是使用route规则对页面的数据进行管理
      mapRules({ route: 'topic-detail' })
    ],
    components: { replyBox },
    computed: {
      data () {
        return this.detail.data
      },
      author () {
        return this.detail.data.author
      },
      replies () {
        return this.detail.data.replies
      }
    },
    methods: {
      testThing (ups) { // 验证是否点赞
        return ups.indexOf(this.user.data.id || '') > -1
      },
      fabulousItem ({ ups, id }) { // 点赞
        if (!this.user.data.id) return this.$router.push('/login')
        var index = ups.indexOf(this.user.data.id)
        if (index > -1) {
          ups.splice(index, 1)
        } else {
          ups.push(this.user.data.id)
        }
        http.post(`/reply/${id}/ups`)
      },
      commentShow (item) { // 显示隐藏回复框
        if (!this.user.data.id) return this.$router.push('/login')
        this.detail.commentId = this.detail.commentId === item.id ? null : item.id
      }
    }
  }

</script>

总结

因为篇幅有限,所以只列出了列表和详情的代码,大家有兴趣深入的话,可以看下 vue-cnode 的代码。这是基于 Vuet 进行状态管理的完整项目,包含了用户的登录退出,路由页面,滚动位置还原,帖子编辑状态保存等等,麻雀虽小,却是五脏俱全。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

水平营销

水平营销

[美] 菲利普·科特勒、费尔南多・德・巴斯 / 陈燕茹 / 中信出版社 / 2005-1 / 25.00元

《水平营销》阐明了相对纵向营销而言的的水平营销的框架和理论。引入横向思维来作为发现新的营销创意的又一平台,旨在获得消费者不可能向营销研究人员要求或建议的点子。而这些点子将帮助企业在产品愈加同质和超竞争的市场中立于不败之地。 《水平营销》提到: 是什么创新过程导致加油站里开起了超市? 是什么创新过程导致取代外卖比萨服务的冷冻比萨的亮相? 是什么创新过程导致巧克力糖里冒出了玩具......一起来看看 《水平营销》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试