jQuery 插件编写全攻略

jQuery 插件的本质就是扩展 jQuery 的原型对象 (jQuery.fn) 或 jQuery 对象本身,从而封装可复用的功能,让代码更简洁、更易于维护。

jquery 插件写法教程
(图片来源网络,侵删)

本教程将分为以下几个部分:

  1. 入门篇:创建你的第一个简单插件
  2. 进阶篇:处理选项、方法和链式调用
  3. 高级篇:作用域、事件和默认值
  4. 最佳实践与完整示例

入门篇:创建你的第一个简单插件

目标是创建一个名为 greenify 的插件,它能将任何匹配的元素的文字颜色变为绿色。

第一步:创建插件文件

创建一个新的 JavaScript 文件,jquery.greenify.js

第二步:编写插件代码

jquery.greenify.js 中,编写以下代码:

jquery 插件写法教程
(图片来源网络,侵删)
// 1. 创建一个闭包,避免污染全局命名空间
(function ($) {
    // 2. 将插件定义到 jQuery 的原型对象上
    // 这样所有 jQuery 对象(如 $('div'))都能调用这个方法
    $.fn.greenify = function () {
        // 3. 'this' 关键字指向当前调用插件的 jQuery 对象
        // 如果调用 $('p').greenify(),this $('p')
        // 4. 使用 .css() 方法修改样式
        this.css('color', 'green');
        // 5. 返回 'this' 以支持链式调用
        return this;
    };
// 6. 传入 jQuery 对象 '$' 作为参数,确保在插件内部能正确使用 $
})(jQuery);

代码解析:

  1. (function ($) { ... })(jQuery);: 这是一个立即执行函数表达式,它的主要作用是创建一个独立的作用域,避免插件内部的变量(如 myVar)与外部脚本的全局变量发生冲突,我们传入 jQuery 作为参数,并在函数内部用 来接收它,这样做的好处是,即使其他库(如 Prototype.js)也使用了 符号,我们的插件依然能正常工作。
  2. $.fn.greenify = function () { ... };: 这是核心。$.fn 是 jQuery 原型的引用,当我们给 $.fn 添加一个新属性时,所有通过 jQuery() 或 创建的对象都会拥有这个方法。
  3. this: 在插件函数内部,this 指向调用该方法的 jQuery 对象,它是一个集合,包含了所有匹配的 DOM 元素。$('div').greenify() 中的 this 就是一个包含页面上所有 div 的 jQuery 对象。
  4. this.css('color', 'green');: 我们直接在 this 上调用 jQuery 的其他方法(如 .css(), .html(), .click() 等)来操作这些元素。
  5. return this;: 这是非常重要的一个步骤,返回 this(即调用方法的 jQuery 对象)使得我们的插件可以支持链式调用$('p').greenify().slideUp(); 会先让段落变绿,然后再滑上去。

如何使用?

  1. 引入 jQuery 库。
  2. 引入你刚写的 jquery.greenify.js 文件。
  3. 在你的 JavaScript 代码中调用它。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">jQuery Plugin Demo</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="jquery.greenify.js"></script>
    <style>
        p { margin: 10px; }
    </style>
</head>
<body>
    <p>第一段文字</p>
    <p>第二段文字</p>
    <button id="greenifyBtn">点击让段落变绿</button>
    <script>
        $(document).ready(function() {
            // 点击按钮后,让所有段落变绿
            $('#greenifyBtn').on('click', function() {
                $('p').greenify();
            });
        });
    </script>
</body>
</html>

进阶篇:处理选项、方法和链式调用

简单的插件功能有限,一个强大的插件应该允许用户通过选项来自定义行为,并且可能提供公共方法来控制插件。

目标:创建一个 highlight 插件

  • 功能:高亮显示文本。
  • 选项:可以自定义高亮颜色和背景色。
  • 方法:提供一个 publicMethod 来获取当前的高亮颜色。

代码实现 (jquery.highlight.js)

(function ($) {
    // 插件主函数
    $.fn.highlight = function (options) {
        // 1. 合并用户传入的选项和默认选项
        // $.extend 是一个非常有用的工具函数
        var settings = $.extend({
            color: 'red',
            backgroundColor: 'yellow'
        }, options);
        // 2. 'this' 指向调用插件的 jQuery 对象
        this.css({
            'color': settings.color,
            'background-color': settings.backgroundColor
        });
        // 3. 返回 'this' 以支持链式调用
        return this;
    };
})(jQuery);

如何使用?

// 使用默认选项
$('p').highlight();
// 自定义选项
$('p').highlight({
    color: 'blue',
    backgroundColor: 'lightgrey'
});

添加公共方法

我们希望插件不仅能被调用,还能提供一些方法供外部调用,比如获取或设置内部状态。

