从零开始实现类 antd 分页器(二):分页核心代码

5,110 阅读15分钟

本文是使用 Typescript 开发类 antd 分页器,并发布 npm 的第二篇,因为最近在业务中使用了 antd 中的 Pagination 开发了相关业务,而自己又对组件的封装由很有兴趣,所以打算用 ts 来封装一个具有 antd 所有功能的 Pagination。相信你也能轻轻松松的发布一个 npm 组件了。
相关系列文章
从零开始实现类 antd 分页器(一):搭建项目架构
从零开始实现类 antd 分页器(二):分页核心代码
从零开始实现类 antd 分页器(三):发布npm
写作过程中有些方法的具体实现没有讲到,大家可以自行查看源码。本案例项目仓库:
闲来无事,造个轮子 —— darrell-wheels

 

为了写作方便,antd 的 分页器我统一以 antd-pagination 代替,自己的分页器统一以 my-pagination 代替。

 

效果图

  • 基础 + 更多

  • 跳转页数 [ showQuickJumper ] + 改变页数 [ showSizeChanger ]

  • 迷你 [ size: 'small' ]

  • 简洁 [ simple: true ]

  • 显示总数 [ showTotal ]

  • 修改上一步和下一步相应文字 [ itemRender ]

 

分页逻辑

这里面的逻辑是实现这个分页器最最关键的地方,同时也是分页器的难点所在。

我们要计算出每一页的页码排布情况,什么时候该显示省略号,什么时候改成全部显示等等。

antd 的分页

我试了一下 antd-pagination 的页码显示,假设有 30 页,它的页码是如下分布的,我们以 allPages 代表总页数,current 代表当前是第几页:

// allPages = 30

当 current = 1, 显示 1 2 3 4 5 ... 30
当 current = 2, 显示 1 2 3 4 5 ... 30
当 current = 3, 显示 1 2 3 4 5 ... 30
当 current = 4, 显示 1 2 3 4 5 6 ... 30
当 current = 5, 显示 1 ... 3 4 5 6 7 ... 30

...

当 current = 13, 显示 1 ... 11 12 13 14 15 ... 30
当 current = 14, 显示 1 ... 12 13 14 15 16 ... 30
当 current = 15, 显示 1 ... 13 14 15 16 17 ... 30
当 current = 16, 显示 1 ... 14 15 16 17 18 ... 30

...

当 current = 26, 显示 1 ... 24 25 26 27 28 ... 30
当 current = 27, 显示 1 ... 25 26 27 28 29 30
当 current = 28, 显示 1 ... 26 27 28 29 30
当 current = 29, 显示 1 ... 26 27 28 29 30
当 current = 30, 显示 1 ... 26 27 28 29 30

antd-pagination 有一个参数 showLessItems,当它为 true 时,意思是当前页面 左右显示各 1 个,为 false 时,代表当前页面 左右各显示 2 个。在这里我们把 当前页面 左右显示几个 暂且记为 pageBufferSize

也就是说,antd-paginationpageBufferSize 只有两个值 1 和 2,不过两个已经完全够了。

上面的例子,pageBufferSize = 2,及 showLessItemsfalse

找规律

