Vue.js实现多条件筛选、搜索、排序及分页的表格功能


Posted in Javascript onNovember 24, 2020

与上篇实践教程一样,在这篇文章中,我将继续从一种常见的功能——表格入手,展示Vue.js中的一些优雅特性。同时也将对filter功能与computed属性进行对比,说明各自的适用场景,也为vue2.0版本中即将删除的部分filter功能做准备。

需求分析

还是先从需求入手,想想实现这样一个功能需要注意什么、大致流程如何、有哪些应用场景。

  • 表格本身是一种非常常用的组件,用于展示一些复杂的数据时表现很好。
  • 当数据比较多时,我们需要提供一些筛选条件,让用户更快列出他们关注的数据。
  • 除了预设的一些筛选条件,可能还需要一些个性化的输入搜索功能。
  • 对于有明显顺序关系的数据,例如排名、价格等,还需要排序功能方便快速倒置数据。
  • 如果数据量较大,需要分页展示表格。

需要注意的是,上述的这些需求其实和大部分数据库提供的功能是非常一致的,而且由于数据库拥有索引等优化方式以及服务器更好的性能,更加适合处理这些需求。不过现在流行的前后端分离,也是希望让客户端在合理的范围内,更多的分担服务器端的压力,所以当找到一个平衡时,在前端处理适量的需求是正确的选择。

接下来就尝试用vue完成这些需求吧。

完成Table.vue

因为这样一个多功能表格可能会应用在多个项目中,所以设计思路上尽量将表格相关的内容放在Table.vue组件中,减少耦合,方便复用。

获取测试数据

为了更好的对比前端实现以上需求的利与弊,我们需要一份较大较复杂的测试数据。幸运的是我之前的一个项目中,设计的一份API正好满足这一需求,数据为魔兽世界竞技场的天梯排行API,目前这个API处于开放状态,接口详见Myarena介绍。

与上一篇教程相类似,还是新建一个api文件夹以及一个arena.js用于管理API接口。再在App.vue中引入arena.js,在created阶段获取数据。作为一个demo,我们只获取region为CN、laddar为3v3的数据,不过只要将两个参数通过v-model绑定给对应的表单控件,就能很轻松的实现不同地区数据的切换。

引入table.vue组件

如之前所说,思路上我们希望减少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组件用于展示胜率,这是因为我希望最终的实际效果是这样的:

Vue.js实现多条件筛选、搜索、排序及分页的表格功能

一开始我直接在胜率所在的<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设计中的灵活性让我们有了更多的可能性,但在学习时,应该以搞明白不同方式在不同场景中的优劣为目标,实际运用时选择最好的那一种。

用filters实现需求

在例子中,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还是十分明智的!

用computed属性完成需求

在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中也可能重复执行,所以如果执行的操作消耗很大,建议同样添加判断,避免重复执行。

在例子代码中,我在每个方法中都统计了执行的步数,实际结果显示设置一个合理的过滤顺序可以避免一些性能问题,结果如下:

Vue.js实现多条件筛选、搜索、排序及分页的表格功能

可以看出初始化时,在没有任何过滤的情况下,sort的步数较高。而一旦添加了一些过滤条件之后,顺位靠后的filter和sort的步数都会大幅度减少。

DEMO地址

由于工作比较忙,暂时没有打算将开头中展示的MyArena项目重构,不过可以想象那会是一个很好的用vue制作单页应用的示例,后续的教程中可能会用来做例子。

本次教程中的例子,专注于展示多功能表格本身

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

