1
最近在业务中使用了 antd 中的 Pagination 开发了相关业务,而自己又对组件的封装由很有兴趣,所以打算用 ts 来封装一个具有 antd 所有功能的 Pagination
文章有点长,但是看完之后,相信你也能轻轻松松的发布一个 npm 组件了。
源码地址:darrell-wheels,写作过程中有些方法的具体实现没有讲到,大家可以自行查看源码。

 

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

 

效果图

  • 基础 + 更多

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

  • 迷你 [ size: 'small' ]

  • 简洁 [ simple: true ]

  • 显示总数 [ showTotal ]

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

 

项目初始化

.
├── src        // 组件源代码目录
    ├── components     // 轮子的目录
        ├──pangation
           ├── example     // 在这里我放了 分页器的 html dom 片断
           ├── src     // 分页器源代码
           └── README.md
    ├── helpers  // 工具函数目录
    ├── types  // typescripe 的接口定义
    └── styles   // 样式文件目录
├── node_modules // 第三方的依赖
├── config  // webpack配置
    ├── webpack.base.js // 公共配置
    ├── webpack.dev.config.js // 开发环境配置
    └── webpack.prod.config.js // 打包发布环境配置
├── example    // 开发时预览代码
    └── src    // 示例代码目录
        ├── app.js     // 入口 js 文件
        └── index.html // 入口 html 文件
├── lib // 组件打包结果目录
├── .babelrc // babel 配置文件
├── .gitignore // git上传时忽略的文件
├── .npmignore // npm 发布时忽略的文件
├── index.html // 项目 html 模版
├── README.md
├── tsconfig.json // ts 的配置文件
├── webpack.dev.config.js // webpack 配置文件
├── package-lock.json
└── package.json // 当前整一个项目的依赖

 

安装依赖

  • 安装 babel 编译相关的依赖:
npm i @babel/cli @babel/core @babel/preset-env -D
  • 项目采用 webpack 做构建,同时使用 webpack-dev-server 作为本地开发服务器
npm install webpack webpack-cli webpack-dev-server webpack-merge -D
  • 项目采用 ts 开发,安装 typescripttslint
npm install typescript ts-loader tslint tslint-loader tslint-config-prettier tslint-config-standard -D
  • 项目样式使用 less,需要安装 less 环境,同时安装 相应的编译 css 的文件
npm install less less-loader style-loader css-loader -D
  • 编译图片、字体相应
npm install file-loader url-loader -D
  • 安装相应的 webpackplugins,下面的 plugins 的作用我就不细说了。
npm install uglifyjs-webpack-plugin html-webpack-plugin clean-webpack-plugin mini-css-extract-plugin -D

执行完以上命令,此时 package.json 中包含的依赖信息如下:

{
  "devDependencies": {
    "@babel/cli": "^7.8.3",
    "@babel/core": "^7.8.3",
    "@babel/preset-env": "^7.8.3",
    "clean-webpack-plugin": "^0.1.19",
    "css-loader": "^1.0.0",
    "file-loader": "^2.0.0",
    "html-webpack-plugin": "^3.2.0",
    "less": "^3.10.3",
    "less-loader": "^5.0.0",
    "mini-css-extract-plugin": "^0.9.0",
    "style-loader": "^0.21.0",
    "ts-loader": "^6.2.1",
    "tslint": "^5.20.1",
    "tslint-config-prettier": "^1.15.0",
    "tslint-config-standard": "^8.0.1",
    "tslint-loader": "^3.5.4",
    "typescript": "^3.7.3",
    "uglifyjs-webpack-plugin": "^1.2.7",
    "url-loader": "^3.0.0",
    "webpack": "^4.16.3",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.5",
    "webpack-merge": "^4.2.2"
  },
  "dependencies": {}
}

 

配置 webpack 和 babel

配置 webpack

对于 webpack 的配置我们区分:

  • 开发环境配置文件webpack.dev.config.js
  • 打包发布环境配置文件webpack.prod.config.js

