第九集: 从零开始实现一套pc端vue的ui组件库( 分页器组件 )

第九集: 从零开始实现( 分页器组件 )

本集定位:
分页器这个组件也算是个老朋友了, 还记得刚学js的时候, 写个分页器要300行代码,要是能穿越回去, 我得好好教教我自己设计模式😹.

随着现在手机地位的提升, 大部分人上网的时间都用在了手机上, pc端的确是少了很多很多, 而分页器这种类型的组件, 真的并不很适合手机, 已经不符合人类的操作体验了, 人们现在都是划划划或拉拉拉的动作驱动翻页, 分页器其实要配合鼠标才能有很好的感受, 人的手指点击并不精准, 而且点击的时候还会遮盖住视野, 反正本人更推荐pc端使用分页器来跳转, 移动端可不要这么玩, 多想点让用户更舒服的操作吧.

本次编写参考了饥人谷的视频, 同时也看了element的源码, 但最终还是按我自己的想法构建了这个组件.

  1. 增加了自己的一些理解, 实现方式与想法挺有趣的, 毕竟代码这种东西不是一成不变的, 更多的玩法才能使编程更有生命力,
  2. 去掉了比如说一个输入框, 可以跳转到固定页数, 这个功能我去年做了半年的后台管理系统, 一次也没用上.
  3. 没有去想传统组件一样, 用户传入总条数, 然后我来给分成对应的页数, 而是采用用户自己传入分成了多少页.
  4. 本次激活的页面会以v-model的形式与用户进行交互, 也就是说这个变量是双向的, 上面说去掉的两个功能就非常好实现了, 但本次不会做, 毕竟实践经验告诉我, 做了真没啥用

一. 基本结构
这个结构是参考的别人的源码... 还有挺好的, 虽然dom写的有点丑, 但是逻辑清晰, 易维护.

老样子
vue-cc-ui/src/components/Pagination/index.js

import Pagination from './main/pagination.vue';

Pagination.install = function(Vue) {
    Vue.component(Pagination.name, Pagination);
  };

export default Pagination;

躯壳

  1. 单独写了 第一位与最后一位, 默认就是这两位要一直展示给用户选择
  2. ...这种标志也是单独写了, 毕竟首尾都直接写了, 那他也可以这样操作
  3. 左右两边的两个按钮允许用户插入自己的代码
  4. 这个结构的好处就是把问题具体化了, 不用考虑其他的, 当前核心问题就是如何求出中间for循环的数据,也就是本集的重点了.
<template>
  <div class='cc-pagination'>
    <button class="btn-prev">
      <slot name='previous'> &lt; </slot>
    </button>
    <ul class='cc-pagination__box'>
      <li>1</li>
      <li>··</li>
      <li ....>{{item}}</li>
      <li>··</li>
      <li v-if="总页数!== 1">{{总页数}}</li>
    </ul>
    <button class="btn-prev">
      <slot name="next"> &gt; </slot>
    </button>
  </div>
</template>

先展示一下基本的样子
图片描述

css 方面


@include b(pagination) {
    cursor: pointer;
    color: #606266; // 这个颜色很柔和的黑
    align-items: center;
    display: inline-flex;
    justify-content: center;
    .btn-prev { // 按钮去掉默认样式
        border: none;
        outline: none;
        background-color: transparent;
        &:hover { // 这个nomal是个柔和的蓝色
            color: $--color-nomal
        }
    }
}

二. 功能的定义

  1. pageTotal: 总的页数, 就是说比这波数据分成700页显示, 那就传进来700,
  2. pageSize: 最多显示多少个分页标示, 比如说 传入了3, pageTotal传入了6, 那就是
    1,2 ... 6 页面上只显示这三个数. 经过很多次实验, 这个数最小也要传5, 不然体验会很差,最大可以传无限, 朋友们有机会可以自己试试.
  3. value: 实现v-model的基本元素
  4. validator: 这个函数是子组件接收参数时的校验函数, 这里不能修改参数, 他只负责告诉用户传的对不对就好了, 不要有太多功能, 逻辑分散的话不好维护.
  5. 下面的代码出现了三个重复的函数, 那么 必须要封装一个共用的工具函数了
