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

这篇文章主要为大家详细介绍了Vue.js实现多条件筛选、搜索、排序及分页的表格功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

与上篇实践教程一样,在这篇文章中,我将继续从一种常见的功能——表格入手,展示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完成了它所有的功能,代码如下:

  1. <template>  
  2.  <div class=“container”>  
  3.  <v-table  
  4.  :rows=“rows”></v-table>  
  5.  </div>  
  6. </template>  
  7.   
  8. <script>  
  9. import arena from ’./api/arena’  
  10. import vTable from ’./components/Table’  
  11.   
  12. export default {  
  13.  components: { vTable },  
  14.  data () {  
  15.  return {  
  16.  region: ‘CN’,  
  17.  laddar: ‘3v3’,  
  18.  rows: []  
  19.  }  
  20.  },  
  21.  methods: {  
  22.  getLaddar (region, laddar) {  
  23.  arena.getLaddar(region, laddar, (err, val) => {  
  24.  if (!err) {  
  25.   this.rows = val.rows  
  26.  }  
  27.  })  
  28.  }  
  29.  },  
  30.  created () {  
  31.  this.getLaddar(this.region, this.laddar)  
  32.  }  
  33. }  
  34. </script>  
<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中的重点代码如下:

  1. <template>  
  2.  <tbody>  
  3.  <tr  
  4.  v-for=”player of players  
  5.  :class=“player.factionId? ‘horde’:’alliance’”>  
  6.  <th>{{ player.ranking }}</th>  
  7.  <th>{{ player.rating }}</th>  
  8.  <th>  
  9.  <span  
  10.  class=“class”  
  11.  :style=”{ backgroundImage: ‘url(http://7xs8rx.com1.z0.glb.clouddn.com/class.png)’,  
  12.    backgroundPosition: player.classIcon }”></span>  
  13.  {{ player.name }}  
  14.  </th>  
  15.  <th>{{ player.realmName }}</th>  
  16.  <th>  
  17.  <bar  
  18.  :win=“player.weeklyWins”  
  19.  :loss=“player.weeklyLosses”></bar>  
  20.  </th>  
  21.  <th>  
  22.  <bar  
  23.  :win=“player.seasonWins”  
  24.  :loss=“player.seasonLosses”></bar>  
  25.  </th>  
  26.  </tr>  
  27.  </tbody>  
  28. </template>  
  29.   
  30. <script>  
  31. import Bar from ’./Bar’  
  32. import { classIdToIcon } from ’../assets/utils’  
  33.   
  34. export default {  
  35.  components: { Bar },  
  36.  props: {  
  37.  rows: {  
  38.  type: Array,  
  39.  default: () => {  
  40.  return []  
  41.  }  
  42.  }  
  43.  },  
  44.  computed: {  
  45.  players () {  
  46.  this.rows = this.handleBefore(this.rows)  
  47.  return this.rows  
  48.  }  
  49.  },  
  50.  methods: {  
  51.  handleBefore (arr) {  
  52.  console.log(‘before handle’)  
  53.  if (this.rows[0]) {  
  54.  arr.forEach((item) => {  
  55.   if (item.weeklyWins === 0 && item.weeklyLosses === 0) {  
  56.   item.weeklyRate = -1  
  57.   } else {  
  58.   item.weeklyRate = item.weeklyWins / (item.weeklyWins + item.weeklyLosses)  
  59.   }  
  60.   if (item.seasonWins === 0 && item.seasonLosses === 0) {  
  61.   item.seasonRate = -1  
  62.   } else {  
  63.   item.seasonRate = item.seasonWins / (item.seasonWins + item.seasonLosses)  
  64.   }  
  65.   item.classIcon = classIdToIcon(item.classId)  
  66.  })  
  67.  }  
  68.  return arr  
  69.  }  
  70.  }  
  71. }  
  72. </script>  
<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中模板部分代码如下:

  1. <template>  
  2.  <div class=“clear-fix”>  
  3.  <span  
  4.  v-if=“!hasGame || win / total > 0”  
  5.  :style=“{ width: 100 * win / total + ’%’ }”  
  6.  :class=“hasGame? ”:’no-game’”  
  7.  class=“win-bar”>  
  8.  {{ hasGame? (100 * win / total).toFixed(1) + ’%’:’无场次’ }}  
  9.  </span>  
  10.  <span  
  11.  v-if=“loss / total > 0”  
  12.  :style=“{ width: 100 * loss / total + ’%’ }”  
  13.  class=“loss-bar”>  
  14.  {win === 0? ‘0%’:” }}  
  15.  </span>  
  16.  </div>  
  17. </template>  
<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>变为:

  1. <tr  
  2. v-for=”player of players  
  3. | filterBy query in ‘name’ ‘realmName’  
  4. | orderBy sort.key sort.val  
  5. | limitBy 20 (page-1)*20”  
  6. :class=“player.factionId? ‘horde’:’alliance’”>  
<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排序我是通过以下代码实现的,方式可能不是太好,特此列出:

  1. <thead>  
  2.  <tr>  
  3.  <th  
  4.  @click=“sort = {key: ‘ranking’, val: -sort.val}”>排名</th>  
  5.  <th  
  6.  @click=“sort = {key: ‘rating’, val: -sort.val}”>分数</th>  
  7.  <th>资料</th>  
  8.  <th>服务器</th>  
  9.  <th  
  10.  @click=“sort = {key: ‘weeklyRate’, val: -sort.val}”>本周战绩</th>  
  11.  <th  
  12.  @click=“sort = {key: ‘seasonRate’, val: -sort.val}”>赛季战绩</th>  
  13.  </tr>  
  14. </thead>  
<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中也可能重复执行,所以如果执行的操作消耗很大,建议同样添加判断,避免重复执行。

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

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

DEMO地址

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

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

DEMO地址点我
Github仓库

写作计划

上周是Vue.js开发实践的第一篇文章,也是我第一次在SF社区的个人专栏里发表文章,希望能够把平时遇到的一些问题和解决的思路分享给大家,自己也进行一个梳理。

开发实践这个系列会用一些小例子,展示一些思路,实现一些有用、可复用的常见功能。计划中,还会有Vue.js实战系列和Sails.js实战系列两个系列的文章。
前者从较完整的项目出发,分析技术选型、vue-router和vuex的使用、多端共用代码、后期维护等方面的一些考量。后者则是用Sails.js这个框架构建企业级Node.js后端的一些尝试和心得,包括框架的优缺点、横向对比以及细节摸索等等。
目前也在关注阿里的开源项目Weex的内测进展,理想中的状态是用Weex实现项目在移动端App的开发,真正完成JS全栈,不过Weex还没正式开源,有待观望,所以只是后期设想,暂时不在计划内。

文章目前就只发在SF的专栏里,所以有意见建议都请在文章底部留言。同时由于以上所说的所有工作都由我一个人在负责,所以文章的更新可能时快时慢,争取做到一周一篇。

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值