与上周的第一篇实践教程同样,在这篇文章中,我将继续从一种常见的功能——表格入手,展现Vue.js中的一些优雅特性。同时也将对filter功能与computed属性进行对比,说明各自的适用场景,也为vue2.0版本中即将删除的部分filter功能作准备。php
仍是先从需求入手,想一想实现这样一个功能须要注意什么、大体流程如何、有哪些应用场景。css
表格自己是一种很是经常使用的组件,用于展现一些复杂的数据时表现很好。前端
当数据比较多时,咱们须要提供一些筛选条件,让用户更快列出他们关注的数据。vue
除了预设的一些筛选条件,可能还须要一些个性化的输入搜索功能。git
对于有明显顺序关系的数据,例如排名、价格等,还须要排序功能方便快速倒置数据。github
若是数据量较大,须要分页展现表格。vue-router
须要注意的是,上述的这些需求其实和大部分数据库提供的功能是很是一致的,并且因为数据库拥有索引等优化方式以及服务器更好的性能,更加适合处理这些需求。不过如今流行的先后端分离,也是但愿让客户端在合理的范围内,更多的分担服务器端的压力,因此当找到一个平衡时,在前端处理适量的需求是正确的选择。vuex
接下来就尝试用vue完成这些需求吧。数据库
由于这样一个多功能表格可能会应用在多个项目中,因此设计思路上尽可能将表格相关的内容放在Table.vue组件中,减小耦合,方便复用。segmentfault
为了更好的对比前端实现以上需求的利与弊,咱们须要一份较大较复杂的测试数据。幸运的是我以前的一个项目中,设计的一份API正好知足这一需求,数据为魔兽世界竞技场的天梯排行API,目前这个API处于开放状态,接口详见Myarena介绍。
与上一篇教程相相似,仍是新建一个api文件夹以及一个arena.js用于管理API接口。再在App.vue中引入arena.js,在created阶段获取数据。做为一个demo,咱们只获取region为CN、laddar为3v3的数据,不过只要将两个参数经过v-model绑定给对应的表单控件,就能很轻松的实现不一样地区数据的切换。
如以前所说,思路上咱们但愿减小table组件与外部环境的耦合,因此咱们给Table.vue设置一个props属性rows,用于获取App.vue取回的数据。
在App.vue中注册table组建时要注意,命名不能用默认的table,因此注册为vTable,就能用<v-table>标签引入table组件了。
目前为止,咱们的App.vue完成了它全部的功能,代码以下:
<template> <div class="container"> <v-table :rows="rows"></v-table> </div> </template> <script> import arena from './api/arena' import vTable from './components/Table' export default { components: { vTable }, data () { return { region: 'CN', laddar: '3v3', rows: [] } }, methods: { getLaddar (region, laddar) { arena.getLaddar(region, laddar, (err, val) => { if (!err) { this.rows = val.rows } }) } }, created () { this.getLaddar(this.region, this.laddar) } } </script>
实际的App.vue中还有一个获取API中的最后更新时间的操做,以及一些css设置,篇幅考虑这里进行了省略,对完整代码有兴趣的能够移步文章末尾的Github仓库。
Table.vue的template中主要为3部分,分别是用于搜索、筛选和分页的表单控件、用于排序表格的表头thead以及用于展现数据的tbody。
首先来完成tbody的部分,基本思路就是用v-for遍历数据,再经过模板填入,须要注意如下几个重点:
返回的数据不必定彻底符合要求。例如我但愿实现经过胜率排序,但数据中只包含了胜负场数,须要先计算一次。2. 数据中用于表现玩家职业的数据为classId这个属性,但在实际项目中我想要用各职业的icon展现职业,因此我在utils.js中实现了各一个classIdToIcon的工具函数,用于映射classId至sprite图中的background-position。
以上两点说明咱们最好不要遍历props得到的rows这一原始数据。所以另建了一个computed属性players,并在其中完成了前期处理,我把全部的前期处理放在了handleBefore中。
因为即将使用的各类filters操做比较复杂,因此在handlebefore中进行了console.log('before handle'),方便咱们验证handlebefore在什么阶段被执行了。
完成布局以后,目前Table.vue中的重点代码以下:
<template> <tbody> <tr v-for="player of players :class="player.factionId? 'horde':'alliance'"> <th>{{ player.ranking }}</th> <th>{{ player.rating }}</th> <th> <span class="class" :style="{ backgroundImage: 'url(http://7xs8rx.com1.z0.glb.clouddn.com/class.png)', backgroundPosition: player.classIcon }"></span> {{ player.name }} </th> <th>{{ player.realmName }}</th> <th> <bar :win="player.weeklyWins" :loss="player.weeklyLosses"></bar> </th> <th> <bar :win="player.seasonWins" :loss="player.seasonLosses"></bar> </th> </tr> </tbody> </template> <script> import Bar from './Bar' import { classIdToIcon } from '../assets/utils' export default { components: { Bar }, props: { rows: { type: Array, default: () => { return [] } } }, computed: { players () { this.rows = this.handleBefore(this.rows) return this.rows } }, methods: { handleBefore (arr) { console.log('before handle') if (this.rows[0]) { arr.forEach((item) => { if (item.weeklyWins === 0 && item.weeklyLosses === 0) { item.weeklyRate = -1 } else { item.weeklyRate = item.weeklyWins / (item.weeklyWins + item.weeklyLosses) } if (item.seasonWins === 0 && item.seasonLosses === 0) { item.seasonRate = -1 } else { item.seasonRate = item.seasonWins / (item.seasonWins + item.seasonLosses) } item.classIcon = classIdToIcon(item.classId) }) } return arr } } } </script>
能够看到,我还引入了一个Bar.vue组件用于展现胜率,这是由于我但愿最终的实际效果是这样的:
一开始我直接在胜率所在的<th>标签中进行各类操做,但可想而知在进行一些边界状况的判断时,会出现各类含有player.weeklyWins, player.weeklyLosses等长命名变量的三元表达式。
原本是出于便利考虑,却反而致使代码难以维护。所以新建了个一个bar组件,将胜负传入组件中,在bar组件内部用更语义化的方式实现,Bar.vue中模板部分代码以下:
<template> <div class="clear-fix"> <span v-if="!hasGame || win / total > 0" :style="{ width: 100 * win / total + '%' }" :class="hasGame? '':'no-game'" class="win-bar"> {{ hasGame? (100 * win / total).toFixed(1) + '%':'无场次' }} </span> <span v-if="loss / total > 0" :style="{ width: 100 * loss / total + '%' }" class="loss-bar"> {{ win === 0? '0%':'' }} </span> </div> </template>
更好理解和维护了,不是吗?
在使用vue的过程当中,须要注意的是框架中许多方法其实在内部最终是异曲同工。
例如咱们能够直接在元素中执行一些对数据的操做,例如@click="show = !show",一样的咱们也能够对事件绑定方法,再在方法中操做数据,例如@click="toggle", toggle () { this.show = !this.show }。还好比咱们能够用computed属性和watch属性实现不少相同的功能,接下来还将用computed去实现和filters相同的功能。
vue设计中的灵活性让咱们有了更多的可能性,但在学习时,应该以搞明白不一样方式在不一样场景中的优劣为目标,实际运用时选择最好的那一种。
在例子中,players实际是一个5000条数据的数组,在不作任何处理时,将直接渲染出5000个<tr>,因此先赶忙过滤吧!
对于v-for循环,vue中提供了3中filters过滤数组,分别为filterBy, orderBy, limitBy,其功能对应了搜索/筛选、排序和分页,实现分别是使用了Array.filter, Array.sort(), Array.slice()。
这三种filters在使用时很是便利,只要在v-for后用|分离再添加对应的filters便可,这3中filter的具体参数能够查看官方API,这里很少作赘述。
须要注意的是,实际的过程是先将被遍历的数组(例子中的players)依次经过过滤器,再将最后一个过滤器返回的数组进行v-for操做。
所以,filters放置的顺序是须要根据需求来调整的,也由于每种过滤器的内部实现效率不一样,因此在需求优先级不明显时,应该以效率为优先。
注意:实际测试时,发现不论怎么过滤数组,handleBefore方法都没有再次执行,也就是说players数组并无被改动过。
例如在个人例子中,我但愿能够筛选出名字或者服务器包含了我所输入内容的玩家,而且将他们按照某种方式排序,最后的结果每页只显示20条。
那么显然剪切数组永远应该放在最后一步,而排序和过滤在需求中没有明显的优先级。可是大部分状况下,sort的效率都要低于filter,因此咱们先进行filter,减小数组长度,再sort。
有了这一思路以后,用于v-for的<tr>变为:
<tr v-for="player of players | filterBy query in 'name' 'realmName' | orderBy sort.key sort.val | limitBy 20 (page-1)*20" :class="player.factionId? 'horde':'alliance'">
这里直接将各个变量动态化,再经过Table.vue中的input绑定v-model以及表头thead绑定@click事件来改变筛选的条件,就已经实现了大部分的搜索、过滤、分页功能。
表头改变sort排序我是经过如下代码实现的,方式可能不是太好,特此列出:
<thead> <tr> <th @click="sort = {key: 'ranking', val: -sort.val}">排名</th> <th @click="sort = {key: 'rating', val: -sort.val}">分数</th> <th>资料</th> <th>服务器</th> <th @click="sort = {key: 'weeklyRate', val: -sort.val}">本周战绩</th> <th @click="sort = {key: 'seasonRate', val: -sort.val}">赛季战绩</th> </tr> </thead>
能够看到,经过vue的filters功能,已经能够轻松完成咱们的大部分功能,代码量极少。这也是vue2.0前瞻发布以后,提出废弃部分filters功能后许多人反应较为强烈的缘由。
可是如同做者在改动说明中所说,filters对于初学者来讲不易理解,而且filters的功能均可以用computed属性进行更灵活、更好把控的实现。并且在一些复杂条件下,堆叠过滤器会形成一些额外的复杂性以及不方便之处。
那么何为复杂条件呢?例如我增添两个需求,一是按职业筛选玩家,而是筛选出必定分数以上的玩家,那么后者用filterBy就不太好实现了。
咱们须要将对分数段的过滤放在filters以前进行,但又要注意不破坏players数组自己。在实际完成时,会发现这个过程仍是比较纠结的。
除此以外,咱们还会发现分页中最重要的一个信息——总页数咱们获取不到。由于vue并无把一串过滤管道中产出的最终用于v-for的数组暴露出来,因此咱们没法得到这个实际被循环的数组的长度。
在实际hack这些需求时,发现很容易与filters的执行顺序发生冲突,所以决定从新用computed属性来实现一遍全部功能,不借助自带的filters。
固然,在这一段的前半部分中,咱们显而易见的感觉到了来自filters的便利性。若是需求中filters能够知足,那么在1.x版本中使用filters仍是十分明智的!
在Github仓库中,我用Table.vue.bak文件储存了以前一段中用filters实现的代码,方便与咱们接下里的实现进行比较。
首先整理一下用computed属性来实现的思路:
首先要实现filterBy, orderBy, limitBy这三个filter的功能,上文中已经提到了他们的内部实现,因此分别用Array.filter, Array.sort和Array.slice重写一遍并不复杂。
说是computed属性实现,其实也仍是只有players这个computed属性,只是在其内部执行了全部的过滤动做,咱们实际是把各类过滤器的逻辑放置在各个method中。
不建议把各个过滤method写的过于抽象,由于就是内置filters高度抽象致使一些特殊需求没法实现,因此不妨就以最针对性的方式:一个method对应一种过滤。
在执行各个过滤method时,依然有最初提到的顺序带来的效率问题。由于vue牵一发而动全身的特性,任何一个过滤条件改变时,全部过滤method都会执行一遍,因此尽快用高效的过滤器缩短数组长度显得更为重要。
我尝试过经过watch属性实现最小化method调用,但无奈功力不够没能实现。同时我也认为前端处理大量数据的状况不多见,而且用第4点中的数据进行优化后,执行效率不算过低,因此不必在这个方面作过多纠结。真有性能瓶颈时,从服务器端寻求解决会更简单。
注意:在实现各类过滤method时,建议阅读vue中filterBy, orderBy, limitBy三部分的实现源码,其自己对于数组的操做就有一些优化,很是值得学习。在一些特殊状况中,例如数组中大量相等值时,过于简单的sort function会致使执行步数激增,vue中的一些处理都予以了避免。
根据需求目标,我设置了如下这些method(顺序即为执行顺序):
classFilter:过滤玩家职业,经过item.classId === this.class进行判断,this.class绑定的是一个select控件。
queryFilter:匹配玩家姓名中的字段,经过item.name.indexOf(this.query)判断,this.query则绑定一个input控件。
ratingFilter:筛选玩家分数段,经过item.rating >= this.rating进行判断,this.rating绑定了一个类型为range的input控件,range的范围则是用computed属性进行计算。
sortTable:由于Array.sort进行的步数较多,因此放在数组被上述3个method处理的较短后进行。
paginate:全部过滤操做完毕以后,就能够进行分页了。在使用Array.slice()以前,先将数组的长度传给this.total储存起来,用于在分页后计算总的页数。
除了以上几个过滤method之外,固然也还有handleBefore方法对数组进行前期处理。可是因为players每次都会从新计算,因此为了放止handleBefore被重复执行,应该加上必定的判断条件,例如handleBefore添加的属性是否已经存在了等等。
同时,还能够把一些不须要在过滤以前执行的动做从handleBefore中拿出,例如例子中的classId转换为Icon,能够在过滤以后对最终要展现的数据进行便可,减小一些步数。因此又设置了一个handleAfter方法,用于在分页完成以后进行后续操做,固然在handleAfter中也可能重复执行,因此若是执行的操做消耗很大,建议一样添加判断,避免重复执行。
在例子代码中,我在每一个方法中都统计了执行的步数,实际结果显示设置一个合理的过滤顺序能够避免一些性能问题,结果以下:
能够看出初始化时,在没有任何过滤的状况下,sort的步数较高。而一旦添加了一些过滤条件以后,顺位靠后的filter和sort的步数都会大幅度减小。
因为工做比较忙,暂时没有打算将开头中展现的MyArena项目重构,不过能够想象那会是一个很好的用vue制做单页应用的示例,后续的教程中可能会用来作例子。
本次教程中的例子,专一于展现多功能表格自己
上周是Vue.js开发实践的第一篇文章,也是我第一次在SF社区的我的专栏里发表文章,但愿可以把平时遇到的一些问题和解决的思路分享给你们,本身也进行一个梳理。
开发实践这个系列会用一些小例子,展现一些思路,实现一些有用、可复用的常见功能。计划中,还会有Vue.js实战系列和Sails.js实战系列两个系列的文章。
前者从较完整的项目出发,分析技术选型、vue-router和vuex的使用、多端共用代码、后期维护等方面的一些考量。后者则是用Sails.js这个框架构建企业级Node.js后端的一些尝试和心得,包括框架的优缺点、横向对比以及细节摸索等等。
目前也在关注阿里的开源项目Weex的内测进展,理想中的状态是用Weex实现项目在移动端App的开发,真正完成JS全栈,不过Weex还没正式开源,有待观望,因此只是后期设想,暂时不在计划内。
文章目前就只发在SF的专栏里,因此有意见建议都请在文章底部留言。同时因为以上所说的全部工做都由我一我的在负责,因此文章的更新可能时快时慢,争取作到一周一篇。