Пишем плагин для jQuery

18.09.2011
@LEXXX_NF

В сети довольно много статей про то, как писать плагины для jQuery, но я решил, что лучше читать первоисточник. А поскольку русской версии мне найти не удалось, и руки чесались что-нибудь попереводить, я попереводил. Оригинал статьи тут, а мой перевод — ниже.

Итак, вы уже разобрались с jQuery и теперь хотели бы научиться писать собственные плагины. Отлично! Вы пришли в правильное место. Расширение возможностей jQuery с помощью плагинов и методов — это очень мощный механизм, который сэкономит вам и вашим коллегам не один час разработки. Эта статья описывает базовые моменты, рекомендации и распространённые ошибки, которые случаются при написании плагина.

Содержание

  1. 1. Приступая к работе
  2. 2. Контекст
  3. 3. Основы
  4. 4. Цепочки вызовов
  5. 5. Опции и значения по умолчанию
  6. 6. Пространства имён
    1. 6.1. Методы
    2. 6.2. События
    3. 6.3. Данные
  7. 7. Заключение и рекомендации

Приступая к работе

Начните писать jQuery-плагин с добавления новой функции-свойства к объекту jQuery.fn. Название функции и будет названием вашего плагина:

jQuery.fn.myPlugin = function() {

  // Творить волшебство здесь

};

Но постойте! Где же чудный значок доллара, к которому я так привык? Он есть, но чтобы убедиться, что ваш плагин не будет конфликтовать с другими библиотеками, которые тоже могут использовать знак доллара, будет лучше сделать так. Передадим объект jQuery в самовызывающуюся функцию (замыкание), которая привяжет его к знаку доллара, так что никакая другая библиотека не сможет его переопределить в текущем scopе'е исполнения.

(function( $ ){
  $.fn.myPlugin = function() {

    // Творить волшебство здесь

  };
})( jQuery );

Да, так лучше. Теперь внутри этого замыкания мы можем сколько угодно использовать знак доллара вместо jQuery.

Контекст

Теперь, когда у нас есть заготовка, мы можем начать писать собственно код плагина. Но перед этим, я бы хотел сказать пару слов про контекст. В непосредственном scope'е функции-плагина, ключевое слово this - это тот самый jQuery-объект, из которого был вызван плагин.

(function( $ ){

  $.fn.myPlugin = function() {

    //  нет необходимости делать $(this), потому что
    // "this" уже является jQuery-объектом

    // $(this) — это тоже самое, что и $($('#element'));

    this.fadeIn('normal', function() {

      //  this — элемент DOM

    });

  };
})( jQuery );
$('#element').myPlugin();

Основы

Теперь, когда мы знаем, что такое контекст jQuery-плагинов, давайте напишем плагин, который действительно что-то делает.

(function( $ ){

  $.fn.maxHeight = function() {

    var max = 0;

    this.each(function() {
      max = Math.max( max, $(this).height() );
    });

    return max;
  };
})( jQuery );
var tallest = $('div').maxHeight(); // Возвращает высоту самого высокого div'а

Этот простой плагин возвращает высоту самого высокого div'а на странице, используя метод .height().

Цепочки вызовов

Предыдущий пример возвращает высоту самого высокого div'а на странице, но зачастую задача плагина не вернуть определённое значение, а модифицировать каким-то образом набор элементов, и передать их дальше, следующему методу в цепочке. В этом красота разработки с использованием jQuery и это одна из причин, почему jQuery так популярен. Чтобы цепочки вызовов работали с вашим плагином, вы должны убедиться, что он возвращает this.

(function( $ ){

  $.fn.lockDimensions = function( type ) {

    return this.each(function() {

      var $this = $(this);

      if ( !type || type == 'width' ) {
        $this.width( $this.width() );
      }

      if ( !type || type == 'height' ) {
        $this.height( $this.height() );
      }

    });

  };
})( jQuery );
$('div').lockDimensions('width').css('color', 'red');

Благодаря тому, что плагин возвращает this, работают цепочки вызовов и jQuery-коллекция может быть передана следующему методу jQuery, например методу .css. Так что, если ваш плагин не должен возвращать какое-то значение, вам всегда следует возвращать this. Кроме того, как вы уже могли заметить, передаваемые в вызове плагина аргументы попадают в текущий scope функции-плагина. Так в предыдущем примере строка 'width' становится аргументом type внутри функции-плагина.

Опции и значения по умолчанию

Для сложных плагинов, которые обладают большим количеством параметров, рекомендуется задавать значения по умолчанию, которые могут быть переопределены (с помощью метода $.extend) во время вызова. Таким образом, при вызове плагина можно указать всего один параметр, который заменит соответствующее значение по умолчанию, а не перечислять все возможные параметры. Вот как это делается.

