本篇文章是一篇Vue.js的教程,目标在于用一种常见的业务场景——分页/无限加载,帮助读者更好的理解Vue.js中的一些设计思想。与许多Todo List类的入门教程相比,更全面的展现使用Vue.js完成一个需求的思考过程;与一些构建大型应用的高阶教程相比,又更专一于一些零碎细节的实现,方便读者快速掌握、致用。css
当一个页面中信息量过大时(例如一个新闻列表中有200条新闻须要展现),就会产生问题,例如:html
数据量过大,影响加载速度前端
用户体验差,很难定位到以前本身看过的某篇文章vue
扩展性差,若是200条变为2000条或者更多node
因此常见的解决思路就是至底时加载数据或者分页展现。无限加载的实现过程相似于:webpack
ajax类方法获取数据git
数据存入本地数组es6
数组中的每条数据对应插入一个HTML模板片断中github
将HTML片断append到节点中web
前端分页的实现过程相似于:
ajax类方法获取数据
数据替换本地数组
数组中的每条数据对应插入一个HTML模板片断中
清空节点后将HTML片断append到节点中
每每修改或者维护代码时,咱们会发现渲染HTML和插入部分是比较烦人的。由于咱们须要将HTML拼接成字符串,在对应的位置插入数据,每每就是一段很是长的字符串,以后想要加个class都费劲。es6的模板字符串让这个状况有所好转,可是依然有瑕疵(例如实际编写时没法HTML代码高亮)。
同时咱们还须要写很多for或者forEach去循环数组,再命令式的append,若是这段代码片断有一些复杂的交互,可能还须要经过事件代理绑定一堆方法。
若是在完成这类业务时,你也遇到过上述的问题,那么你就会发现Vue真是太coooooool了,let's vue!
强烈推荐使用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的设计思想有了以下思路:
在信息列表中,咱们会完成咱们上文中提到的几个步骤,而这些步骤都只和信息列表自己有关,与Next按钮间惟一的联系就是Next点击后须要触发信息列表去获取,而这能够经过props传递。因此咱们把列表及其自身业务逻辑、样式都放在List.vue这个组件中。
咱们为按钮定义了一些基本的样式,可是咱们用的css选择器就是一个.button类名,可能会和别的组件中的.button样式冲突,因此咱们加入了一个scoped属性,让App.vue中的style样式只做用于这个组件内部。
注意:scoped并不会影响css的做用优先级,使用scoped不表明不会被外部样式表覆盖。
咱们想引入一些基础样式,好比reset.css。若是在项目中使用了sass之类的语言,那么能够将对应的外部sass文件放在assets文件夹中,经过import引入。普通的css能够直接写在一个不加scoped属性的组件中,可是若是你肯定这个样式表不会被频繁改动,那么也能够做为第三方静态资源引入index.html中。例如这个例子中,我在index.html中加入了:
<link rel="stylesheet" href="./static/reset.css">
效果:
目前咱们主要的业务逻辑都是围绕信息列表展开的,也就是咱们建立的List.vue。
首先,咱们须要获取目标数据,我选用了cnodejs.org社区的API做为例子进行编写。若是你也想用一个封装好的ajax库的话,应该这么作:
将目标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'
在这个例子中,咱们只须要调用文章列表这一个接口,可是实际项目中,可能你须要调用不少接口,而这些接口又会在多个组件中被用到。那么调用接口的逻辑四散在各个组件中确定是很差的,想象一下对方的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>
编写和完善功能的过程当中,已经充分体现了Vue.js清晰和便利的一面,接下来继续看看其它好用的功能,首先就是transition动画。
为了展现transition的威力,首先我找到了一个模仿的对象:lavalamp.js(Demo地址)。
在Demo中能够看到页面以一种很是优雅的动画过渡完成了切换内容的过程,其自己是用JQuery+CSS动画完成的,我准备用Vue.js进行改写。
首先学习了一下原做者的实现思路之后,发现是将一个div做为loader,position设定为fixed。当翻页时,根据点击的按钮不一样,loader从顶部或者底部扩展高度,达到100%。数据加载完毕后,再折叠高度,最终隐藏。
那么初步的思路以下:
添加一个loader,最小高度与按钮一致,背景同为黑色,让过渡显得更天然。
loader高度须要达到一个屏幕的高度,因此设置html和body的height为100%。
须要有一个值,做为loader是否显示的依据,我定为finish,其默认值值为true,经过给loader添加v-show="!finish"来控制其显示。
在next和previous方法中添加this.finish = false触发loader的显示。
在App.vue和List.vue创建一个双向的props属性绑定至finish,当List.vue中的get方法执行完毕后,经过props将App.vue中的finish设定为true,隐藏loader。
给loader添加一个transition。因为动画分为顶部展开和底部展开两种,因此使用动态的transition为其指定正确的transition名称。
新增一个值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对路由规则进行一些配置。
咱们的思路包括:
咱们须要在url上反映出当前所处的页数。
url中的页数应该与全部组件中的page值保持一致。
点击Next和Previous按钮要跳转到对应的url去。
在这个例子中咱们没有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。