pageSize: {
      type: Number,
      default: 5,
      validator: function(value){
      if (value < min || value !== ~~value) {
        throw new Error(`最小为5的整数`);
       }
       return true;
      }
    },
    value: {
      // 选中页
      type: Number,
      required: true,
      validator: function(value){
      if (value < min || value !== ~~value) {
        throw new Error(`最小为1的整数`);
       }
       return true;
      }
    },
    pageTotal: {
      // 总数
      type: Number,
      default: 1,
      required: true,
      validator: function(value){
      if (value < 1 || value !== ~~value) {
        throw new Error(`最小为1的整数`);
       }
       return true;
      }
    }

抽离工具函数
vue-ui/my/vue-cc-ui/src/assets/js/utils.js

// inspect单词就是检测的意思, 暂时业务只需要传入一个最小值;
export function inspect(min) {
// 返回一个函数作为真正的校验函数
  return function(value) {
// 小于这个最小值或不是整数的都要抛错
// ~~这个位运算符的写法的意思就是取整, 取整之后与没取整相等, 当然就不是浮点数
// ~运算符是对位求反,1变0,0变1,也就是求二进制的反码
    if (value < min || value !== ~~value) {
      throw new Error(`最小为${min}的整数`);
    }
    return true;
  };
}

经过抽离, 我这里就可以化简了, 清爽了很多

    pageSize: {
      type: Number,
      default: 5,
      validator: inspect(5)
    },
    value: {
      type: Number,
      required: true,
      validator: inspect(1)
    },
    pageTotal: {
      type: Number,
      default: 1,
      required: true,
      validator: inspect(1)
    }

三. 完善页码的展示(重点)
逐一分析:

  1. 前面说了, 首尾页码已经直接写上了, 所以比如用户定义的pageSize为5 那么我就要取出中间的3个,
    比如用户当前在 第6页, 总页数 12页, 那么 1,...,5,6,7,...,12 中间的567就是我要获取的目标
  2. 本次选择用计算属性来做, 可以监控v-model的实时变化.名为showPages, 供li去循环展示;
  3. 兼容value值出现错误的情况
  4. 这种做法肯定是有偏移的, 比如说 用户输入了pageSize为4, 会出现两种情况让你选择
    1,...,5,6,...,12, 1,...,6,7,...,12 在6被激活时, 到底是要5,还是7这个没必要纠结, 随便写一个就好了, 因为我纠结了一下感觉没意义😖;
  5. 做法思路, 拿到当前要激活的页码value, 然后向他左右延伸, 比如拿到value是 6, 那么左右就是5,7, 这样不断的遍历拿值, 最终在规定数量内, 并且不要触及边界条件.
showPages() {
// 习惯性的定义返回的变量
      let result = [],
      // 拿到所需的变量
        value = this.value,
        pageTotal = this.pageTotal,
        // 因为要去掉头尾, 所以-2
        pageSize = this.pageSize - 2;
      // 防止用户输入错误引起的混乱, 比如用户的缓存, 要返给用户, 让用户去处理, 因为很可能v-model出现死循环
      if (value > pageTotal) {
      // 友好的触发一个错误事件
        this.$emit("error", value, pageTotal);
        value = pageTotal;
      }
      // 如果被激活的页面在1与end之间, 则把value放入数组, 不然的话会出现重复值
      if (value > 1 && value < pageTotal) result.push(value);
      // 左右开弓, 求出当前激活的页码左右的数据
      for (let i = 1; i <= pageSize; i++) {
      // 加法, 所以检测小于总数就行
        if (value + i < pageTotal) {
          result.push(value + i);
          // 随时甄别是否已经符合条件, 取值已够就退出;
          if (result.length >= pageSize) break;
        }
      // 减法, 只要检测大于1就行
        if (value - i > 1) {
          result.unshift(value - i);
          if (result.length >= pageSize) break;
        }
      }
      return result;
    },

上面的li标签 放心遍历了

 <li v-for="item in showPages"
          :key='item'
          :class="{'is-active':value === item}">{{item}}</li>

四. 定义事件

