Vue.js实现无限加载与分页功能开发


Posted in Javascript onNovember 03, 2016

本篇文章是一篇Vue.js的教程,目标在于用一种常见的业务场景——分页/无限加载,帮助读者更好的理解Vue.js中的一些设计思想。与许多Todo List类的入门教程相比,更全面的展示使用Vue.js完成一个需求的思考过程;与一些构建大型应用的高阶教程相比,又更专注于一些零碎细节的实现,方便读者快速掌握、致用。

需求分析

当一个页面中信息量过大时(例如一个新闻列表中有200条新闻需要展示),就会产生问题,例如:

》数据量过大,影响加载速度

》用户体验差,很难定位到之前自己看过的某篇文章

》扩展性差,如果200条变为2000条或者更多

所以常见的解决思路就是至底时加载数据或者分页展示。无限加载的实现过程类似于:

1.ajax类方法获取数据

2.数据存入本地数组

3.数组中的每条数据对应插入一个HTML模板片段中

4.将HTML片段append到节点中

前端分页的实现过程类似于:

1.ajax类方法获取数据

2.数据替换本地数组

3.数组中的每条数据对应插入一个HTML模板片段中

4.清空节点后将HTML片段append到节点中

往往修改或者维护代码时,我们会发现渲染HTML和插入部分是比较烦人的。因为我们需要将HTML拼接成字符串,在对应的位置插入数据,往往就是一段非常长的字符串,之后想要加个class都费劲。es6的模板字符串让这个情况有所好转,但是依然有瑕疵(例如实际编写时无法HTML代码高亮)。同时我们还需要写不少for或者forEach去循环数组,再命令式的append,如果这段代码片段有一些复杂的交互,可能还需要通过事件代理绑定一堆方法。

如果在完成这类业务时,你也遇到过上述的问题,那么你就会发现Vue真是太coooooool了,let's vue!

新建一个Vue.js项目

强烈推荐使用vue-cli来新建一个项目。

一开始你可能会认为用node.js和npm安装一大堆库,生成了一些你不太了解的目录和配置文件,一写代码还会跳出一堆eslint的提示。但是这绝对物有所值,因为这样的一个模板可以帮你更好的理解Vue.js组织文件的思路,并且当你适应之后,你会发现这些条条框框极大地加快了你的开发效率。

在这次的教程中,我们新建了一个名叫loadmore的项目,具体的新建项目流程可以参照官网教程的安装一节。

布局页面结构

为了配合教程的逐步深入,我先从完成 加载更多 功能入手。为了和之后的分页保持一致,我的页面准备由两部分组成,一是信息列表,二是底部的一个加载更多的按钮,我将他们都放在App.vue这个根组件中。

<template>
 <div id="app">
 <list></list>
 <a class="button" @click="next" >GO NEXT</a>
 </div>
</template>

<script>
import List from './components/List'

export default {
 components: {
 List
 },
 data () {
 return {
 ...
 }
 },
 methods: {
 next () {
 ...
 }
 }
}
</script>

<style scoped>
 .button {
 display: block;
 width: 100%;
 background: #212121;
 color: #fff;
 font-weight: bold;
 text-align: center;
 padding: 1em;
 cursor: pointer;
 text-decoration: none;
 }
 .button span {
 margin-left: 2em;
 font-size: .5rem;
 color: #d6d6d6;
 }
</style>

在这个过程中,我们根据Vue的设计思想有了如下思路:

1.在信息列表中,我们会完成我们上文中提到的几个步骤,而这些步骤都只和信息列表本身有关,与Next按钮间唯一的联系就是Next点击后需要触发信息列表去获取,而这可以通过props传递。所以我们把列表及其自身业务逻辑、样式都放在List.vue这个组件中。

2.我们为按钮定义了一些基本的样式,但是我们用的css选择器就是一个.button类名,可能会和别的组件中的.button样式冲突,所以我们加入了一个scoped属性,让App.vue中的style样式只作用于这个组件内部。