我们将两个文件中 公共配置 抽出来放在 webpack.base.js

const path = require('path');

module.exports = {
  resolve: {
    extensions: [ '.ts', '.tsx', '.js', '.json']
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: ['ts-loader']
      }, {
        test: /\.(ts|tsx)?$/,
        exclude: /node_modules/,
        use: ['tslint-loader'],
      }, {
        test: /\.less$/,
        exclude: /node_modules/,
        use: [{
          loader: 'style-loader'
        }, {
          loader: 'css-loader'
        }, {
          loader: 'less-loader'
        }]
      }, {
        test: /\.(eot|woff2?|woff|ttf|svg|otf)$/,
        use: ['file-loader'],
      }
    ]
  },
};
  • 开发时采用的 webpack 配置写在 webpack.dev.config.js
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.js'); // 引用公共配置
const CleanWebpackPlugin = require('clean-webpack-plugin'); //每次构建清理dist目录
const HtmlWebpackPlugin = require('html-webpack-plugin');

const devConfig = {
  mode: 'development', // 开发模式
  devtool: 'cheap-module-eval-source-map',
  entry: path.join(__dirname, "../example/src/app.js"), // 项目入口,处理资源文件的依赖关系
  output: {
    path: path.join(__dirname, "../example/src/"),
    filename: "bundle.js",
    // 使用webpack-dev-sevrer启动开发服务时,
    // 并不会实际在`src`目录下生成bundle.js,打包好的文件是在内存中的,但并不影响我们使用。
  },
  module: {
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, '../index.html'),
      filename: 'index.html',
    }),
    new CleanWebpackPlugin(['dist']),
  ],
  devServer: {
    contentBase: path.join(__dirname, '../example/src/'),
    compress: true,
    port: 3001, // 启动端口为 3001 的服务
    open: true // 自动打开浏览器
  },
};

// 将baseConfig和devConfig合并为一个配置
module.exports = merge(devConfig, baseConfig);
  • 打包时采用的 webpack 配置写在 webpack.prod.config.js
const path = require('path');
const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const baseConfig = require('./webpack.base.js'); // 引用公共配置

const prodConfig = {
  mode: 'production', // 生产模式
  devtool: 'cheap-module-source-map',
  entry: path.join(__dirname, "../src/index.ts"), // 项目入口,处理资源文件的依赖关系
  output: { // 出口文件
    path: path.resolve(__dirname, '../lib/'),
    filename: "darrellWheels.min.js",
    libraryTarget: 'umd',     // 采用通用模块定义
    library: 'darrellWhells'
  },
  plugins: [
    new UglifyJsPlugin({
      test: /\.js($|\?)/i
    }),
  ],
  module: {
    rules: [
      {
        test: /\.less$/,
        exclude: /node_modules/,
        use: [{
          loader: 'style-loader'
        }, {
          loader: 'css-loader'
        }, {
          loader: 'less-loader'
        }]
      },
    ]
  }
};

module.exports = merge(prodConfig, baseConfig); // 将baseConfig和devConfig合并为一个配置
  • 最后修改 package.json scripts,以便我们能更好的对项目进行 打包运行发布。如下:
...
"scripts": {
  "start": "webpack-dev-server --config config/webpack.dev.config.js",
  "build": "webpack --config config/webpack.prod.config.js",
  "pub": "npm run build && npm publish"
},
...

 

配置 babel

其实这里我们不需要配置,因为我们使用 ts-loader,他已经帮我们做了 babel 的事情

我们需要使用 babel 把我们的代码编译成 es5 版本。在项目根目录下建好的 .babelrc文件内补充以下内容:

{
  "presets": ["@babel/preset-env"]
}

开发一个最简单的组件

  • 首先我们在 src/components/pagination/src 下新建一个 index.ts,作为 my-pagination 的入口文件,我们简单的在页面中输出一行字
// src/components/pagination/src

function pagination() {
  var dom = document.getElementById('pagination');
  var pagination = document.createElement('div');
  pagination.innerText = '分页轮子的方法';
  dom.append(pagination);
}