说了这么多, 结构已经做好了, 那么就需要事件的驱动了;

  1. 这个事件负责通知父级改变值, 同时会做相应的校验;
  2. 参数为当前想要激活哪一页
  3. 每次事件都通知父级会有重复的激活, 所以这个方法里面会把想要激活的页码与当前激活的页码进行比较, 放在抛出的事件的第二个参数里面, 用户只要判断isNoChange的真伪就知道是否要请求新数据了, 用户还可以根据这个提示用户"您已在xx页"
 handlClick(page) {
      if (page < 1) page = 1;
      if (page > this.pageTotal) {
        page = this.pageTotal;
      }
      let isNoChange = this.value === page;
      this.$emit("input", page);
      // 当前值, 与当前值相比是否有变化
      this.$emit("onChange", page, isNoChange);
    }
  1. 前进后退直接+1-1就行了
  2. ...按钮要做一下处理, 因为涉及到前进与后退的加减,
  3. ...按钮的点击我设计为跳转到一个当前正好看不到的页面, 当前点不到的就行
  4. 分为两个函数来处理
 // 左侧的...
    previous() {
      // 左侧未显示的第一个
      let page = this.showPages[0];
      this.handlClick(page - 1);
    },
    // 右侧的...
    next() {
    // 右侧未显示的第一个
      let len = this.showPages.length,
        page = this.showPages[len - 1] + 1;
      this.handlClick(page + 1);
    },

把时间放到dom上吧

<template>
  <div class='cc-pagination'>
    <button class="btn-prev"
            @click="handlClick(value-1)"
            // 到头了要提示用户, 显示出禁止点击的样式
            :style="{'cursor': (value === 1)?'not-allowed':'pointer'}">
      <slot name='previous'> &lt; </slot>
    </button>
    <ul class='cc-pagination__box'>
      <li @click="handlClick(1)"
          :class="{'is-active':value === 1}">1</li>
      <li v-if='showLeft'
          @click="previous">··</li>
      <li v-for="item in showPages"
          :key='item'
          @click="handlClick(item)"
          :class="{'is-active':value === item}">{{item}}</li>
      <li v-if='showRight'
          @click="next">··</li>
      <li v-if="pageTotal !== 1"
          @click="handlClick(pageTotal)"
          :class="{'is-active':value === pageTotal}">{{pageTotal}}</li>
    </ul>
    <button class="btn-prev"
            @click="handlClick(value+1)"
            // 到头了要提示用户, 显示出禁止点击的样式
            :style="{'cursor': (value === pageTotal)? 'not-allowed':'pointer'}">
      <slot name="next"> &gt; </slot>
    </button>
  </div>
</template>

为了判断左右的 ...是否显示, 我们也要抽离出判断的逻辑
比如说中间的那个数组两边的元素连接上了, 就不显示.. 否则出现.

showLeft() {
      let { showPages, pageTotal, pageSize } = this;
      // 左边不是2, 并且pageTotal超出规定数才显示, 不然1 ... ... 2 很尴尬
      return showPages[0] !== 2 && pageTotal > pageSize;
    },
    showRight() {
      let { showPages, pageTotal, pageSize } = this,
        len = showPages.length;
      return showPages[len - 1] !== pageTotal - 1 && pageTotal > pageSize;
    }

至此, 功能性的东西才告一段落

五. 丰富样式与效果

  1. 用户可以传background 以激活背景色效果, 看对比图

图片描述
图片描述

  1. 对比一下大量数据的美, 😃哈哈哈, 这个组件的特色就是可以无限多

图片描述
图片描述

代码实现一下

// background是关键字, 尤其涉及到css 不要直接使用
// js里面为了方便用户, 可以适当使用
<ul class='cc-pagination__box'
        :class="{'ground-box':background}">

css
单独抽离出ground样式, 为以后的扩展做准备


.ground {
        background-color: #f4f4f5;
        ;
        border-radius: 4px;
    
        &:hover {
            background-color: $--color-nomal;
            color: white;
        }
    }
    .ground-box { // 背景色是关键字
        &>li {
            @extend .ground;
        }
        &>.is-active{
            background-color: $--color-nomal;
            color: white;
        }
    }

hideOne 属性, 开启只有一页的时候不显示组件

// 最外层的父级定义就好了
 <div class='cc-pagination'
       v-if='!(hideOne && pageTotal === 1)'>

total: 开启左侧显示条数模式
我做的与别人不同, 你传了我就显示, 没传就无所谓, 没有附加的功能

<p v-if="total"
       class="total-number">总共 <span> {{total}}</span> 条</p>

图片描述

麻雀虽小, 五脏俱全, 做这个也花费了半天的时间, 测出好多问题, 都一一改进了.

end
另外最近计划做一个vue,vuex, vue-router, webpack 原理解析的系列,也是一点点从零开始, 期待大家继续一起学习,一起进步, 实现自我价值!!
下一集准备聊聊 计数器...上期就这么说的😓;
更多好玩的效果请关注个人博客:链接描述
github地址:链接描述

阅读 1.1k

推荐阅读