注意:scoped并不会影响css的作用优先级,使用scoped不代表不会被外部样式表覆盖。

3.我们想引入一些基础样式,比如reset.css。如果在项目中使用了sass之类的语言,那么可以将对应的外部sass文件放在assets文件夹中,通过import引入。普通的css可以直接写在一个不加scoped属性的组件中,但是如果你确定这个样式表不会被频繁改动,那么也可以作为第三方静态资源引入index.html中。例如这个例子中,我在index.html中加入了:

<link rel="stylesheet" href="./static/reset.css">

效果:

Vue.js实现无限加载与分页功能开发

完成List.vue

目前我们主要的业务逻辑都是围绕信息列表展开的,也就是我们创建的List.vue。首先,我们需要获取目标数据,我选用了cnodejs.org社区的API作为例子进行编写。如果你也想用一个封装好的ajax库的话,应该这么做:

引入第三方JS库
将目标JS库文件放在static文件夹中,例如我选择的是reqwest.js,然后在index.html先引入。

<script src="./static/reqwest.min.js"></script>

然后在build配置文件夹中,修改webpack.base.conf.js,export externals属性:

externals: {
 'reqwest': 'reqwest'
}

这样我们在我们的项目中,就可以随时加载第三方库了。

import reqwest from 'reqwest'

写个API接口
在这个例子中,我们只需要调用文章列表这一个接口,但是实际项目中,可能你需要调用很多接口,而这些接口又会在多个组件中被用到。那么调用接口的逻辑四散在各个组件中肯定是不好的,想象一下对方的url发生了变化,你就得在无数个组件中一个个检查是否要修改。

所以我在src文件夹中新建了一个api文件夹,用于存放各类API接口。当前例子中,要获取的是新闻列表,所以新建一个news.js文件:

import reqwest from 'reqwest'

const domain = 'https://cnodejs.org/api/v1/topics'

export default {
 getList (data, callback) {
 reqwest({
 url: domain,
 data: data
 })
 .then(val => callback(null, val))
 .catch(e => callback(e))
 }
}

这样我们就拥有了一个获取新闻列表的API:getList。

编写组件

我们用一个<ol>作为新闻列表,内部的每一个<li>就是一条新闻,其中包括标题、时间和作者3个信息。

在data中,我们用一个名为list的数组来储存新闻列表的数据,一开始当然是空的。我们再在data中设置一个名为limit的值,用来控制每页加载多少条数据,作为参数传给getList这个API。

因此我们的template部分是这样的(加入了一些style美化样式):

<template>
 <ol>
 <li v-for="news of list">
 <p class="title">{{ news.title }}</p>
 <p class="date">{{ news.create_at }}</p>
 <p class="author">By: {{ news.author.loginname }}</p>
 </li>
 </ol>
</template>

<style scoped>
 ol {
 margin-left: 2rem;
 list-style: outside decimal;
 }
 li {
 line-height: 1.5;
 padding: 1rem;
 border-bottom: 1px solid #b6b6b6;
 }
 .title {
 font-weight: bold;
 font-size: 1.3rem;
 }
 .date {
 font-size: .8rem;
 color: #d6d6d6;
 }
</style>

之后我们显然需要使用getList来获取数据,不过先想想我们会在哪几个地方使用呢?首先,我们需要在组件开始渲染时自动获取一次列表,填充基础内容。其次,我们在每次点击APP.vue中的Next按钮时也需要获取新的列表。

所以我们在methods中定义一个get方法,成功获取到数据后,就把获取的数组拼接到当前list数组后,从而实现了加载更多。

沿着这个思路,再想想get方法需要的参数,一个是包含了page和limit两个属性的对象,另一个是回调函数。回调函数我们已经说过,只需要拼接数组即可,因此只剩下最后一个page参数还没设置。

在初始化的时候,page的值应该为1,默认是第一页内容。之后page的值只由Next按钮改变,所以我们让page通过props获取App.vue中传来的page值。

