编写目的
博主课余时间在老师那边做web前端开发,也看了些开发视频,接触到了使用jQuery进行面向组件的开发。在做网页时发现:表格datatable在展示数据时使用频繁,故尝试着在此基础上进行封装,形成自己的插件,事实证明结果也是蛮好的,可以达到很大程度的复用,减少代码量和便于修改。
需要注意的是自己编写的js 是在 jQuery.datatables 和 bootstrap.datatables上进行扩展封装的。
一、首先可以想到的是定义一个Table类,声明它的构造函数:
;(function($) {
'use strict';
function Table() {
}
})(jQuery);
其次定义了一个对象之后我们需要让它能在其他文件中使用,可是又不想污染全局环境,就使用jQuery进行扩展,注意这是组件化开发,故需要和Html的特性的dom元素结合一起使用,即使用是面向组件的:
比如页面中有如下dom结构,其实就是一个很普通的表格:
<table class="table table-bordered table-hover" id="bookTable" style="width: 100%;">
<thead>
<tr>
<th>
...
</th>
</tr>
</thead>
<tbody>
<!--ajax-->
</tbody>
</table>
那么实际使用就是:
$('#bookTable').myTable(options);
其中options是一个自定义的参数对象,以键值对的形式初始化组件。myTable就是根据jQuery扩展出来的方法。
二、清楚了这个js是要让jQuery对象进行组件化之后,就要将方法暴露给jQuery对象。可以知道jQuery提供了方法来扩展方法库
$.fn.extend();
现在就是怎么使用这个方法,能达到最大程度的复用。进一步改造,得到如下形式:
;(function($) {
'use strict';
function Table($elem, options) {
this.$elem = $elem;
this.options = options;
}
Table.DEFAULTS = {
sequence: 0,
destroy: false,
ordering: false,
serverSide: true,
searching: false,
language: {
"sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项",
"sInfoEmpty": "显示第 0 至 0 项结果,共 0 项",
"oPaginate": {
"sFirst": "首页",
"sPrevious": "上页",
"sNext": "下页",
"sLast": "末页"
},
"sEmptyTable": "无数据",
"sLengthMenu": "显示 _MENU_ 项结果"
}
};
// 单例模式 生成 表格
$.fn.extend({
myTable: function (option) {
return this.each(function () {
let $this = $(this),
// 合并对象
options = $.extend({}, Table.DEFAULTS, typeof option === "object" && option),
mode = $this.data("inited");
if (!mode) {
$this.data("inited", mode = new Table($this, options));
}
});
}
});
})(jQuery);
myTable就是我们扩展出来的内容,myTable是一个函数,里面的 return 需要特别注意。需要单独讲一下:
return this.each(function () {
let $this = $(this),
// 合并对象
options = $.extend({}, Table.DEFAULTS, typeof option === "object" && option),
mode = $this.data("inited");
if (!mode) {
$this.data("inited", mode = new Table($this, options));
}
});
myTable中的this就是调用这个方法进行组件初始化的jQuery对象,而使用jQuery选择器选出来的对象不一定只有一个,jQuery帮我们封装好了,无论是一个还是多个jQuery对象使用each方法都是没问题的。在this.each 方法中就是每个单独的 dom对象了。这样就可以实现调用一次方法可以让被选择到的jQuery对象全部被初始化。
个人认为这个return里面分为三步:
1.先将初始化的dom对象转换为jQuery对象
2.将传入的自定义参数对象和Table.DEFAULTS默认参数对象合并,合并时使用的是:$.extend();
options = $.extend({}, Table.DEFAULTS, typeof option === "object" && option)
如果option参数类型是对象类型则进行合并,否则默认采用Tables.DEFAULTS默认参数对象
3.采用 单例模式 的思想,获取该jQuery对象上挂载data属性 ‘inited’,如果没有,则表示“我还没有被初始化。”,并且将其初始化
mode = $this.data("inited");
if (!mode) {
$this.data("inited", mode = new Table($this, options));
}
初始化时将,new Table()对象传入data属性 'inited'中,此时inited属性有了对象值了,若存在重复初始化,mode在下一次初始化时就有值不为空,就不再为其初始化,达到单例模式的作用,避免重复初始化。
这里终于用到了我们自己写的构造函数入口,第一个参数将$this 即初始化的jQuery对象传入,第二个参数是最终得到的参数对象。
mode = new Table($this, options)
三、向外提供了入口和向内提供一个构造函数之后,可以开始搭建这个自己构建的类的骨架了。
我也不是一开始就知道这个自定义的Table类是怎么构建的,而是在做项目的过程中一步一步发现哪些是该类本身应该有的行为,哪些可以封装进来,哪些代码在编写的过程中重复了。
就是在这个过程中,我个人觉得,一个表格有这几个抽象的行为:
1.表格的初始化
2.删除表格的行
3.添加表格的行
1.表格的初始化:
将简单的赋值放在Table 构造函数里面,将一些逻辑代码放在_init() 函数里面并且挂载到Table的原型上,实现行为共享,但是数据相互独立。
经过初始化的改造,现在代码长这样了:
;(function($) {
'use strict';
function Table($elem, options) {
this.$elem = $elem;
this.options = options;
this.$table = null;
this._init();
}
Table.prototype._init = function () {
const self = this;
this.$table = this.$elem.DataTable(this.options);
if (this.options.serverSide) {
this.$elem.on('draw.dt', function () {
self.$table.column(self.options.sequence, {
search: 'applied',
order: 'applied'
}).nodes().each(function (cell, i) {
//i 从0开始,所以这里先加1
i = i + 1;
//服务器模式下获取分页信息,使用 DT 提供的 API 直接获取分页信息
const page = self.$table.page.info();
//当前第几页,从0开始
const pageno = page.page;
//每页数据
const length = page.length;
//行号等于 页数*每页数据长度+行号
cell.innerHTML = (i + pageno * length);
});
});
} else {
this.$table.on('draw.dt', function() {
self.$table.column(self.options.sequence, {
search: 'applied',
order: 'applied'
}).nodes().each(function(cell, i) {
cell.innerHTML = i + 1;
});
});
}
};
Table.DEFAULTS = {
sequence: 0,
destroy: false,
ordering: false,
serverSide: true,
searching: false,
language: {
"sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项",
"sInfoEmpty": "显示第 0 至 0 项结果,共 0 项",
"oPaginate": {
"sFirst": "首页",
"sPrevious": "上页",
"sNext": "下页",
"sLast": "末页"
},
"sEmptyTable": "无数据",
"sLengthMenu": "显示 _MENU_ 项结果"
}
};
// 单例模式 生成 表格
$.fn.extend({
myTable: function (option) {
return this.each(function () {
let $this = $(this),
// 合并对象
options = $.extend({}, Table.DEFAULTS, typeof option === "object" && option),
mode = $this.data("inited");
if (!mode) {
$this.data("inited", mode = new Table($this, options));
}
});
}
});
})(jQuery);
_init() 函数里面主要分两步:1.进行dataTable的初始化;2.判断dataTable是服务器端渲染还是浏览器端渲染(毕竟两者的排序号列的形成和特定的行为调用是有区别的)
完成这一步其实已经可以调用自己写的接口来调用初始化组件了:
$("#myTable").myTable({
// 形成的序号列是第几列,从0开始
sequence: 1,
ajax: {
url: CONFIG.AJAX_URL+"/book/bookPage",
type: "POST",
data: {"send": 0, "payStatus": 0}
},
rowId: "book.bookId",
columns: [
{
data: "book.bookId",
render: function (data, __, row, ___) {
bookId = data;
return row;
}
},
....
]
});
把变的部分以参数键值对的方式传入,把不变的部分封装到类中。可以看到变的部分就是:
序号列是第几列;
在服务器渲染的前提下:
ajax请求的url等;
给每行<tr>设置的id属性;
每行的内容,列columns等等。
而在非服务器渲染的前提下,初始化就更简单了,极大地减少了初始化的代码量:
memberInfoTable.myTable({
serverSide: false,
// 其他参数
pageLength: 5,
lengthChange: false
});
2.删除表格的行:
删除表格的行这个行为不需要关心表格的结构,表格的内容,不管它有多少列,删除指定行就可以了。如此就可以为自定义类添加 删除行 的行为。
// 表格 删除某一行
Table.prototype.deleteRow = function (options) {
const self = this;
this.$elem.on("click", options.row, function () {
const $this = $(this);
mbBox.confirm({
message: "确定要删除吗?",
callback() {
const id = $($this.parents().parents()[0]).attr("id");
self._deleteRow(id, options);
}
});
});
};
Table.prototype._deleteRow = function (id, options) {
const self = this;
for (let j in options.data) {
if (options.data.hasOwnProperty(j)) {
options.data[j] = id;
}
}
$.ajax({
url: options.url,
type: "get",
dataType: "json",
data: options.data,
success: function (response) {
if (response.code === 1) {
self.$table.row("#" + id).remove().draw(false);
}
mbBox.alert({
message: response.msg
});
},
error: function (error) {
mbBox.alert({
title: "错误",
message: error
});
}
});
};
可是现在如何才能调用这个能删除行的行为呢?我们只能在暴露接口的地方进行修改了,在观看学习视频之后,发现可以将那个return改写成这样:
// 单例模式 生成 表格
$.fn.extend({
myTable: function (option, value) {
return this.each(function () {
let $this = $(this),
// 合并对象
options = $.extend({}, Table.DEFAULTS, typeof option === "object" && option),
mode = $this.data("inited");
if (!mode) {
$this.data("inited", mode = new Table($this, options));
}
if (option && typeof mode[option] === "function") {
mode[option](value);
}
});
}
});
多了最后的一个if 判断,和接口入口传入的参数:
if (option && typeof mode[option] === "function") {
mode[option](value);
}
mode 是 new Table() 对象,调用一个对象的方法除了 obj.method() 以外,如果传入的是字符串 “方法名” 的话,可以使用 obj[method]()的方式调用该方法。经此进一步改造,就可以实现删除行为的挂载了,在初始化后如果还想给表格添加删除行为:
// 封装后的删除操作
bookTable.myTable("deleteRow",{
row: ".deleteBook",
url: CONFIG.AJAX_URL+"/book/delBook",
data:{
bookId: ""
}
});
此时传入的 option 不是一个参数对象了,而是一个方法名字符串,然后第二个参数 value就是这个方法的参数对象,value在deleteRow() 这个方法中就变成了相应的options。
;
(function ($) {
'use strict';
function Table($elem, options) {
this.$elem = $elem;
this.options = options;
this.$table = null;
this._init();
}
Table.DEFAULTS = {
sequence: 0,
destroy: false,
ordering: false,
serverSide: true,
searching: false,
language: {
"sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项",
"sInfoEmpty": "显示第 0 至 0 项结果,共 0 项",
"oPaginate": {
"sFirst": "首页",
"sPrevious": "上页",
"sNext": "下页",
"sLast": "末页"
},
"sEmptyTable": "无数据",
"sLengthMenu": "显示 _MENU_ 项结果"
}
};
Table.prototype._init = function () {
const self = this;
this.$table = this.$elem.DataTable(this.options);
if (this.options.serverSide) {
this.$elem.on('draw.dt', function () {
self.$table.column(self.options.sequence, {
search: 'applied',
order: 'applied'
}).nodes().each(function (cell, i) {
//i 从0开始,所以这里先加1
i = i + 1;
//服务器模式下获取分页信息,使用 DT 提供的 API 直接获取分页信息
const page = self.$table.page.info();
//当前第几页,从0开始
const pageno = page.page;
//每页数据
const length = page.length;
//行号等于 页数*每页数据长度+行号
cell.innerHTML = (i + pageno * length);
});
});
} else {
this.$table.on('draw.dt', function () {
self.$table.column(self.options.sequence, {
search: 'applied',
order: 'applied'
}).nodes().each(function (cell, i) {
cell.innerHTML = i + 1;
});
});
}
};
// 刷新表格
Table.prototype.reloadTable = function (options) {
this.$table.settings()[0].ajax.data = options;
this.$table.ajax.reload();
};
// 表格 添加一行
Table.prototype.addRow = function (options) {
options.data.forEach((ele) => {
// 将 ele 传出,过滤成想要的数据形式 {id: , body: }
let result = options.filterData(ele);
const $rowCode = $(this.$table.row.add(result.body).node());
$rowCode.attr("id", result.id);
$($rowCode.find("td").get(options.idColumn)).attr("data-id", result.id);
});
this.$table.draw();
};
// 表格 删除某一行
Table.prototype.deleteRow = function (options) {
const self = this;
this.$elem.on("click", options.row, function () {
const $this = $(this);
mbBox.confirm({
message: "确定要删除吗?",
callback() {
const id = $($this.parents().parents()[0]).attr("id");
self._deleteRow(id, options);
}
});
});
};
Table.prototype._deleteRow = function (id, options) {
const self = this;
for (let j in options.data) {
if (options.data.hasOwnProperty(j)) {
options.data[j] = id;
}
}
$.ajax({
url: options.url,
type: "get",
dataType: "json",
data: options.data,
success: function (response) {
if (response.code === 1) {
self.$table.row("#" + id).remove().draw(false);
mbBox.alert({
message: "删除成功!"
});
} else {
mbBox.alert({
message: response.msg
});
}
},
error: function (error) {
mbBox.alert({
title: "错误",
message: error
});
}
});
};
// 单例模式 生成 表格
$.fn.extend({
myTable: function (option, value) {
return this.each(function () {
let $this = $(this),
// 合并对象
options = $.extend({}, Table.DEFAULTS, typeof option === "object" && option),
mode = $this.data("inited");
if (!mode) {
$this.data("inited", mode = new Table($this, options));
}
if (option && typeof mode[option] === "function") {
mode[option](value);
}
});
}
});
})(jQuery);