export default pagination;
  • 我们在 src/index.ts 下将轮子导出
// src/index.ts
import Pagination from './components/pagination/src/index';
export { Pagination };
  • 我们修改根目录下的 模板 index.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>分页</title>
</head>
<body>
  <div id="pagination"></div>
</body>
</html>
  • 接着我们修改 example/src 下的 app.js 文件
import { Pagination } from '../../src/index'; // 引入组件

Pagination(); // 运行

最后我们启动服务,在命令行中输入:npm start

我们看到在页面中出现了 分页轮子的方法,如图所示:

在开发过程(使用 webpack-dev-sevrer 启动开发服务)时,并不会实际在 src 目录下生成 bundle.js,打包好的文件是在内存中的,如果你想看 在 example 下面生成了什么的话,你可以做如下改动:

- "start": "webpack-dev-server --config config/webpack.dev.config.js",
+ "start": "webpack --config config/webpack.dev.config.js",

项目初始化就暂时讲到这里,如果还有什么问题,大家可以自行去看一下源码配置。

接下来我们就来讲一下 分页的逻辑

更多的 webpack 配置,可以看笔者最近正在写的 webpack4.0 学习文档

 

分页逻辑

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

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

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,用于对页面的初始化。具体的大家可以参考样式源码

 

发布到 npm

组件打包

我们修改 package.json 中的 main 字段,它的作用是声明组件的入口文件。

开发者在 import 我们的组件的时候会引入main 字段中 export 的内容。

到现在为止我们的 package.json 内容如下:

{
  "name": "darrell-wheels",
  "version": "1.0.6",
  "description": "some wheels",
  "main": "lib/index.js",
  "scripts": {
    "start": "webpack-dev-server --config config/webpack.dev.config.js",
    "build": "webpack --config config/webpack.prod.config.js",
    "pub": "npm run build && npm publish"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/darrell0904/darrell-wheels.git"
  },
  "keywords": [
    "wheels",
    "pagination"
  ],
  "author": "darrell",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/darrell0904/darrell-wheels/issues"
  },
  "homepage": "https://github.com/darrell0904/darrell-wheels#readme",
  "devDependencies": {
    "@babel/cli": "^7.8.3",
    "@babel/core": "^7.8.3",
    "@babel/preset-env": "^7.8.3",
    "clean-webpack-plugin": "^0.1.19",
    "css-loader": "^1.0.0",
    "file-loader": "^2.0.0",
    "html-webpack-plugin": "^3.2.0",
    "less": "^3.10.3",
    "less-loader": "^5.0.0",
    "mini-css-extract-plugin": "^0.9.0",
    "style-loader": "^0.21.0",
    "ts-loader": "^6.2.1",
    "tslint": "^5.20.1",
    "tslint-config-prettier": "^1.15.0",
    "tslint-config-standard": "^8.0.1",
    "tslint-loader": "^3.5.4",
    "typescript": "^3.7.3",
    "uglifyjs-webpack-plugin": "^1.2.7",
    "url-loader": "^3.0.0",
    "webpack": "^4.16.3",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.5",
    "webpack-merge": "^4.2.2"
  },
  "dependencies": {}
}

 

组件测试

在发布到 npm 之前,我们需要自己先测试一下组件的功能。

  • 我们可以使用 npm link 把打包之后的组件引入到全局 node_modules 中,在根目录下运行命令:
npm run build
npm link

  • 接着我们进入到 example/src 文件,使用如下命令,将刚刚引入到全局的 darrell-wheels 映射到项目的 node_modules 中去:
cd example/src
npm link react-demo-component
  • 接着修改 example/src/app.js 中的内容:
// import { Pagination } from '../../src/index';
import { Pagination } from 'darrell-wheels';