接着观察上面这一组数据,我们可以找一下规律,粗粗一看,我们会发现:

  • 第 1 页 到 第 3 页 显示的页码是一样的,只有 一个后面省略号
  • 第 4 页 比 前 3 页 多了一个页码,但是还是只有 一个后面省略号(临界点)
  • 第 5 页 开始,到 第 26 页 出现了 两个省略号,并且中间都是 5 个
  • 第 27 页 跟第四页类似 又回到了 一个前面省略号(临界点
  • 第 28 页 到 第 30 页,显示的页面一样,并只有 一个前面省略号

代码实现

我们在这里先暂时不考虑复杂的 dom 输出,只使用简单的字符串输出。

这里笔者想到两种思路:

第一种思路

简单粗暴:就是把用代码翻译上面我们发现的规律,我们可以用 allPagescurrentpageBufferSize 来表示:

// 临界点
当 current = 5,转化为代码 1 + pageBufferSize * 2
当 current = 26,转化为代码 allPages - pageBufferSize * 2

// 在临界点 区域内,都是双省略号
current >= 1 + pageBufferSize * 2 && current <= allPages + pageBufferSize * 2

// 然后在对 第4页 和 第27页做一下特殊处理
// 这两页的页码虽然只有一个省略号,但是比正常的会多出一个页码
当 current = 4,转化为代码 pageBufferSize * 2
当 current = 27,转化为代码 allPages - pageBufferSize * 2 + 1

// 接下来就是最简单的
当 current = 1 || 2 || 3,转化为代码 < pageBufferSize * 2 - 1
当 current = 28 || 29 || 30,> allPages - pageBufferSize * 2 + 1

第二种思路

我们主要来讲一下第二种思路,这个也是 antd-pagination 内使用的方法,我们接下来的方法也只满足 pageBufferSize1 或者 2

我们定义一个函数 showPages 方法来实现相关的逻辑;

/**
  * 输出每一页 显示的页码 的 字符串
  * @param current:当前页码;
  * @param allPages:总页数;
  * @param pageBufferSize:页数左右两边显示几个(1个 或 2个)
  */

function showPages (current, allPages, pageBufferSize) {
  let str = '';
  
  ...
  // 待完成的逻辑
  ...
  
  return str.trim();
}

// 总页数
let total = 30;

// 循环输出每页的页码字符串
for (let i = 1; i <= total; i++) {
  console.log(showPages(i, total, 2));
}

首先 antd-pagination 当总页数小于等于 5 + 2 * pageBufferSize 的时候,不管 current 是 第几页,所有的页码都会展现,我们可以在 showPages 添加如下代码:

// 当总页数 <= 5 + 2 * pageBufferSize

function showPages (current, allPages, pageBufferSize) {
  if (allPages <= 5 + pageBufferSize * 2) {
    for (let i = 1; i <= allPages; i++) {
      str = str + ' ' + i;
    }
  }
}

此时我们设置 allPages = 8,在浏览器中我们可以看到如下图所示:

观察上面的字符串我们发现,当有两个省略号的时候,前面字符肯定是 1 ...,最后两个字符肯定是 ... 30

1. 先循环输出 当前页码 与 页码前后 pageBufferSize 个数

我们在代码里面定义一个 left,代表字符串循环从这个数开始;再定义 right,代表字符串循环到此结束。left 的值根据前几页的 currentpageBufferSize ,我们很简单的可以得到。

let left = Math.max(1, current - pageBufferSize);

同理可以得到 right 的值:

let right = Math.min(current + pageBufferSize, allPages);

因为 前三页后三页 都是显示 5 个数的,所以在这种情况下,我们要对 leftright 在根据 pageBufferSize 做一个调整:

if (current - 1 <= pageBufferSize) {
  right = 1 + pageBufferSize * 2;
}

if (allPages - current <= pageBufferSize) {
  left = allPages - pageBufferSize * 2;
}

接着我们便可以循环输出了:

for (let i = left; i <= right; i++) {
  str = str + ' ' + i;
}

2. 然后再去拼相关的省略号

接下来我们就要给上面的 str 拼省略号了。

这这我们在第一种思路里面讲过,我们可以很快的写出如下代码:

if (current - 1 >= pageBufferSize * 2) {
  str = '... ' + str;
}

if (allPages - current >= pageBufferSize * 2) {
  str = str + ' ...';
}

这里面需要注意一个点,这个判断方法在 pageBufferSize = 1 的时候,在第3页 和 allPages - 2 页会出现问题,如图所示:

所以我们要在加上对这个的判断:

if (current - 1 >= pageBufferSize * 2 && current !== 1 + 2) {
  str = '... ' + str;
}

if (allPages - current >= pageBufferSize * 2 && current !== allPages - 2) {
  str = str + ' ...';
}

3. 最后再去比较第一个值和最后一个值 是不是 1 和 总页数。

省略号我们加完之后,我们就只剩最后一步了。我们需要判断 第一步的 leftright 是不是第 1 页和 最后一页,我们可以写出以下代码:

if (left !== 1) {
  str = 1 + ' ' + str;
}

if (right !== allPages) {
  str = str + ' ' + allPages;
}

到这里我们的分页逻辑算是大功告成,完整的代码如下:

function showPages (current, allPages, pageBufferSize) {
  let str = '';

  if (allPages <= 5 + pageBufferSize * 2) {
    for (let i = 1; i <= allPages; i++) {
      str = str + ' ' + i;
    }
  } else {
    let left = Math.max(1, current - pageBufferSize);
    let right = Math.min(current + pageBufferSize, allPages);

    if (current - 1 <= pageBufferSize) {
      right = 1 + pageBufferSize * 2;
    }

    if (allPages - current <= pageBufferSize) {
      left = allPages - pageBufferSize * 2;
    }

    for (let i = left; i <= right; i++) {
      str = str + ' ' + i;
    }

    if (current - 1 >= pageBufferSize * 2 && current !== 1 + 2) {
      str = '... ' + str;
    }

    if (allPages - current >= pageBufferSize * 2 && current !== allPages - 2) {
      str = str + ' ...';
    }

    if (left !== 1) {
      str = 1 + ' ' + str;
    }

    if (right !== allPages) {
      str = str + ' ' + allPages;
    }
  }

  return str.trim();
}

打开浏览器,分别测试一下 pageBufferSize12 时,如下图:

总算是搞定了,终于可以快乐的进行下一步的开发了。

大家如果还有什么不懂,还可以看一下这篇文章 十五分钟--分页逻辑--包学包会,写的很详细。

 

基本架构

插件的基本架构

首先我们在 src/components/pagination/src 下创建 pagination,并新建 index.ts 文件,

import '../../../../styles/normal.less';
import '../../../../styles/pagination/index.less';
//  默认配置
import defaults from "./defaults";
import { PaginationConfig } from '../../../../types/index';

class Pagination {
  private options: PaginationConfig;
  private pageElement: any;
  private total: any;
  private current: any;
  private pageCount: any;
  private pageSize: any;
  
  constructor(selector: any, options = {}) {
    // 默认配置
    this.options = Object.assign(defaults, options);
    this.init(selector);
  }
  
  /**
   * 初始化相应的 分页函数
   * @param selector
   */
  private init (selector:any) {

    // 分页器元素
    this.pageElement = ?(selector)[0];

    // 数据总数
    this.total = this.options.total;
    // 当前页码
    this.current = this.options.current || this.options.defaultCurrent;
    // 一页显示多少数据
    this.pageSize = this.options.pageSize || this.options.defaultPageSize;
    // 总页数
    this.pageCount = Math.ceil(this.total / this.pageSize);

    // 渲染相应的 Dom
    this.renderPages();

    // 改变页数并触发事件
    this.changePage();
  }
}

同时我们在 src/types 文件夹下新建 index.ts,此文件定义了 my-pagination 参数的接口:

// 一些配置参数
// 解释在 option.ts 上

export interface PaginationConfig {
  // 当前页数
  current?: number;

  // 默认第几页
  defaultCurrent?: number;

  // 默认的 pageSize
  defaultPageSize?: number;

  // 是否 disables
  disabled?: boolean;

  // 在只有一页的时候,是否隐藏
  hideOnSinglePage?: boolean;

  // 用于自定义页码的结构,可用于优化 SEO
  itemRender?: (
    page: number,
    type: 'page' | 'prev' | 'next' | 'jump-prev' | 'jump-next',
    originalElement: any
  ) => any;

  // 每页条数
  pageSize?: number;

  // 指定每页可以显示多少条
  pageSizeOptions?: string[];

  // 这个是页数周围各显示几页;true:2;false:1;
  showLessItems?: boolean;

  // 是否可以快速跳转至某页
  showQuickJumper?: boolean;

  // 是否可以改变 pageSize
  showSizeChanger?: boolean;

  // 用于显示数据总量和当前数据顺序
  showTotal?: (total: number, ranger: any) => any;

  simple?: boolean; // 当添加该属性时,显示为简单分页

  // 当为「small」时,是小尺寸分页
  size?: string;

  // 数据总数
  total: number;

  // 页码改变的回调,参数是改变后的页码及每页条数
  onChange?: (page: number, pageSize: number) => void;

  //pageSize 变化的回调
  onShowSizeChange?: (current: number, size: number) => void;

}

首先我们在 src/components/pagination/src 新建 defaults.ts 文件,定义分页器的默认参数:

import { PaginationConfig } from '../../../../types/index'
import { noop } from '../../../../helpers/utils'

const defaults: PaginationConfig = {
  defaultCurrent: 1,
  defaultPageSize: 10,
  hideOnSinglePage: false,
  pageSizeOptions: ['10', '20', '30', '40'],
  showLessItems: false,
  showQuickJumper: false,
  showSizeChanger: false,
  size: '',
  total: 0,
  onChange: noop,
  onShowSizeChange: noop,
}

export default defaults;

 

插件的工具函数

我们在 src/helpers 新建 utils.ts,包含了一些在代码中使用到的工具函数:

  • noop:空函数
  • addClass(elem: any, className: any):添加 class 名字
  • removeClass(elem: any, className: any):移除 class 名字
  • hasClass(elem: any, className: any):是否拥有相应的 class
  • addEvent(element: any, type: any, handler: any):添加事件的方法函数
  • removeHandler(element: any, type: any, handler: any):移除事件的方法函数
  • ?(selector:any, context?:any):模仿 jQuery $()

渲染 Dom

Dom 结构

为了更加清晰的讲一下 dom 结构,我们可以看一下完成后是怎么样的。如下图:

我们可以看到分页器的 Dom 结构是 以 ul 为包裹元素,每一个子元素都是一个 li,在这里有两种情况。

  • li 是 页数,在 li 之中是 a 标签,标签里页数。
  • li 是 操作页数的按钮(上一页/下一页),在 li 之中是 a 标签,标签上是相应的符号。

渲染

  • 首先完善上面的 renderPages 函数。
/**
 * 渲染分页器
 */
private renderPages () {
  if (!this.pageElement) return console.error('请设置包裹容器');
  
  this.pageElement.innerHTML = ""; // 清空 html
  
  const current = this.current;
  const pageSize = this.pageSize;
  const total = this.total;
  const pageCount = this.pageCount;
  
  let fragment: any = ''; // 创建代码片段
  
  fragment = this.showPages();
  
  let UlEle = document.createElement("ul"); // 创建包裹元素

  UlEle.appendChild(fragment);
  UlEle.setAttribute('class', 'darrell-pagination');
  
  this.pageElement.appendChild(UlEle); // 塞入到 分页器中
}
  • 完善 showPages 函数,此函数将 分页逻辑 里面得出的字符串输出的转化为 Dom 输出:
/**
 * 通过页数,渲染相应的 分页 html
 */
private showPages () {
  // 当前页数
  const current = this.current;
  // 总页数
  const allPages = this.pageCount;
  // 页数周围显示几个
  const pageBufferSize = this.options.showLessItems ? 1 : 2;
  // 代码片段
  let fragment = document.createDocumentFragment();

  if (allPages <= 5 + pageBufferSize * 2) {
    // 通过 renderDom 渲染 html
    const fragmentHtml = this.renderDom(1, allPages);
    fragment.appendChild(fragmentHtml);
  } else {
    let left = Math.max(1, current - pageBufferSize);
    let right = Math.min(current + pageBufferSize, allPages);

    if (current - 1 <= pageBufferSize) {
      right = 1 + pageBufferSize * 2;
    }

    if (allPages - current <= pageBufferSize) {
      left = allPages - pageBufferSize * 2;
    }

    // 通过 renderDom 渲染 html
    const fragmentHtml = this.renderDom(left, right);
    fragment.appendChild(fragmentHtml);

    if (current - 1 >= pageBufferSize * 2 && current !== 1 + 2) {
      // 添加前面的省略号
      this.addFragmentBefore(fragment, [Pagination.PAGE_INFOS[2]]);
    }

    if (allPages - current >= pageBufferSize * 2 && current !== allPages - 2) {
      // 添加后面的省略号
      this.addFragmentAfter(fragment, [Pagination.PAGE_INFOS[3]]);
    }

    if (left !== 1) {
      // 如果不是第一页,插入第一页
      this.addFragmentBefore(fragment, [
        this.getPageInfos('darrell-pagination-item', 1)
      ]);
    }

    if (right !== allPages) {
      // 如果不是最后一页,插入最后
      this.addFragmentAfter(fragment, [
        this.getPageInfos('darrell-pagination-item', allPages)
      ]);
    }
  }

  // 添加 前一页的 dom
  this.addFragmentBefore(fragment, [Pagination.PAGE_INFOS[0]]);
  // 添加 后一页的 dom
  this.addFragmentAfter(fragment, [Pagination.PAGE_INFOS[1]]);

  return fragment;
}
  • 完善 renderDom 函数,这个主要用来渲染页码;同时完善 addFragmentBeforeaddFragmentAfter 函数,向代码片段前后插入代码。
/**
 * 循环渲染相应的 Li 元素
 * @param begin 
 * @param end 
 */
private renderDom (begin: number, end: number) {
  let fragment = document.createDocumentFragment();
  let str = "";
  for (let i = begin; i <= end; i++) {
    this.addFragmentAfter(fragment, [
      this.getPageInfos('darrell-pagination-item', i);
    ]);
  }
  return fragment;
}

/**
 * 往 html 片段中 前 添加 html
 * @param fragment 
 * @param datas 
 */
private addFragmentBefore (fragment: any, datas: any) {
  fragment.insertBefore(this.createLiHtml(datas), fragment.firstChild);
}

/**
 * 往 html 片段中 后 添加 html
 * @param fragment 
 * @param datas 
 */
private addFragmentAfter (fragment: any, datas: any) {
  fragment.appendChild(this.createLiHtml(datas));
}
  • 完善 createLiHtml 函数,这个是最终我们生成 Dom 节点的函数。 在之前的所有函数我们其实都没有真正意义上生成 liDom 节点。
/**
 * 创建 Li 元素
 * @param liItemInfo 
 */
private createLiHtml (liItemInfo: Array<any>) {
    let fragment = document.createDocumentFragment();
    // 创建 li 标签
    let liEle = document.createElement("li");

    // liItemInfo 会传入相应的 id,className,和 a 标签中的内容
    const id = liItemInfo[0].id;
    const className = liItemInfo[0].className;
    const content = liItemInfo[0].content;

    // 当前页码
    const current = this.current;

    // 添加样式、id、与 页码
    liEle.setAttribute('class', `${className} liEventTarget`);
    liEle.setAttribute('id', id);
    liEle.setAttribute('data-page', content);

    let aEle;

    if (id === 'prev') {
      // 渲染上一页的 icon,getEveryIconType() 函数是返回一个 icon 的 dom
      aEle = this.getIcon(this.getEveryIconType().prevIconEle)
    } else if (id === 'next') {
      // 渲染下一页的 icon
      aEle = this.getIcon(this.getEveryIconType().nextIconEle)
    } else if (id === 'jump-prev') {
      // 渲染上一批的 icon
      aEle = this.getIcon(this.getEveryIconType().jumpPrevWrapEle)
    } else if (id === 'jump-next') {
      // 渲染下一批的 icon
      aEle = this.getIcon(this.getEveryIconType().jumpNextWrapEle)
    } else if (id === 'page') {
      // 渲染页数
      if (current === parseInt(content, 10)) {
        // 高亮选中的页数
        addClass(liEle, Pagination.CLASS_NAME.ITEM_ACTIVE);
      }

      let aEleNew = document.createElement("a");
      aEleNew.innerHTML = content;

      aEle = aEleNew
    }

    // 添加到代码片段中
    liEle.appendChild(aEle);
    fragment.appendChild(liEle);

    return fragment;
}

 

一些说明

上面我们列出的函数里有些函数没有讲到,具体是如下的作用。

  • Pagination.PAGE_INFOS:返回 前一步/后一步/前几步/后几步 的 id 与 样式

  • getPageInfos :返回页数的数据

  • CLASS_NAME:样式名的定义

  • this.getEveryIconType():此函数返回图标的相关 dom ,如图:

 

事件绑定

事件绑定我们可以通过 事件委托 来做。

事件委托就是利用冒泡的原理,把事件加到父元素或祖先元素上,触发执行效果,大家可以参考

事件绑定、事件监听、事件委托,讲的很清楚。

我们将事件绑定在 this.pageElement 上,同时判断元素 li 上的 id 属性来进行分发事件:

import { addEvent } from '../../../../helpers/utils';
/**
 * 改变页数
 */
private changePage () {
    if (!this.pageElement) return;
    // 获取根元素
    let pageElement = this.pageElement;
    // 给根元素绑定事件
    addEvent(pageElement, 'click', (ev: any) => {
      let e = ev || window.event;
      let target = e.target || e.srcElement;
			
      // 通过 a 元素,找到其父元素 li 元素。
      const parentNode = target.parentNode;

      if (parentNode && parentNode.nodeName.toLocaleLowerCase() == "li") {
        if (parentNode.id === "prev") {
          // 前一页
          if (!this.hasPrev()) return false;
          this.prevPage();
        } else if (parentNode.id === "next") {
          if (!this.hasNext()) return;
					// 后一页
          this.nextPage();
        } else if (parentNode.id === "jump-prev") {
          // 前一批
          this.jumpPrevPage();
        } else if (parentNode.id === "jump-next") {
          // 后一批
          this.jumpNextPage();
        } else if (parentNode.id === "page") {
          // 页面跳转
          const page = parentNode.dataset.page;
          this.goPage(parseInt(page, 10));
        } else {
          // 直接 return,不做任何事情
          return;
        }
        // 重新渲染分页器
        this.renderPages();
      }
    });
}

里面涉及到的页面跳转函数我这里就不列举了,具体可以看源码。

 

回调事件

回调事件有两个:

  • onChange:页数改变
  • onShowSizeChangepageSize 变化的回调

我们先来实现 onChangeonShowSizeChange 我们在后面会讲到。

其实很简单,只要在 changePage 函数里面,重新渲染函数 renderPages 之后,执行一些回调函数就行:

private changePage () {
  ...
  // 重新渲染分页器
  this.renderPages();

  // 如果有参数有 onChange,就将 页数,和 pagesize 传入执行
  this.options.onChange && this.options.onChange(this.current, this.pageSize);
  ...
}

 

my-pagination 更多参数

更多参数其实我们都是围绕 renderPages 这个函数进行的。

disabled

禁用分页器,其实就是在包裹元素 ul 加上 darrell-pagination-disabled 样式。同时在事件绑定的地方,需要加上分页器是 disabled,就不绑定相应的事件

/**
 * 渲染分页器
 */
private renderPages () {
  ...
  const isDisabled = this.options.disabled;
  ...
  
  let UlEle = document.createElement("ul");

  UlEle.appendChild(fragment);
  UlEle.setAttribute('class', 'darrell-pagination');
  
  if (isDisabled) {
    addClass(UlEle, 'darrell-pagination-disabled');
  }
  ...
}
  
/**
 * 改变页数
 */
private changePage () {
  ...
  const isDisabled = this.options.disabled;
  // 不是 disabled 时,做事件绑定
  if (!isDisabled) {
    // 做事件绑定
  }
}

hideOnSinglePage

只有一页时隐藏,只要在 renderPages 中,渲染 dom 之前判断 总页数是否为一

/**
 * 渲染分页器
 */
private renderPages () {
  ...
  const hideOnSinglePage = this.options.hideOnSinglePage;

  // 如果是 一页,就直接不去渲染 dom 了。
  if (hideOnSinglePage) {
    if (pageCount === 1) {
      return false;
    }
  }
  
  ...
}

size

size=small 时,在包裹元素 ul 加上 mini 就行

/**
 * 渲染分页器
 */
private renderPages () {
  ...
  const isSmall = this.options.size;
  ...
  
  let UlEle = document.createElement("ul");

  UlEle.appendChild(fragment);
  UlEle.setAttribute('class', 'darrell-pagination');
  
  if (isSmall) {
    addClass(UlEle, 'mini');
  }
  ...
}

simple

当其为 true 时,我们只需要简单的展示当前页数和总页数就行了

/**
 * 渲染分页器
 */
private renderPages () {
  ...
  const simple = this.options.simple;
  ...
  
  let fragment: any = ''; 

  if (!simple) {
    fragment = this.showPages();
  } else {
    // 渲染简单分页的 dom
    fragment = this.simplePages();
  }
  ...
}

this.simplePages() 这个参数时用来生成简单分页的 dom 的,具体可以看源码

total

数据总数,用于显示数据总量和当前数据顺序,是一个函数,输出一个以 页数总页数 相关的字符串。

/**
 * 渲染分页器
 */
private renderPages () {
  ...
  const showTotal = this.options.showTotal;
  ...
  
  // 有 showTotal 并且不是 simple
  if (showTotal && typeof showTotal === 'function' && !simple) {
    let LiTextEle = document.createElement("li");
    // 加上相关的样式
    LiTextEle.setAttribute('class', 'darrell-pagination-total-text');

    // 将总页数,和这页显示从第几条开始到第几条结束 的 range 传入 showTotal
    const TotalText = showTotal(
      this.total,
      [
        this.total === 0 ? 0 : (current - 1) * pageSize + 1,
        current * pageSize > total ? total : current * pageSize,
      ]
    );

    LiTextEle.innerHTML = TotalText;

    UlEle.insertBefore(LiTextEle, UlEle.firstChild);
  }
  ...
}

具体字符串有使用者定义 ,

showQuickJumper

是否可以快速跳转至某页,我们要生成一个快速跳转的 dom,插入到包裹容器之中 。

/**
 * 渲染分页器
 */
private renderPages () {
  ...
  const showQuickJumper = this.options.showQuickJumper;
  ...
  
  if (showQuickJumper && !simple) {
    // 生成 快速跳转至某页的 dom
    let quickJumperEle = this.createQuickJumperDom();
    UlEle.appendChild(quickJumperEle);
  }
  ...
}

this.createQuickJumperDom():这个函数是快速生成到某页的 Dom,具体可以看源码

showSizeChanger

是否可以改变 pageSize,我们要生成一个页面改变的类 select 元素,插入到包裹容器之中。

关于类 select 对象,我的做法是新建一个 select 类,做法和分页器一样,有时间我会在写一个仿 Antdselect 框,使用就是 new 一个 select 对象,并传入 包裹 id 和 相关的参数。

/**
 * 渲染分页器
 */
private renderPages () {
  ...
  const showSizeChanger = this.options.showSizeChanger;
  ...
  
  if (showSizeChanger && !simple) {
    // 放改变 size 的框
    let LiEle = document.createElement("li");
    LiEle.setAttribute('id', 'select');
    LiEle.setAttribute('class', 'darrell-pagination-options');

    UlEle.appendChild(LiEle);
  }
  
  ...
  
  this.pageElement.appendChild(UlEle);
  
  if (showSizeChanger && !simple) {
    new Select('#select', {
      // 当前的 pageSize
      value: this.pageSize,
      // 当前页数
      currentPage: this.current,
      // 传入是否 disabled
      disabled: isDisabled,
      // 执行 pageSize 改变后的回调
      onShowSizeChange: (current: number, size: number) => {
        // pageSize
        this.pageSize = size;

        // 总页数
        this.pageCount = Math.ceil(this.total / this.pageSize);

        // 当前页数
        if (current > this.pageCount) {
          current = this.pageCount;
        } else if (current < 1) {
          current = 1
        }

        this.current = current;
        // 重新渲染 分页器
        this.renderPages();
        // 执行 pageSize 改变后的回调
        this.options.onShowSizeChange && this.options.onShowSizeChange(current, size);
      },
    });
  }
  
  ...
}

select 类,我写在了 src/components/pagination/src/select 这个文件夹下,用于生生相应的 select 元素,具体可以看源码

itemRender

就是我们可以自定义分页器的文案,增强 seo,其本身也是一个函数,接受三个参数 current:当前页数;type:按钮类型;originalElement:元素的 dom;从而使我们能自定义文字内容。

这个函数运行的时机是在渲染元素时候去做,即在函数 createLiHtml

/**
 * 默认的 render 方法
 */
static defaultItemRender = (page: number, type: 'page' | 'prev' | 'next' | 'jump-prev' | 'jump-next', element: any) => {
    return element;
}

/**
 * 创建 Li 元素
 * @param liItemInfo 
 */
private createLiHtml (liItemInfo: Array<any>) {
  // itemRender 函数
  const itemRender = this.options.itemRender || Pagination.defaultItemRender;

  if (id === 'prev') {
    ...
    // 前一页
    aEle = itemRender(
      this.getPrevPage(),
      'prev',
      this.getIcon(this.getEveryIconType().prevIconEle)
    )
  } else if (id === 'next') {
    ...
    // 后一页
    aEle = itemRender(
      this.getNextPage(),
      'next',
      this.getIcon(this.getEveryIconType().nextIconEle)
    )
  } else if (id === 'jump-prev') {
    ...
    // 前一批
    aEle = itemRender(
      this.getJumpPrevPage(),
      'jump-prev',
      this.getIcon(this.getEveryIconType().jumpPrevWrapEle)
    )
  } else if (id === 'jump-next') {
    ...
    // 后一批
    aEle = itemRender(
      this.getJumpNextPage(),
      'jump-next',
      this.getIcon(this.getEveryIconType().jumpNextWrapEle)
    )
  } else if (id === 'page') {
    ...
    // 渲染页数
    aEle = itemRender(
      parseInt(content, 10),
      'page',
      aEleNew,
    )
  }
  ...
}

this.getIcon:这个方法是返回 icondom 元素

this.getEveryIconType:上面我们有讲到过,具体可以看源码

 

my-pagination 样式

对于样式,笔者在这里就不细说了,主要就是沿用了 antd-pagination 的样式,同时还增加了 normal.css,用于对页面的初始化。具体的大家可以参考样式源码

 

小结

这一节我们详细的讲了一下分页核心逻辑的开发,从思路分析到具体的代码实现,同时笔者也参考了 手把手教你用原生JavaScript造轮子十五分钟--分页逻辑--包学包会 这两篇文章,那么接下去我们就可以将组件发布到 npm 上去了,大家可以移步下一节 从零开始实现类 antd 分页器(三):发布npm 查看具体的 npm 发布流程。

 

参考内容