jQuery 插件開發(Plugin)
jQuery 插件(Plugin)是擴充 jQuery 功能的方式,讓你能夠將常用的功能封裝起來,方便重複使用。本篇將介紹如何開發自己的 jQuery 插件,以及最佳實踐。
插件基本概念
jQuery 插件有兩種類型:
- 實例方法插件:擴充
$.fn,可在 jQuery 物件上呼叫 - 工具函式插件:擴充
$,直接在 jQuery 上呼叫
// 實例方法插件
$('div').myPlugin();
// 工具函式插件
$.myUtility();
基本插件架構
最簡單的插件
// 定義插件
$.fn.highlight = function () {
this.css('background-color', 'yellow');
return this; // 返回 this 以支援鏈式呼叫
};
// 使用
$('p').highlight();
使用 IIFE 包裹
為了避免污染全域命名空間,建議使用立即執行函式表達式(IIFE):
(function ($) {
$.fn.highlight = function () {
this.css('background-color', 'yellow');
return this;
};
})(jQuery);
這種寫法的好處:
- 確保
$指向 jQuery(即使使用了noConflict) - 建立私有範圍,避免變數洩漏
支援選項
大多數插件都需要支援可自訂的選項:
(function ($) {
$.fn.highlight = function (options) {
// 合併預設值與使用者選項
var settings = $.extend(
{
color: 'yellow',
duration: 1000,
},
options
);
return this.each(function () {
$(this).css('background-color', settings.color);
});
};
})(jQuery);
// 使用
$('p').highlight(); // 使用預設值
$('p').highlight({ color: 'red' }); // 自訂顏色
$('p').highlight({ color: 'blue', duration: 500 });
暴露預設值
讓使用者可以全域修改預設值:
(function ($) {
$.fn.highlight = function (options) {
var settings = $.extend({}, $.fn.highlight.defaults, options);
return this.each(function () {
$(this).css('background-color', settings.color);
});
};
// 暴露預設值
$.fn.highlight.defaults = {
color: 'yellow',
duration: 1000,
};
})(jQuery);
// 全域修改預設值
$.fn.highlight.defaults.color = 'lightblue';
// 現在所有呼叫都會使用新的預設值
$('p').highlight();
完整插件模板
基本模板
(function ($) {
'use strict';
var pluginName = 'myPlugin';
// 預設選項
var defaults = {
option1: 'value1',
option2: 100,
onComplete: null,
};
// 插件建構函式
function Plugin(element, options) {
this.element = element;
this.$element = $(element);
this.settings = $.extend({}, defaults, options);
this.init();
}
// 插件方法
Plugin.prototype = {
init: function () {
// 初始化邏輯
this.bindEvents();
},
bindEvents: function () {
var self = this;
this.$element.on('click.' + pluginName, function () {
self.doSomething();
});
},
doSomething: function () {
// 執行某些操作
this.$element.addClass('active');
// 觸發回呼
if (typeof this.settings.onComplete === 'function') {
this.settings.onComplete.call(this.element);
}
},
destroy: function () {
// 清理:移除事件和資料
this.$element.off('.' + pluginName);
this.$element.removeData('plugin_' + pluginName);
},
};
// 避免多重實例化
$.fn[pluginName] = function (options) {
return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName, new Plugin(this, options));
}
});
};
// 暴露預設值和建構函式
$.fn[pluginName].defaults = defaults;
$.fn[pluginName].Plugin = Plugin;
})(jQuery);
支援方法呼叫的模板
允許使用者呼叫插件的方法:
(function ($) {
'use strict';
var pluginName = 'accordion';
var defaults = {
speed: 300,
oneOpen: true,
};
function Accordion(element, options) {
this.element = element;
this.$element = $(element);
this.settings = $.extend({}, defaults, options);
this.init();
}
Accordion.prototype = {
init: function () {
this.$headers = this.$element.find('.accordion-header');
this.$panels = this.$element.find('.accordion-panel');
this.bindEvents();
},
bindEvents: function () {
var self = this;
this.$headers.on('click.' + pluginName, function () {
self.toggle($(this).index());
});
},
toggle: function (index) {
var $panel = this.$panels.eq(index);
if (this.settings.oneOpen) {
this.$panels.not($panel).slideUp(this.settings.speed);
}
$panel.slideToggle(this.settings.speed);
},
open: function (index) {
this.$panels.eq(index).slideDown(this.settings.speed);
},
close: function (index) {
this.$panels.eq(index).slideUp(this.settings.speed);
},
openAll: function () {
this.$panels.slideDown(this.settings.speed);
},
closeAll: function () {
this.$panels.slideUp(this.settings.speed);
},
destroy: function () {
this.$headers.off('.' + pluginName);
this.$element.removeData('plugin_' + pluginName);
},
};
$.fn[pluginName] = function (options) {
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function () {
var instance = $.data(this, 'plugin_' + pluginName);
if (!instance) {
// 初始化
$.data(this, 'plugin_' + pluginName, new Accordion(this, options));
} else if (typeof options === 'string' && typeof instance[options] === 'function') {
// 呼叫方法
instance[options].apply(instance, args);
}
});
};
$.fn[pluginName].defaults = defaults;
})(jQuery);
// 使用
$('.accordion').accordion({ speed: 500 });
// 呼叫方法
$('.accordion').accordion('open', 0);
$('.accordion').accordion('closeAll');
$('.accordion').accordion('destroy');
最佳實踐
1. 保持鏈式呼叫
永遠返回 this,讓使用者可以繼續鏈式操作:
$.fn.myPlugin = function () {
// ... 做一些事情
return this; // 重要!
};
// 使用者可以這樣
$('div').myPlugin().addClass('active').fadeIn();
2. 使用 .each() 處理多元素
插件可能被應用在多個元素上,使用 .each() 確保每個元素都被處理:
$.fn.myPlugin = function () {
return this.each(function () {
var $this = $(this);
// 對每個元素進行操作
});
};
3. 使用命名空間
為事件使用命名空間,方便之後移除:
// 綁定事件
this.$element.on('click.myPlugin', handler);
// 移除事件
this.$element.off('.myPlugin');
4. 避免重複實例化
使用 $.data() 檢查是否已經初始化:
$.fn.myPlugin = function () {
return this.each(function () {
if (!$.data(this, 'myPlugin')) {
$.data(this, 'myPlugin', new Plugin(this));
}
});
};
5. 提供銷毀方法
讓使用者能夠清理插件:
destroy: function() {
// 移除事件監聽
this.$element.off('.' + pluginName);
// 移除新增的元素或類別
this.$element.find('.plugin-added').remove();
this.$element.removeClass('plugin-active');
// 移除資料
this.$element.removeData('plugin_' + pluginName);
}
6. 使用私有方法
在插件內部使用私有函式,不暴露給外部:
(function ($) {
// 私有函式
function privateHelper(value) {
return value * 2;
}
$.fn.myPlugin = function () {
var result = privateHelper(10); // 內部使用
// ...
};
})(jQuery);
實作範例
簡單 Tooltip 插件
(function ($) {
'use strict';
var defaults = {
position: 'top',
delay: 200,
class: 'tooltip',
};
$.fn.tooltip = function (options) {
var settings = $.extend({}, defaults, options);
return this.each(function () {
var $this = $(this);
var $tooltip = null;
var showTimeout;
$this.on('mouseenter.tooltip', function () {
var title = $this.attr('title') || $this.data('tooltip');
if (!title) return;
// 移除原生 title 提示
$this.data('tooltip', title).removeAttr('title');
showTimeout = setTimeout(function () {
$tooltip = $('<div>').addClass(settings.class).text(title).appendTo('body');
positionTooltip($this, $tooltip, settings.position);
$tooltip.fadeIn(150);
}, settings.delay);
});
$this.on('mouseleave.tooltip', function () {
clearTimeout(showTimeout);
if ($tooltip) {
$tooltip.fadeOut(150, function () {
$(this).remove();
});
$tooltip = null;
}
});
});
};
function positionTooltip($element, $tooltip, position) {
var offset = $element.offset();
var width = $element.outerWidth();
var height = $element.outerHeight();
var tipWidth = $tooltip.outerWidth();
var tipHeight = $tooltip.outerHeight();
var positions = {
top: {
top: offset.top - tipHeight - 10,
left: offset.left + (width - tipWidth) / 2,
},
bottom: {
top: offset.top + height + 10,
left: offset.left + (width - tipWidth) / 2,
},
left: {
top: offset.top + (height - tipHeight) / 2,
left: offset.left - tipWidth - 10,
},
right: {
top: offset.top + (height - tipHeight) / 2,
left: offset.left + width + 10,
},
};
$tooltip.css(positions[position]);
}
$.fn.tooltip.defaults = defaults;
})(jQuery);
// 使用
$('[data-tooltip]').tooltip({ position: 'bottom' });
計數器插件
(function ($) {
'use strict';
var defaults = {
start: 0,
min: 0,
max: Infinity,
step: 1,
onChange: null,
};
function Counter(element, options) {
this.$element = $(element);
this.settings = $.extend({}, defaults, options);
this.value = this.settings.start;
this.init();
}
Counter.prototype = {
init: function () {
this.render();
this.bindEvents();
this.updateDisplay();
},
render: function () {
this.$element.addClass('counter');
this.$minus = $('<button>', { class: 'counter-minus', text: '-' });
this.$plus = $('<button>', { class: 'counter-plus', text: '+' });
this.$display = $('<span>', { class: 'counter-display' });
this.$element.append(this.$minus, this.$display, this.$plus);
},
bindEvents: function () {
var self = this;
this.$minus.on('click.counter', function () {
self.decrement();
});
this.$plus.on('click.counter', function () {
self.increment();
});
},
increment: function () {
if (this.value < this.settings.max) {
this.value += this.settings.step;
this.updateDisplay();
this.triggerChange();
}
},
decrement: function () {
if (this.value > this.settings.min) {
this.value -= this.settings.step;
this.updateDisplay();
this.triggerChange();
}
},
setValue: function (value) {
value = Math.max(this.settings.min, Math.min(this.settings.max, value));
this.value = value;
this.updateDisplay();
this.triggerChange();
},
getValue: function () {
return this.value;
},
updateDisplay: function () {
this.$display.text(this.value);
this.$minus.prop('disabled', this.value <= this.settings.min);
this.$plus.prop('disabled', this.value >= this.settings.max);
},
triggerChange: function () {
if (typeof this.settings.onChange === 'function') {
this.settings.onChange.call(this.$element[0], this.value);
}
this.$element.trigger('counter:change', [this.value]);
},
destroy: function () {
this.$element.off('.counter').removeClass('counter').empty().removeData('counter');
},
};
$.fn.counter = function (options) {
var args = Array.prototype.slice.call(arguments, 1);
var returnValue = this;
this.each(function () {
var instance = $.data(this, 'counter');
if (!instance) {
$.data(this, 'counter', new Counter(this, options));
} else if (typeof options === 'string') {
var result = instance[options].apply(instance, args);
if (options === 'getValue') {
returnValue = result;
return false; // 中斷迭代
}
}
});
return returnValue;
};
$.fn.counter.defaults = defaults;
})(jQuery);
// 使用
$('#counter').counter({
start: 5,
min: 0,
max: 10,
onChange: function (value) {
console.log('新值:', value);
},
});
// 呼叫方法
$('#counter').counter('setValue', 7);
var value = $('#counter').counter('getValue');
推薦的第三方插件
UI 相關
- Select2 - 增強的下拉選單
- DataTables - 表格增強
- Slick - 輪播/滑動元件
- Magnific Popup - 燈箱效果
- jQuery UI - 官方 UI 元件庫
表單相關
- jQuery Validation - 表單驗證
- jQuery Mask - 輸入遮罩
- Dropzone.js - 拖放上傳
其他
- Waypoints - 捲動觸發
- Lazy Load - 延遲載入圖片
- Chart.js - 圖表(搭配 jQuery 使用)