new Pagination('#pagination', {
  total: 500,

  onChange: (page, pageSize) => {
    console.log('---page---', page);
    console.log('---pageSize---', pageSize);
  },
  onShowSizeChange: (page, size) => {
    console.log('---page--11-', page);
    console.log('---size--11-', size);
  },
});
  • 我们可以看到页面中出现了分页组件,如下图:

到这里我们组件内部测试算是通过了,接下去我们就发布到 npm 上去。

 

发布组件

配置 .npmignore

此文件的作用就是:指定发布 npm 的时候需要忽略的文件和文件夹。如果省略掉这一项,所有文件包括源代码会被一起上传到 npm,如下所示。

# 指定发布 npm 的时候需要忽略的文件和文件夹
# npm 默认不会把 node_modules 发上去

config # webpack配置
example # 开发时预览代码
src # 组件源代码目录
.babelrc # babel 配置

注册 npm 账号并发布

注册 npm 账号,有一点我们需要注意,一半我们在使用 npm 的时候 会用 taobao 的镜像,因为这样速度会比较快,但是当我们传到 npm 时,一定要切换到官方源。

  • 我们可以通过 npm config list 查看当前使用的源地址,通过以下命令切换 npm 源:
npm config set registry http://registry.npmjs.org
  • 我们也可以使用 nrm 来更方便的进行源管理,具体我这里就不详细说了,大家可以看:使用NRM进行NPM的源管理
  • 最后我们使用 npm run pub 命令发布组件到 npm

当看到下图,说明发布成功了:

我们在官网上搜索 darrell-wheels,可以看到如图:

 

在项目中使用

我们在 react 项目中使用一下,在这里我们就使用 create-react-app 快速搭建一个 react 项目:

// 全局安装 create-react-app
npm install create-react-app -g

// 使用 create-react-app 创建 my-app
create-react-app my-app

// 创建成功后,我们进入 my-app,并启动项目
cd my-app
npm start

出现下图,说明项目启动成功:

接着我们安装 darrell-wheels

npm install darrell-wheels -D

最后我们修改一下 App.js 文件:

import React from 'react';
import './App.css';
import { Pagination } from 'darrell-wheels';

function itemRender(current, type, originalElement) {

  if (type === 'prev') {
    return <a>上一个</a>;
  }
  if (type === 'next') {
    return <a>下一个</a>;
  }

  if (type === 'page') {
    return <a>{`第${current}个`}</a>
  }
  return originalElement;
}

class App extends React.Component {

  componentDidMount() {
    if (this.Pagination) {

      new Pagination('#Pagination', {
        total: 500,
        showTotal: (total: any, range: any) => {
          return `${range[0]}-${range[1]} of ${total} items`;
        },
        simple: false,
        onChange: (page: any, pageSize: any) => {
          console.log('---onChange---', page);
          console.log('---onChange---', pageSize);
        },
        onShowSizeChange: (page: any, size: any) => {
          console.log('---onShowSizeChange---', page);
          console.log('---onShowSizeChange---', size);
        },
        itemRender: (current: any, type: any, originalElement: any): any => {
          function createAEle (content: string) {
            let aEle = document.createElement("a");
            aEle.innerHTML = content;
            return aEle;
          }

          if (type === 'prev') {
            return createAEle('上一个');
          }

          if (type === 'next') {
            return createAEle('下一个');
          }

          if (type === 'page') {
            return createAEle(`第${current}只`);
          }

          return originalElement;
        }
      });
    }
  }

  render() {
    return (
      <div
        id="Pagination"
        className="Pagination"
        ref={
          (Node) => {
            this.Pagination = Node;
          }
        }
      >
        11111
      </div>
    );
  }
}

export default App;

重新运行一下 npm start ,我们可以看到 my-pagination 已经开始工作了:

 

小结

到这里我们就完成了从零开始使用 typescript 开发一个类 Antd 的 分页器元素,通过这次开发,让我对 插件的开发流程webpack 的配置typescript 的基础使用插件的打包发布测试 等大体有了一个比较清楚的认知。

希望能对大家有所帮助。

 

参考内容


Darrell
38 声望5 粉丝