(function( $ ){

  $.fn.tooltip = function( options ) {

    var settings = {
      'location'         : 'top',
      'background-color' : 'blue'
    };

    return this.each(function() {
      // Если опции существуют, давайте объединим из с нашими значениями по умолчанию
      if ( options ) {
        $.extend( settings, options );
      }

      // Здесь идёт код плагина tooltip

    });

  };
})( jQuery );
$('div').tooltip({
  'location' : 'left'
});

В этом примере, после вызова плагина tooltip с заданными параметрами, значение для параметра location переписывается и становится равным 'left', а значение background-color остаётся как и было 'blue'. Итоговый объект с параметрами будет выглядеть так:

{
  'location'         : 'left',
  'background-color' : 'blue'
}

Значения по умолчанию — это отличный способ предоставить максимальную гибкость настройки плагина без необходимости указывать все параметры сразу.

Пространства имён

Важная часть разработки плагинов — пространства имён. Правильное применение пространств имён почти наверняка гарантирует, что ваш плагин не будут переопределён другим плагином или кодом, расположенным на той же странице. Так же, с помощью пространств имён разработчику плагинов легче следить за своими методами, событиями и данными.

Методы

Ни при каких обстоятельствах плагин не должен использовать больше одного пространства имён в объекте jQuery.fn.