(function ($) {
    // 保存对插件的引用,以便在闭包中访问
    var pluginName = 'highlight';
    // 创建一个构造函数,用于管理每个实例的状态
    function Plugin(element, options) {
        this.element = element;
        this.$element = $(element);
        this.settings = $.extend(true, {}, $.fn[pluginName].defaults, options);
        this._name = pluginName;
        this.init();
    }
    // 在构造函数的原型上添加方法
    Plugin.prototype = {
        init: function () {
            // 初始化代码
            this.applyStyles();
        },
        applyStyles: function () {
            this.$element.css({
                'color': this.settings.color,
                'background-color': this.settings.backgroundColor
            });
        },
        // 公共方法:获取当前颜色
        getColor: function () {
            return this.settings.color;
        },
        // 公共方法:更新颜色并重新应用样式
        updateColor: function (newColor) {
            this.settings.color = newColor;
            this.applyStyles();
        }
    };
    // 将构造函数赋值给 jQuery.fn
    $.fn[pluginName] = function (options) {
        // 'this' 是调用插件的 jQuery 对象集合
        // 我们需要为集合中的每个元素都创建一个插件实例
        // 使用 .each() 遍历
        this.each(function () {
            // 避免重复初始化
            if (!$.data(this, 'plugin_' + pluginName)) {
                $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
            }
        });
        // 返回 'this' 以支持链式调用
        return this;
    };
    // 设置默认值
    $.fn[pluginName].defaults = {
        color: 'red',
        backgroundColor: 'yellow'
    };
})(jQuery);

如何使用带有方法的插件?

// 初始化插件
$('p').highlight({
    color: 'green'
});
// 调用公共方法
var currentColor = $('p').highlight('getColor');
console.log(currentColor); // 输出 "green"
// 调用另一个公共方法
$('p').highlight('updateColor', 'purple');

高级篇:作用域、事件和默认值

作用域和 this

在插件内部,this 始终指向单个 DOM 元素,而不是 jQuery 对象,这是因为 jQuery 的内部 .each() 方法在迭代时,会将 this 设置为当前循环的 DOM 元素。

$.fn.myPlugin = function() {
    // 这里的 'this' 是 DOM 元素,<p>
    // 如果要使用 jQuery 方法,需要将其包装起来
    $(this).css('border', '1px solid black');
    // 如果你想在循环中操作所有元素,通常不需要手动循环
    // jQuery 的方法(如 .css())本身就会对集合中的所有元素生效
    // 下面的代码是错误的,会导致重复操作
    // this.each(function() { $(this).css(...); }); // 不推荐
};

事件处理

在插件中绑定事件时,一定要在 destroy 方法(或插件卸载时)中解绑事件,以防止内存泄漏。

jquery 插件写法教程
(图片来源网络,侵删)
(function ($) {
    $.fn.onHover = function () {
        return this.each(function () {
            var $this = $(this); // 缓存 jQuery 对象
            // 绑定mouseenter事件
            $this.on('pluginOnHover', function () {
                $(this).addClass('hovered');
            });
            // 绑定mouseleave事件
            $this.on('pluginOnHoverLeave', function () {
                $(this).removeClass('hovered');
            });
        });
    };
    // 假设我们有一个销毁方法
    $.fn.destroyOnHover = function () {
        return this.each(function () {
            var $this = $(this);
            // 解绑所有由插件绑定的事件
            $this.off('.pluginOnHover'); // 移除所有命名空间为 'pluginOnHover' 的事件
        });
    };
})(jQuery);

默认值

最佳实践是将默认值定义在插件外部,这样用户可以在不修改插件源码的情况下,通过修改 $.fn.yourPlugin.defaults 来改变全局默认值。

// 在插件外部定义默认值
$.fn.tooltip.defaults = {
    offsetX: 10,
    offsetY: 15,
    content: 'Default tooltip content'
};
(function ($) {
    $.fn.tooltip = function (options) {
        // 合并选项,将用户传入的选项覆盖默认值
        var settings = $.extend({}, $.fn.tooltip.defaults, options);
        // ... 插件逻辑 ...
        console.log('Tooltip offset is:', settings.offsetX, settings.offsetY);
        console.log('Content is:', settings.content);
        return this;
    };
})(jQuery);
// 使用
$('.my-element').tooltip(); // 使用全局默认值
$('.another-element').tooltip({ offsetX: 20 }); // 覆盖 offsetX

最佳实践与完整示例

一个完整的、生产级别的插件应该遵循以下最佳实践:

  1. 使用 IIFE:避免全局污染。
  2. 明确的命名空间:使用插件名作为函数和事件的命名空间(如 myplugin-doSomething),避免与其他库或插件冲突。
  3. 私有函数:将不希望暴露给用户的函数定义为插件作用域内的私有函数。
  4. 实例化:为每个元素创建一个独立的实例,管理各自的状态(使用 $.data)。
  5. 提供销毁方法:提供一个 destroydispose 方法,用于清理事件监听器和数据,防止内存泄漏。
  6. 支持链式调用:始终返回 this
  7. 文档化:为你的插件编写清晰的文档,说明用法、选项和事件。

完整示例:一个简单的 Toc (Table of Contents) 插件

这个插件会为页面中的标题(h1, h2, h3...)生成一个目录,并支持点击目录项平滑滚动到对应位置。

文件: jquery.toc.js