最后则是补充get方法触发的条件。一是在组件的生命周期函数created中调用this.get()获取初始内容,另一是在page值变化时对应获取,所以我们watch了page属性,当其变化时,调用this.get()。

最后List.vue的script长这样:

<script>
import news from '../api/news'

export default {
 data () {
 return {
 list: [],
 limit: 10
 }
 },
 props: {
 page: {
 type: Number,
 default: 1
 }
 },
 created () {
 this.get()
 },
 watch: {
 page (val) {
 this.get()
 }
 },
 methods: {
 get () {
 news.getList({
 page: this.page,
 limit: this.limit
 }, (err, list) => {
 if (err) {
 console.log(err)
 } else {
 list.data.forEach((data) => {
 const d = new Date(data.create_at)
 data.create_at = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
 })
 this.list = this.list.concat(list.data)
 }
 })
 }
 }
}
</script>

同时我们将App.vue中的<list>修改为:

<list :page="page"></list>

再为page在App.vue中添加一个初始值以及对应的方法next:

data () {
 return {
 page: 1
 }
},
methods: {
 next () {
 this.page++
 }
}

这样我们就已经完成了加载更多的功能。

Vue.js实现无限加载与分页功能开发

改写为分页

因为之前我们的思路非常清晰,代码结构也很明了,所以改写起来会非常简单,只需要将List.vue中拼接数组改为赋值数组就可以了:

// 常规loadmore
// this.list = this.list.concat(list.data)
// 分页
this.list = list.data

就这么简单的一行就完成了功能的改变,这就是Vue.js中核心的数据驱动视图的威力。当然,接下来我们还要做点更cooooool的。

添加功能

因为分页替换了原来的数组,所以仅仅一个Next按钮不够用了,我们还需要一个Previous按钮返回上一页。同样的,也给Previous按钮绑定一个previous方法,除了用this.page--改变page的值以外,还需要对this.page === 1的边界条件进行一个判断。

同时为了方便知道我们当前的页数,在按钮中,加入{{ page }}显示页数。

<a class="button" @click="next" >GO NEXT<span>CURRENT:{{page}}</span></a>

transition动画
编写和完善功能的过程中,已经充分体现了Vue.js清晰和便利的一面,接下来继续看看其它好用的功能,首先就是transition动画。

为了展示transition的威力,首先我找到了一个模仿的对象:lavalamp.js( Demo地址 )。

在Demo中可以看到页面以一种非常优雅的动画过渡完成了切换内容的过程,其本身是用JQuery+CSS动画完成的,我准备用Vue.js进行改写。

首先学习了一下原作者的实现思路以后,发现是将一个div作为loader,position设定为fixed。当翻页时,根据点击的按钮不同,loader从顶部或者底部扩展高度,达到100%。数据加载完毕后,再折叠高度,最终隐藏。

那么初步的思路如下:

1.添加一个loader,最小高度与按钮一致,背景同为黑色,让过渡显得更自然。

2.loader高度需要达到一个屏幕的高度,所以设置html和body的height为100%。

3.需要有一个值,作为loader是否显示的依据,我定为finish,其默认值值为true,通过给loader添加v-show="!finish"来控制其显示。

4.在next和previous方法中添加this.finish = false触发loader的显示。

5.在App.vue和List.vue建立一个双向的props属性绑定至finish,当List.vue中的get方法执行完毕后,通过props将App.vue中的finish设定为true,隐藏loader。

6.给loader添加一个transition。由于动画分为顶部展开和底部展开两种,所以使用动态的transition为其指定正确的transition名称。

7.新增一个值up,用于判断动画从哪个方向开始,其默认值为false。在previous方法中,执行this.up = true,反之在next方法中,则执行this.up = false。

根据思路,写出的loader应该是这样的(style等样式设定在最后统一展示):

<div id="loader" v-show="!finish" :transition="up? 'up-start':'down-start'">
 <span>Loading</span>
