Vue.js 项目重构,轻松实现上拉加载滚动位置还原 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
1340641314
V2EX    分享创造

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

  •  
  •   1340641314
    lzxb 2017-06-19 08:11:15 +08:00 3692 次点击
    这是一个创建于 3040 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    上一篇《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进行状态管理的完整项目,包含了用户的登录退出,路由页面,滚动位置还原,帖子编辑状态保存等等,麻雀虽小,却是五脏俱全。

    1 条回复    2017-06-19 08:16:32 +08:00
    zaxlct
        1
    zaxlct  
       2017-06-19 08:16:32 +08:00 via Android
    Good
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3344 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 10:43 PVG 18:43 LAX 03:43 JFK 06:43
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86