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 相关文章推荐
jquery 图片Silhouette Fadeins渐显效果
Feb 07 Javascript
jQuery each()方法的使用方法
Mar 18 Javascript
Javascript 设计模式(二) 闭包
May 26 Javascript
javascript 使td内容不换行不撑开
Nov 29 Javascript
js监听滚动条滚动事件使得某个标签内容始终位于同一位置
Jan 24 Javascript
AngularJS学习笔记之TodoMVC的分析
Feb 22 Javascript
JavaScript鼠标事件,点击鼠标右键,弹出div的简单实例
Aug 03 Javascript
JavaScript使用正则表达式获取全部分组内容的方法示例
Jan 17 Javascript
JS中mouseup事件丢失的原因与解决办法
Jun 14 Javascript
在小程序/mpvue中使用flyio发起网络请求的方法
Sep 13 Javascript
小程序怎样让wx.navigateBack更好用的方法实现
Nov 01 Javascript
uni-app使用countdown插件实现倒计时
Nov 01 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 session_start()关于Cannot send session cache limiter - headers already sent错误解决方法
2009/11/27 PHP
Yii中表单用法实例详解
2016/01/05 PHP
PHP+Ajax无刷新带进度条图片上传示例
2017/02/08 PHP
Ubuntu上安装yaf扩展的方法
2018/01/29 PHP
laravel http 自定义公共验证和响应的方法
2019/09/29 PHP
Node.js中使用jQuery的做法
2016/08/17 Javascript
vue cli构建的项目中请求代理与项目打包问题
2018/02/26 Javascript
Vue SPA单页应用首屏优化实践
2018/06/28 Javascript
利用jquery和BootStrap实现动态滚动条效果
2018/12/03 jQuery
jQuery表单选择器用法详解
2019/08/22 jQuery
为nuxt项目写一个面包屑cli工具实现自动生成页面与面包屑配置
2019/09/29 Javascript
细述Javascript的加法运算符的具体使用
2019/10/18 Javascript
Vue监听滚动实现锚点定位(双向)示例
2019/11/13 Javascript
js+canvas实现简单扫雷小游戏
2021/01/22 Javascript
原生js实现html手机端城市列表索引选择城市
2020/06/24 Javascript
Vue toFixed保留两位小数的3种方式
2020/10/23 Javascript
对于Python装饰器使用的一些建议
2015/06/03 Python
正则给header的冒号两边参数添加单引号(Python请求用)
2019/08/09 Python
Python将二维列表list的数据输出(TXT,Excel)
2020/04/23 Python
HTML中fieldset标签概述及使用方法
2013/02/01 HTML / CSS
美国定制钻石订婚戒指:Ritani
2017/12/08 全球购物
应用化学专业本科生求职信
2013/09/29 职场文书
本科生求职简历的自我评价
2013/10/21 职场文书
专业实习自我鉴定
2013/10/29 职场文书
行政主管岗位职责
2013/11/18 职场文书
陈欧广告词
2014/03/14 职场文书
家长学校工作方案
2014/05/07 职场文书
2014高考励志标语
2014/06/05 职场文书
中国梦口号
2014/06/13 职场文书
七一建党节演讲稿
2014/09/11 职场文书
2015大学生实训报告
2014/11/05 职场文书
2015廉洁自律个人总结
2015/02/14 职场文书
物业前台接待岗位职责
2015/04/03 职场文书
将图片保存到mysql数据库并展示在前端页面的实现代码
2021/05/02 MySQL
Elasticsearch 聚合查询和排序
2022/04/19 Python
CSS 实现角标效果的完整代码
2022/06/28 HTML / CSS