</div>

可以看到我设定了up-start和down-start两种transition方式,对应的css动画代码如下:

.down-start-transition {
 bottom: 0;
 height: 100%;
 }
 .down-start-enter {
 animation: expand .5s 1 cubic-bezier(0, 1, 0, 1) both;
 }
 .down-start-leave {
 animation: collapse .5s 1 cubic-bezier(0, 1, 0, 1) both;
 top: 0;
 bottom: auto;
 }
 .up-start-transition {
 top: 0;
 height: 100%;
 }
 .up-start-enter {
 animation: expand .5s 1 cubic-bezier(0, 1, 0, 1) both;
 }
 .up-start-leave {
 animation: collapse .5s 1 cubic-bezier(0, 1, 0, 1) both;
 top: auto;
 bottom: 0;
 }
 @keyframes expand {
 0% {
 height: 3em;
 transform: translate3d(0, 0, 0);
 }
 100% {
 height: 100%;
 transform: translate3d(0, 0, 0);
 }
 }
 @keyframes collapse {
 0% {
 height: 100%;
 transform: translate3d(0, 0, 0);
 }
 100% {
 height: 3em;
 transform: translate3d(0, 0, 0);
 }
 }

设置了expand和collapse两个animation,再在transition的各个生命周期钩子中做对应的绑定,就达到了和lavalamp.js相接近的效果。

为了保证动画能执行完整,在List.vue的get方法执行完之后,还使用了一个setTimeout定时器让finish延时0.5秒变为true。

优化体验
动画效果完成之后,实际使用时发现lavalamp.js还有个巧妙地设计,就是点击Previous后,页面前往底部,反之点击Next后则前往顶部。

实现后者并不复杂,在next方法中加入以下一行代码调整位置即可:

document.body.scrollTop = 0

previous前往底部则略微复杂一点,因为获取到数据之后,页面高度会发生改变,如果在previous中执行scrollTop的改变,有可能会出现新的内容填充后高度变长,页面不到底的情况。所以我watch了finish的值,仅当点击按钮为previous且finish变化为false至true时前往底部,代码如下:

watch: {
 finish (val, oldVal) {
 if (!oldVal && val && this.up) {
 document.body.scrollTop = document.body.scrollHeight
 }
 }
}

前端路由
完成以上内容之后,发现不论翻到第几页,一旦刷新,就会回到第一页。vue-router就是为解决这类问题而生的。

首先我们引入VueRouter,方式可以参考上文中的“引入第三方JS库”。然后在main.js对路由规则进行一些配置。

我们的思路包括:

1.我们需要在url上反映出当前所处的页数。

2.url中的页数应该与所有组件中的page值保持一致。

3.点击Next和Previous按钮要跳转到对应的url去。

4.在这个例子中我们没有router-view。

因此main.js的配置如下:

import Vue from 'vue'
import App from './App'
import VueRouter from 'VueRouter'

Vue.use(VueRouter)

const router = new VueRouter()
router.map({
 '/page/:pageNum': {
 name: 'page',
 component: {}
 }
})

router.redirect({
 '/': '/page/1'
})

router.beforeEach((transition) => {
 if (transition.to.path !== '/page/0') {
 transition.next()
 } else {
 transition.abort()
 }
})

router.start(App, 'app')

首先定义了一个名为page的具名路径。之后将所有目标路径为'/',也就是初始页的请求,重定向到'/page/1'上保证一致性。最后再在每次路由执行之前做一个判断,如果到了'/page/0'这样的非法路径上,就不执行transition.next()。

根据之前的思路,在App.vue中,获取路由对象的参数值,赋值给page。同时给两个按钮添加对应的v-link。