(function ($) {
    // 插件默认配置
    var defaults = {
        // 选择器,用于定位要生成目录的容器
        container: 'body',
        // 选择器,用于选择要收录的标题
        headings: 'h1, h2, h3, h4, h5, h6',
        // 目录容器的选择器或 jQuery 对象
        tocContainer: '#toc',
        // 是否添加平滑滚动效果
        smoothScroll: true,
        // 滚动速度(毫秒)
        scrollSpeed: 800
    };
    // 构造函数
    function Toc(element, options) {
        this.element = element;
        this.$element = $(element);
        this.settings = $.extend({}, defaults, options);
        this.init();
    }
    // 插件方法
    Toc.prototype = {
        // 初始化
        init: function () {
            this._generateToc();
            this._bindEvents();
        },
        // 生成目录 (私有方法)
        _generateToc: function () {
            var self = this;
            var toc = $('<ul></ul>');
            var headings = $(this.settings.container).find(this.settings.headings);
            if (headings.length === 0) {
                this.settings.tocContainer.hide();
                return;
            }
            headings.each(function (index) {
                var $heading = $(this);
                var text = $heading.text();
                var id = $heading.attr('id') || 'heading-' + index;
                // 如果标题没有 id,则给它一个
                if (!$heading.attr('id')) {
                    $heading.attr('id', id);
                }
                var item = $('<li></li>');
                var link = $('<a></a>', {
                    href: '#' + id,
                    text: text
                });
                item.append(link);
                toc.append(item);
            });
            // 将生成的目录放入容器
            $(this.settings.tocContainer).html('').append(toc);
        },
        // 绑定事件 (私有方法)
        _bindEvents: function () {
            var self = this;
            var $tocContainer = $(this.settings.tocContainer);
            if (this.settings.smoothScroll) {
                $tocContainer.on('click', 'a', function (e) {
                    e.preventDefault(); // 阻止默认的跳转行为
                    var target = $(this.getAttribute('href'));
                    if (target.length) {
                        $('html, body').animate({
                            scrollTop: target.offset().top - 20 // 可以调整偏移量
                        }, self.settings.scrollSpeed);
                    }
                });
            }
        },
        // 公共方法:刷新目录
        refresh: function () {
            this._generateToc();
        },
        // 公共方法:销毁插件
        destroy: function () {
            $(this.settings.tocContainer).off('click'); // 移除事件监听
            // 可以在这里做更多清理工作
            $.removeData(this.element, 'plugin_toc');
        }
    };
    // jQuery 插件定义
    $.fn.toc = function (options) {
        // 创建插件实例
        this.each(function () {
            if (!$.data(this, 'plugin_toc')) {
                $.data(this, 'plugin_toc', new Toc(this, options));
            }
        });
        // 支持链式调用和公共方法调用
        // 如果传入的是字符串,则视为调用公共方法
        if (typeof options === 'string') {
            var args = Array.prototype.slice.call(arguments, 1);
            this.each(function () {
                var instance = $.data(this, 'plugin_toc');
                if (instance && typeof instance[options] === 'function') {
                    instance[options].apply(instance, args);
                }
            });
        }
        return this;
    };
})(jQuery);

如何使用 Toc 插件?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">TOC Plugin Demo</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="jquery.toc.js"></script>
    <style>
        body { font-family: sans-serif; }
        #toc { position: fixed; right: 20px; top: 20px; width: 200px; border: 1px solid #ccc; padding: 10px; }
        #toc ul { list-style-type: none; padding-left: 10px; }
        #toc a { display: block; padding: 5px; color: #333; text-decoration: none; }
        #toc a:hover { background-color: #eee; }
        h1, h2, h3 { margin-top: 50px; }
        .content { width: 70%; margin: 0 auto; }
    </style>
</head>
<body>
    <div id="toc"></div>
    <div class="content">
        <h1>第一章</h1>
        <p>这是第一章的内容...</p>
        <h2>第一节</h2>
        <p>这是第一节的内容...</p>
        <h3>第一小节</h3>
        <p>这是第一小节的内容...</p>
        <h2>第二节</h2>
        <p>这是第二节的内容...</p>
        <h1>第二章</h1>
        <p>这是第二章的内容...</p>
        <h2>第一节</h2>
        <p>这是第二章第一节的内容...</p>
    </div>
    <script>
        $(document).ready(function() {
            // 初始化插件
            $('body').toc({
                tocContainer: '#toc',
                smoothScroll: true,
                scrollSpeed: 500
            });
            // 假设页面内容是动态加载的,可以调用刷新方法
            // $('#loadContentBtn').on('click', function() {
            //     // ... 加载内容 ...
            //     $('body').toc('refresh');
            // });
        });
    </script>
</body>
</html>

编写 jQuery 插件是一个将通用功能模块化的好方法,从简单的函数封装,到复杂的、具有状态管理和事件处理能力的组件,遵循良好的结构和最佳实践,可以让你的插件更健壮、更易于维护和扩展,希望这份教程能帮助你掌握 jQuery 插件的开发!