Javascript 相关文章推荐
javascript 语法基础 想学习js的朋友可以看看
Dec 16 Javascript
Javascript 网页黑白效果实现代码(兼容IE/FF等)
Apr 23 Javascript
javascript判断两个IP地址是否在同一个网段的实现思路
Dec 13 Javascript
js解决select下拉选不中问题
Oct 14 Javascript
jQuery中scrollLeft()方法用法实例
Jan 16 Javascript
JS实现的简单鼠标跟随DiV层效果完整实例
Oct 31 Javascript
JavaScript数据结构与算法之栈与队列
Jan 29 Javascript
JS组件系列之Bootstrap table表格组件神器【终结篇】
May 10 Javascript
浅谈JS函数定义方式的区别
Oct 30 Javascript
angularjs使用directive实现分页组件的示例
Feb 07 Javascript
微信小程序之MaterialDesign--input组件详解
Feb 15 Javascript
Vue-router 报错NavigationDuplicated的解决方法
Mar 31 Javascript
纯JS单页面赛车游戏制作代码分享
Mar 03 #Javascript
JS html时钟制作代码分享
Mar 03 #Javascript
AngularJS路由实现页面跳转实例
Mar 03 #Javascript
vue2笔记 — vue-router路由懒加载的实现
Mar 03 #Javascript
求js数组的最大值和最小值的四种方法
Mar 03 #Javascript
jQuery控制元素隐藏和显示
Mar 03 #Javascript
jQuery插件FusionCharts实现的3D柱状图效果实例【附demo源码下载】
Mar 03 #Javascript
You might like
php 注释规范
2012/03/29 PHP
ThinkPHP CURD方法之page方法详解
2014/06/18 PHP
javascript各种复制代码收集
2008/09/20 Javascript
IE中createElement需要注意的一个问题
2010/07/13 Javascript
FF火狐下获取一个元素同类型的相邻元素实现代码
2012/12/15 Javascript
在Google 地图上实现做的标记相连接
2015/01/05 Javascript
jQuery解析XML文件同时动态增加js文件的方法
2015/06/01 Javascript
JS模仿手机端九宫格登录功能实现代码
2016/04/28 Javascript
AngularJS 作用域详解及示例代码
2016/08/17 Javascript
React入门教程之Hello World以及环境搭建详解
2017/07/11 Javascript
node 命令方式启动修改端口的方法
2018/05/12 Javascript
vue.js通过路由实现经典的三栏布局实例代码
2018/07/08 Javascript
原生js实现淘宝放大镜效果
2020/10/28 Javascript
Vue唯一可以更改vuex实例中state数据状态的属性对象Mutation的讲解
2019/01/18 Javascript
vue组件开发props验证的实现
2019/02/12 Javascript
js中数组常用方法总结(推荐)
2019/04/09 Javascript
vue实现div单选多选功能
2020/07/16 Javascript
python实现的生成随机迷宫算法核心代码分享(含游戏完整代码)
2014/07/11 Python
python实现12306火车票查询器
2017/04/20 Python
Python元字符的用法实例解析
2018/01/17 Python
python3+PyQt5实现支持多线程的页面索引器应用程序
2018/04/20 Python
python多进程实现文件下载传输功能
2018/07/28 Python
Python常见排序操作示例【字典、列表、指定元素等】
2018/08/15 Python
10个Python小技巧你值得拥有
2018/09/29 Python
python中类的属性和方法介绍
2018/11/27 Python
python pygame实现滚动横版射击游戏城市之战
2019/11/25 Python
Python通过2种方法输出带颜色字体
2020/03/02 Python
Pycharm中切换pytorch的环境和配置的教程详解
2020/03/13 Python
Python做图像处理及视频音频文件分离和合成功能
2020/11/24 Python
Skyscanner英国:苏格兰的全球三大领先航班搜索服务之一
2017/11/09 全球购物
Gibson London官网:以地道的英国男装而著称
2019/12/06 全球购物
EMPHASIS艾斐诗官网:周生生旗下原创精品珠宝品牌
2020/12/17 全球购物
英语商务邀请函范文
2014/01/16 职场文书
技术合作协议书范本
2014/04/18 职场文书
springboot @ConfigurationProperties和@PropertySource的区别
2021/06/11 Java/Android
分析并发编程之LongAdder原理
2021/06/29 Java/Android