如题,笔者最近在闲余之时一直在研究vue的常用公共组件的开发,参考的是element和iview组件库的样式,另外通过参考github上的Xue-ui组件和自己的一些想法最近新学习制作了一个简单的Vue分页组件,下面笔者将详细介绍这个组件的设计思路。
首先我们可以先看看最终的组件显示效果:
如上图,这里大致展示了我们要开发的分页组件的具体样式,而之所以说是简单的分页组件是因为这个组件既没有带跳转到指定页面的功能,也没有控制每页显示的数据条数的功能,以element组件为例,一个较为完整的分页组件是这样的:
当然这样的分页组件就比较复杂一些了,因为它还涉及到了下拉选框组件、输入框组件的使用。话说回来,我们今天要实现的只是图1。开发公共组件,俗称造轮子,其实开发界一直不鼓励重复造轮子,但是研究轮子是怎么造出来的,了解轮子的制作原理还是很重要的。
设计思路
首先,我们把要设计开发的分页组件命名为Pager组件。我们根据图1的最终组件显示图,想象出具体的HTML构架。我们发现这里有两种样式,一种是页面数字带背景色的,另一类是简易版的只是单纯的显示页面数字,没有背景色。而实现了带背景色的分页按钮的Pager,简易版的自然而然也就可以实现了。对于这种一排过去含有若干个按钮的页面,我们可以把每一个按钮当成一个div,div中显示具体的分页页码或者“...”。当然,更主流的做法是把这些按钮看成是ul标签下的li,好比是我们当初的开发网页时设计的导航栏一样。另外这里的最左和最右边分页有两个箭头符号“<” ">"用来表示上一页和下一页。这里如果要偷懒的话可以直接用大于小于号来展示,当然如果要做的好一些的话就用svg类型的图片来显示。好了,说到这里其实pager组件的基本HTML框架我们已经了解了,而css的写法也不复杂,唯一需要注意的是这里有两套样式的展示,需要通过组件的属性来切换样式的显示。
在说完了HTML和CSS之后我们来说最复杂的JS部分。首先是Pager组件需要的属性值。那么对于一个分页组件首先是父组件在调用它是需要传入的Props,对于我们需要实现的简单分页组件来说,只要总页数和当前所在页码是必须要传的,另外还有一个控制样式变化的属性我们可以选择性的传递。主要属性就是这些了,接下来就是怎么利用这些属性来展示具体的页码了,下面我们来看一下完整的代码:
<template>
<ul class="c-pager" v-show="!singleHide || total !== 1" :class="{simple}">
<li class="num" :class="{disabled:current===1}" @click="onSkip(-1)">
<c-icon name="arrow" class="arrow-left"></c-icon>
</li>
<li v-for="(page, index) in pages" :key="index" class="num" :class="{active:page===current, seprator:page==='...'}" @click="onClickPage(page)">
<template v-if="page==='...'">
<c-icon name="dot"></c-icon>
</template>
<template v-else>
{{page}}
</template>
</li>
<li class="num" :class="{disabled:current===total}" @click="onSkip(1)">
<c-icon name="arrow" class="arrow-right"></c-icon>
</li>
</ul>
</template>
<script>
import cIcon from '@/components/basic/icon/Icon.vue'
export default {
name: 'cPager',
components: {
cIcon
},
props: {
total: {
type: Number,
required: true
},
current: {
type: Number,
required: true
},
singleHide: {
type: Boolean,
default: true
},
simple: {
type: Boolean,
default: false
}
},
computed: {
pages () {
let array = [1, this.total, this.current, this.current - 1, this.current - 2, this.current + 1, this.current + 2]
if (this.current <= 4) {
array = [1, 2, 3, 4, 5, 6, 7, this.current + 1, this.current + 2, this.total]
}
if (this.current >= this.total - 3) {
array = [1, this.total, this.current, this.total - 1, this.total - 2, this.total - 3, this.total - 4, this.total - 5, this.total - 6]
}
array = this.unique(array.sort((a, b) => a - b))
let pages = array.reduce((prev, current, index, array) => {
prev.push(current)
let length = prev.length
if (prev[length - 2] && current - prev[length - 2] > 1) {
prev.splice(prev.length - 1, 0, '...')
}
return prev
}, [])
pages = pages.filter(n => (n >= 1 && n <= this.total) || n === '...')
return pages
}
},
methods: {
unique (arr) {
let newArray = []
arr.forEach(n => {
if (newArray.indexOf(n) === -1) {
newArray.push(n)
}
})
return newArray
},
onClickPage (page) {
if (page !== '...') {
this.$emit('update:current', page)
}
},
onSkip (num) {
if (num === -1 && this.current > 1) {
this.$emit('update:current', this.current - 1)
}
if (num === 1 && this.current < this.total) {
this.$emit('update:current', this.current + 1)
}
}
}
}
</script>
<style lang="scss" scoped>
@import '@/scss/baseColor.scss';
.c-pager {
font-size: 14px;
font-weight: 600;
display: flex;
justify-content: flex-start;
align-items: center;
line-height: 30px;
user-select: none;
height: 30px;
.arrow-left {
font-size: 10px;
transform: rotateZ(180deg);
}
.arrow-right {
font-size: 10px;
}
> .num {
min-width: 35px;
height: 100%;
background: $bg;
cursor: pointer;
padding: 2px 0;
display: flex;
justify-content: center;
align-items: center;
&:not(:first-child) {
margin-left: 4px;
}
&:hover:not(.seprator) {
color: $p;
}
&.active {
background: $p;
color: #fff;
cursor: default;
&:hover {
color: #fff;
}
}
&.seprator {
cursor: default;
}
&.disabled {
color: $disabled;
cursor: not-allowed;
&:hover {
color: $disabled;
}
}
}
&.simple {
> .num {
background: none;
color: $main;
&:hover:not(.seprator) {
color: $p;
}
&.active {
color: $p;
cursor: default;
}
&.disabled {
color: $disabled;
cursor: not-allowed;
&:hover {
color: $disabled;
}
}
}
}
}
</style>
代码解读:
通过代码我们可以发现Pager组件的实现还是比较简单的,大部分地方都比较好懂,这里主要是利用了pages这个计算属性来存储当前页面需要显示的页码值,另外的两个li则是展示“上一页”和“下一页”这两个按钮。关于pages的计算:pages主要有三种不同的显示形态,这里我们设当前页码值为current,总页码数为total
① 一般显示状态:
② current比较小时左侧页码完整显示不带“...”
③ current比较大时(只比total小一点)右侧页码完整显示不带“...”
在不考虑“...”按钮的展示的情况下我们根据以上三种情况可以得出pages分为为
① let array = [1, this.total, this.current, this.current - 1, this.current - 2, this.current + 1, this.current + 2]
② array = [1, 2, 3, 4, 5, 6, 7, this.current + 1, this.current + 2, this.total]
③ array = [1, this.total, this.current, this.total - 1, this.total - 2, this.total - 3, this.total - 4, this.total - 5, this.total - 6]
当然上述的array数组页码数可能会存在重复的情况,所以我们会在之后的处理中为array去重,这里利用ES6的set去重最为简洁明快,当然自己写一个去重函数也是可以的。 之后我们需要给pages加上"...",也就是以下代码:
let pages = array.reduce((prev, current, index, array) => {
prev.push(current)
let length = prev.length
if (prev[length - 2] && current - prev[length - 2] > 1) {
prev.splice(prev.length - 1, 0, '...')
}
return prev
}, [])
这里利用了一个reduce函数为pages加上"...",这里用到了reduce函数来作为筛选还是比较巧妙的,关于reduce的具体用法我们可以参考这篇文章 https://www.jianshu.com/p/541b84c9df90,reduce函数在这里的主要作用是归并,首先把initialValue参数置为空数组[ ],也就是prev的初始值,之后依次将pages数组中的元素遍历放入prev中,当出现数组长度大于等于2之后,我们需要判断prev数组的最后一个元素与倒数第二个元素之间的差值是不是大于1,如果大于1则说明两个元素之间还可以存在其他数字,此时我们将“...”符号插入到两个元素中间,用以表示省略显示了部分数字。最后我们再筛选一下数组元素,去掉不满足条件的元素:
pages = pages.filter(n => (n >= 1 && n <= this.total) || n === '...')
这样一来便得到了最关键的pages数组。
另外一个比较关键的点是跳转页面触发的函数,由于跳转页面需要改变的是current的值,而current是由父组件传入到Pager组件中的,因此当需要更新current值时,不能在Pager组件中更新,而应该使用$emit函数将current的变化通知到父组件中:
this.$emit('update:current', this.current + 1)
这里将emit的触发事件写成‘update:current’是方便在父组件中直接用.sync符进行绑定current,代码如下:
<c-pager :total="50" :current.sync="current2"></c-pager>
其相当于是:
<c-pager :total="50" :current="current2" @update:current="val => current2 = val"></c-pager>
这样便将current的变化通知到了父组件,然后由父组件来修改current2的值。
总结:
开发公共组件本身并不难,但是在没有相关知识储备和经验的情况下可能无从下手,而在实际的生产活动中我们一向不鼓励重复造轮子,但是了解轮子的构建原理很重要,它不仅能提升我们的工作效率,也加深了我们对语言和框架的认识。分页组件在组件开发中属于比较简单的一类组件了,就像我们上面做的这个分页组件一样,其实思路并不复杂,但是需要大量的基础知识储备。另外我们还可以考虑在原有的组件基础上加入跳转页码和选择每页显示条数的功能。