内容简介: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 进行状态管理的完整项目,包含了用户的登录退出,路由页面,滚动位置还原,帖子编辑状态保存等等,麻雀虽小,却是五脏俱全。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 深度备份还原工具 V1.0 正式发布 — 备份还原,安全可靠
- 使用 nltk 词形还原
- Antsword流量分析与还原
- Redis安全以及备份还原
- Oracle RMAN备份与还原
- 学习bootstrap和jquery框架还原网页
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Base64 编码/解码
Base64 编码/解码
正则表达式在线测试
正则表达式在线测试