最近在业务中使用了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
开发,安装typescript
和tslint
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
- 安装相应的
webpack
的plugins
,下面的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-pagination
的 pageBufferSize
只有两个值 1 和 2,不过两个已经完全够了。
上面的例子,pageBufferSize = 2
,及 showLessItems
为 false
。
找规律
接着观察上面这一组数据,我们可以找一下规律,粗粗一看,我们会发现:
- 第 1 页 到 第 3 页 显示的页码是一样的,只有 一个后面省略号
- 第 4 页 比 前 3 页 多了一个页码,但是还是只有 一个后面省略号(临界点)
- 第 5 页 开始,到 第 26 页 出现了 两个省略号,并且中间都是 5 个
- 第 27 页 跟第四页类似 又回到了 一个前面省略号(临界点)
- 第 28 页 到 第 30 页,显示的页面一样,并只有 一个前面省略号。
代码实现
我们在这里先暂时不考虑复杂的 dom
输出,只使用简单的字符串输出。
这里笔者想到两种思路:
第一种思路
简单粗暴:就是把用代码翻译上面我们发现的规律,我们可以用 allPages
、current
、pageBufferSize
来表示:
// 临界点
当 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
内使用的方法,我们接下来的方法也只满足 pageBufferSize
为 1
或者 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
的值根据前几页的 current
和 pageBufferSize
,我们很简单的可以得到。
let left = Math.max(1, current - pageBufferSize);
同理可以得到 right
的值:
let right = Math.min(current + pageBufferSize, allPages);
因为 前三页 和 后三页 都是显示 5 个数的,所以在这种情况下,我们要对 left
和 right
在根据 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 和 总页数。
省略号我们加完之后,我们就只剩最后一步了。我们需要判断 第一步的 left
和 right
是不是第 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();
}
打开浏览器,分别测试一下 pageBufferSize
为 1
和 2
时,如下图:
总算是搞定了,终于可以快乐的进行下一步的开发了。
大家如果还有什么不懂,还可以看一下这篇文章 十五分钟--分页逻辑--包学包会,写的很详细。
基本架构
插件的基本架构
首先我们在 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
函数,这个主要用来渲染页码;同时完善addFragmentBefore
与addFragmentAfter
函数,向代码片段前后插入代码。
/**
* 循环渲染相应的 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
节点的函数。 在之前的所有函数我们其实都没有真正意义上生成li
的Dom
节点。
/**
* 创建 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
:页数改变 -
onShowSizeChange
:pageSize
变化的回调
我们先来实现 onChange
,onShowSizeChange
我们在后面会讲到。
其实很简单,只要在 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
类,做法和分页器一样,有时间我会在写一个仿Antd
的select
框,使用就是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
:这个方法是返回icon
的dom
元素
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
的基础使用、插件的打包发布测试 等大体有了一个比较清楚的认知。
希望能对大家有所帮助。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。