与上篇实践教程一样,在这篇文章中,我将继续从一种常见的功能——表格入手,展示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>
</div>
实际的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>
</div>
可以看到,我还引入了一个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>
</div>
更好理解和维护了,不是吗?
在使用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(

