本篇文章是一篇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">

效果:

完成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++
}
}

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

改写为分页

因为之前我们的思路非常清晰,代码结构也很明了,所以改写起来会非常简单,只需要将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。

最终的demo地址
Github仓库

Vue.js 开发实践:实现精巧的无限加载与分页功能的更多相关文章

  1. 前端开发工具vue.js开发实践总结

    最近有很长时间没有更新博客了,换了公司,全部的心思都放在项目上了.通过这次项目的上线,让我感受最深的是前后端分离后,前端页面的模块化管理,以及前端页面的数据邦定.在接触vue.js之前,我之前端要用到 ...

  2. 伟景行 citymaker 从入门到精通(1)——js开发,最基本demo,加载cep工程文件

    开发环境:citymaker 7(以下简称cm),jquery,easyui 1.4(界面),visual studio 2012(没有vs,不部署到IIS也行,html文件在本地目录双击打开可用) ...

  3. 用Vue.js开发微信小程序:开源框架mpvue解析

    前言 mpvue 是一款使用 Vue.js 开发微信小程序的前端框架.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小程序提供了代码复用的能力.如果想将 H5 项目改造为小程 ...

  4. vue.js开发SPA常见问题及解决方法

    列表进入详情页的传参问题. 例如商品列表页面前往商品详情页面,需要传一个商品id; <router-link :to="{path: 'detail', query: {id: 1}} ...

  5. Vue.js最佳实践

    Vue.js最佳实践 第一招:化繁为简的Watchers 场景还原: created(){ this.fetchPostList() }, watch: { searchInputValue(){ t ...

  6. windows下vue.js开发环境搭建教程

    这篇文章主要为大家详细介绍了windows下vue.js开发环境搭建教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 最近,vue.js越来越火.在这样的大浪潮下,我也开始进入vue的学习行列中 ...

  7. 基于 Laravel、Vue.js开发的全新社交系统----ThinkSNS+

    什么是ThinkSNS+ ThinkSNS(简称TS)始于2008年,一款全平台综合性社交系统,为国内外大中小企业和创业者提供社会化软件研发及技术解决方案,目前最新版本为ThinkSNS+.新的产品名 ...

  8. electron-vue:Vue.js 开发 Electron 桌面应用

    相信很多同学都知道 Electron 可以帮助开发人员使用前端技术开发桌面客户端应用,今天介绍的 electron-vue 框架是一套基于 Vue.js 开发 Electron 桌面应用的脚手架,该项 ...

  9. MPVUE - 使用vue.js开发微信小程序

    MPVUE - 使用vue.js开发微信小程序 什么是mpvue? mpvue 是美团点评前端团队开源的一款使用 Vue.js 开发微信小程序的前端框架.框架提供了完整的 Vue.js 开发体验,开发 ...

随机推荐

  1. Python:批量创建py文件

    import os filePrefix='Test' fileSuffix='.py' fileNum=7 #文件个数 for i in range(0,fileNum): filename=fil ...

  2. 函数式编程 高阶函数 map&reduce filter sorted

    函数式编程 纯函数:没有变量的函数 对于纯函数而言:只要输入确定,那么输出就是确定的.纯函数是没有副作用的. 函数式编程:允许把函数本身作为参数传入另一个函数,还允许返回一个函数 高阶函数:一个函数的 ...

  3. c语言——uthash使用

    参考:https://troydhanson.github.io/uthash/userguide.html https://blog.csdn.net/lovemust/article/detail ...

  4. Windows Service 安装、启动和卸载

    win+R输入cmd,以管理员身份运行cmd: 安装: 1.cd C:\Windows\Microsoft.NET\Framework\v4.0.30319   回车 2.输入(InstallUtil ...

  5. 云计算与云存储:使用云服务器搭建一个情侣纪念Web服务器

    做完了实验一,做完感觉这门还是蛮好玩的,而且第一实验就很有趣,搭建了一个可以在公网访问的纪念网站给女朋友秀了一下.写好实验报告后简单搬运,应该能给感兴趣的朋友带来帮助. 创建阿里云主机 进入阿里云官方 ...

  6. 探索TiDB Lightning的源码来解决发现的bug

    背景 上一篇<记一次简单的Oracle离线数据迁移至TiDB过程>说到在使用Lightning导入csv文件到TiDB的时候发现了一个bug,是这样一个过程. Oracle源库中表名都是大 ...

  7. 写博客的技巧整理——基于Markdown

    我们需要掌握各种技巧,这样才能在写博客时游刃有余,以下内容觉得不错就点个赞吧 文章目录 1.目录与目录跳转 目录一(示例用勿点) 目录二(示例用勿点) 目录三(示例用勿点) 2.文字与图片 3.引用 ...

  8. PicGo+Typora配置gitee图床

    1 .下载Typora Typora官网地址:https://www.typora.io/#windows 本文使用的Typora版本为 0.11.2 根据自己的要求进行安装即可! 2.下载PicGo ...

  9. Java单例模式示范

    package com.ricoh.rapp.ezcx.iwbservice.util; import java.util.ArrayList; import java.util.List; impo ...

  10. QT designer的安装与汉化(pycharm)

    QT designer的安装 1.安装好Python3的环境 添加环境变量,保证安装正确, 2.安装PyQt5 采用命令安装,Win+R,输入CMD,打开命令框,输入以下命令.后面是豆瓣的镜像地址,是 ...