本文已被整理到了《Vue.js前端组件学习教程》,欢迎大家学习阅读。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript 对象定义方法 简单易学
Mar 22 Javascript
原生js实现跨浏览器获取鼠标按键的值
Apr 08 Javascript
Javscript删除数组中指定元素并返回新数组
Mar 06 Javascript
JS修改地址栏参数实例代码
Jun 14 Javascript
js模态对话框使用方法详解
Feb 16 Javascript
jquery实现图片上传前本地预览
Apr 28 jQuery
JavaScript设计模式之策略模式详解
Jun 09 Javascript
JavaScript利用fetch实现异步请求的方法实例
Jul 26 Javascript
vue利用v-for嵌套输出多层对象,分别输出到个表的方法
Sep 07 Javascript
Django+Vue实现WebSocket连接的示例代码
May 28 Javascript
JS实现音乐钢琴特效
Jan 06 Javascript
在微信小程序中渲染HTML内容3种解决方案及分析与问题解决
Jan 12 Javascript
vue自定义指令实现v-tap插件
Nov 03 #Javascript
angular ngClick阻止冒泡使用默认行为的方法
Nov 03 #Javascript
vue.js指令v-for使用及索引获取
Nov 03 #Javascript
vue.js初学入门教程(1)
Nov 03 #Javascript
把json格式的字符串转换成javascript对象或数组的方法总结
Nov 03 #Javascript
AngularJS出现$http异步后台无法获取请求参数问题的解决方法
Nov 03 #Javascript
Centos7 中安装 Node.js v4.4.4
Nov 03 #Javascript
You might like
如何使用脚本模仿登陆过程
2006/11/22 PHP
解析MySql与Java的时间类型
2013/06/22 PHP
php编写的简单页面跳转功能实现代码
2013/11/27 PHP
PHP根据传来的16进制颜色代码自动改变背景颜色
2014/06/13 PHP
PHP获取数组最大值下标的方法
2015/05/12 PHP
PHP内核学习教程之php opcode内核实现
2016/01/27 PHP
PHP常量define和const的区别详解
2019/05/18 PHP
jquery 3D球状导航的文章分类
2010/07/06 Javascript
用js一次改变多个input的readonly属性值的方法
2014/06/11 Javascript
DOM基础教程之模型中的模型节点
2015/01/19 Javascript
JavaScript中的splice方法用法详解
2016/07/20 Javascript
Javascript实现信息滚动效果
2017/05/18 Javascript
微信小程序自定义弹窗wcPop插件
2018/11/19 Javascript
在vue中使用echars实现上浮与下钻效果
2019/11/08 Javascript
jQuery实现二级导航菜单的示例
2020/09/30 jQuery
Antd下拉选择,自动匹配功能的实现
2020/10/24 Javascript
[52:41]OG vs IG 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/20 DOTA
python实现获取Ip归属地等信息
2016/08/27 Python
Python实现的下载网页源码功能示例
2017/06/13 Python
Python中 传递值 和 传递引用 的区别解析
2018/02/22 Python
Python http接口自动化测试框架实现方法示例
2018/12/06 Python
python误差棒图errorbar()函数实例解析
2020/02/11 Python
Python转换字典成为对象,可以用&quot;.&quot;方式访问对象属性实例
2020/05/11 Python
Python matplotlib可视化实例解析
2020/06/01 Python
非常震撼的纯CSS3人物行走动画
2016/02/24 HTML / CSS
html5 canvas实现给图片添加平铺水印
2019/08/20 HTML / CSS
GUESS盖尔斯法国官网:美国时尚品牌
2016/09/23 全球购物
英国打印机墨水和碳粉商店:Printerinks
2017/06/30 全球购物
澳大利亚婴儿礼品公司:The Baby Gift Company
2018/11/04 全球购物
纽约海:Sea New York
2018/11/04 全球购物
override和overload的区别
2016/03/09 面试题
自荐信的基本格式
2014/02/22 职场文书
小学生作文评语
2014/04/18 职场文书
思想作风整顿个人剖析材料
2014/10/06 职场文书
公证书格式
2015/01/23 职场文书
门卫岗位职责
2015/02/09 职场文书