(function( $ ){

  $.fn.tooltip = function( options ) { // ТАК };
  $.fn.tooltipShow = function( ) { // ДЕЛАТЬ };
  $.fn.tooltipHide = function( ) { // НЕЛЬЗЯ };
  $.fn.tooltipUpdate = function( content ) { // !!! };

})( jQuery );

Такой подход создаёт беспорядок в пространствах имён $.fn. Чтобы этого избежать, соберите все методы вашего плагина в один объект и вызывайте их путем передачи плагину названия нужного метода в качестве параметра.

(function( $ ){

  var methods = {
    init : function( options ) { // ТАК },
    show : function( ) { // ДЕЛАТЬ },
    hide : function( ) { // ПРАВИЛЬНО },
    update : function( content ) { // !!! }
  };

  $.fn.tooltip = function( method ) {

    // Логика вызова метода
    if ( methods[method] ) {
      return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Метод ' +  method + ' не существует в jQuery.tooltip' );
    }

  };

})( jQuery );
$('div').tooltip(); // вызов метода init
$('div').tooltip({  // вызов метода init
  foo : 'bar'
});
$('div').tooltip('hide'); // вызов метода hide
$('div').tooltip('update', 'Это новый контент тултипа!');  //  вызов метода update

Такой тип архитектуры позволяет вызывать методы плагина передачей первым параметром названия метода, а остальными параметрами — параметров, которые могут понадобиться этому методу. Он считается стандартом в среде разработчиков плагинов для jQuery и применяется в несчетном числе проектов, включая плагины и виджеты в jQueryUI.

События

У метода bind есть возможность, о которой мало кто знает, - использование пространств имён для привязки обработчиков событий. Если ваш плагин обрабатывает события, то было бы неплохо для обработчиков указать своё пространство имён. Тогда, если понадобится отвязать эти обработчики от событий, то вы сможете легко это сделать не затрагивая другие обработчики тех же событий. Чтобы назначить пространство имён нужно дописать .namespace к типу привязываемого события.

(function( $ ){

  var methods = {
     init : function( options ) {

       return this.each(function(){
         $(window).bind('resize.tooltip', methods.reposition);
       });

     },
     destroy : function( ) {

       return this.each(function(){
         $(window).unbind('.tooltip');
       })

     },
     reposition : function( ) { // ... },
     show : function( ) { // ... },
     hide : function( ) { // ... },
     update : function( content ) { // ... }
  };

  $.fn.tooltip = function( method ) {

    if ( methods[method] ) {
      return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Метод ' +  method + ' не существует в jQuery.tooltip' );
    }

  };

})( jQuery );
$('#fun').tooltip();
// А потом...
$('#fun').tooltip('destroy');

В этом примере, при инициализации плагина tooltip, происходит привязка обработчика reposition к событию resize объекта window с использованием пространства имён tooltip. Если потом понадобится отключить плагин, то отвязать привязанные им события можно будет передав методу unbind название пространства имён, в нашем случае - "tooltip". Таким образом можно отвязывать обработчики событий, привязанные плагином, не опасаясь случайно отвязать чужие.

Данные

Зачастую, при разработке плагинов, требуется сохранять состояния или проверять, был ли ваш плагин инициализирован на конкретном DOM-элементе. Удобный способ следить за переменными на уровне отдельных элементов — использование метода jQuery data. Но, вместо того, чтобы следить за кучей отдельных data-вызовов с разными именами, лучше сохранить все ваши переменные в один объект, используя его как пространство имён, и делать data-вызов к нему.

(function( $ ){

  var methods = {
     init : function( options ) {

       return this.each(function(){

         var $this = $(this),
             data = $this.data('tooltip'),
             tooltip = $('<div />', {
               text : $this.attr('title')
             });

         // Если плагин еще не был инициализирован
         if ( ! data ) {

           /*
             Здесь делаем еще какие-то вещи
           */

           $(this).data('tooltip', {
               target : $this,
               tooltip : tooltip
           });

         }
       });
     },
     destroy : function( ) {

       return this.each(function(){

         var $this = $(this),
             data = $this.data('tooltip');

         // Используем пространства имён FTW
         $(window).unbind('.tooltip');
         data.tooltip.remove();
         $this.removeData('tooltip');

       })

     },
     reposition : function( ) { // ... },
     show : function( ) { // ... },
     hide : function( ) { // ... },
     update : function( content ) { // ... }
  };

  $.fn.tooltip = function( method ) {

    if ( methods[method] ) {
      return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Метод ' +  method + ' не существует в jQuery.tooltip' );
    }

  };

})( jQuery );

Использование data помогает вам следить за переменными и состояниями между вызовами методов плагина. Хранение всех данных в одном объекте позволяет легко к ним обращаться, и, в случае необходимости, так же легко их удалять.

Заключение и рекомендации

Написание jQuery-плагинов позволяет наиболее эффективно использовать возможности библиотеки. А повторное использование кода экономит ваше время и делает разработку более эффективной. Вот краткие итоги поста, держите их в уме, когда будите писать свой следующий плагин:

  • Всегда оборачивайте свой плагин в функцию (function( $ ){ // здесь сам плагин })( jQuery );.
  • Не оборачивайте this в непосредственном scope плагина.
  • Если ваш плагин не должен возвращать важных значений, то всегда возвращайте this, чтобы работали цепочки вызовов.
  • Не запрашивайте большое количество аргументов при вызове плагина, лучше передавать объект, который переопределит настройки по умолчанию.
  • Не добавляйте в объект jQuery.fn более чем одно пространство имён на плагин.
  • Используйте пространства имён для ваших методов, событий и данных.
  • jQuery.fn читается как "джейКуэри эффин".
#1
Wet
22.09.2011 10:47
Очень хорошо перевел! Много времени заняло?
#2
@LEXXX_NF
22.09.2011 11:03
Если считать чистое время, то часов 10 на перевод + часа 4 на вычитку. Я не очень быстрый переводчик :)
#3
Kola
26.09.2011 08:46
А большая разница будет, если

{
return this.each(function(){
$(window).bind('resize.tooltip', methods.reposition);});
}

заменить на

{
$(window).bind('resize.tooltip', methods.reposition);
return this;
}

? Извиняюсь заранее, если глупый вопрос.
#4
@LEXXX_NF
26.09.2011 10:16
На мой взгляд, предложенный вами вариант, будет работать быстрее. Но, наверняка утверждать я не могу, так как не очень хорошо знаю внутренности jQuery. Надеюсь, мастер-класс Ильи Кантора, на который я уже записался, поможет мне узнать про этот фреймворк больше :)

А первый вариант с return this.each... скорее шаблонный. И, как и все шаблонные решения, он экономит время в ущерб эффективности.
#5
Kola
27.09.2011 08:32
Я тоже подумал, что ради цепочности это сделано, но контексты выполнения в сочетании с глобальностью DOM'а в js для меня не очень ясная вещь.

Надеюсь, напишите отзыв на семинар. У нас был этим летом, но я пропустил.
#6
@LEXXX_NF
26.10.2011 12:12
Семинар был потрясающий. Узнал очень много нового как в плане теории и архитектуры, так и в плане прикладных мелочей.

Рекомендую сходить и начинающим и бывалым.
#7
@LEXXX_NF
26.10.2011 12:26
Кстати, посмотрел в исходниках, как работает привязка событий. jQuery внутри делает each по коллекции и привязывает события к каждому элементу отдельно, но перед этим обрабатывает входящие параметры. Так что, в случае с window, когда коллекция состоит из одного элемента, разницы нет. А если коллекция состоит из нескольких элементов, то лучше не перебирать их вручную, а доверить это внутренним механизмам jQuery. Так мы избежим лишних проверок входных параметров.
#8
dvcarrot
14.05.2012 11:25
Реально помогающая статья, спасибо

Писáть здесь