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

в 15:25, , рубрики: javascript, jquery, jquery plugin authoring, Веб-разработка

Эта статья призвана дать представление об основных правилах, подходах, дающих наилучшие результаты, и распространённых ошибках, на которые стоит обратить внимание при разработке плагинов для JQuery.

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

Сперва создаём новое свойство-функцию для объекта jQuery, где именем нового свойства будет имя нашего плагина:

jQuery.fn.myPlugin = function() {

  // Тут пишем функционал нашего плагина

};

Но постойте, где-же привычный нам значок доллара, который мы все хорошо знаем? Он всё ещё здесь, а чтобы он не конфликтовал с другими библиотеками, которые тоже могут использовать символ доллара, рекомендуется «обернуть» объект jQuery в непосредственно выполняемую функцию-выражение (IIFE, Immediately Invoked Function Expression), которое связывает объект jQuery с символом "$", чтобы он не был переопределён другой библиотекой во время выполнения.

(function( $ ) {
  $.fn.myPlugin = function() {
  
      // Тут пишем функционал нашего плагина

  };
})(jQuery);

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

Контекст

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

И тут часто ошибаются, полагая, что в других вызовах, где jQuery принимает callback-функцию, «this» указывает на элемент DOM-дерева. Что, в свою очередь, приводит к тому, что разработчики дополнительно оборачивают «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-а

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

Поддерживаем возможность цепочек вызовов

Предыдущий пример рассчитывает и возвращает целочисленное значение наиболее высокого div-а на странице. Обычно, плагин модифицирует набор элементов дерева DOM, и передает их дальше, следующему методу в цепочке вызовов. В этом заключается красота 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 в непосредственной области видимости функции плагина. Аргументы, которые передаются в плагин при вызове, передаются в непосредственную область видимости функции плагина. Так, в предыдущем примере, строка 'width' является значением параметра «type» для функции плагина.

Настройки и умолчания

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

Так вместо вызова плагина с большим количеством параметром, вы можете вызвать его с одним параметром, являющимся объектным литералом настроек, которые вы хотите расширить. Например, вы можете сделать так:

(function( $ ){

  $.fn.tooltip = function( options ) {  

    // Создаём настройки по-умолчанию, расширяя их с помощью параметров, которые были переданы
    var settings = $.extend( {
      'location'         : 'top',
      'background-color' : 'blue'
    }, options);

    return this.each(function() {        

      // Тут пишем код плагина tooltip

    });

  };
})( jQuery );

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

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

{
  '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 );

// вызывает метод init
$('div').tooltip(); 

// вызывает метод init
$('div').tooltip({
  foo : 'bar'
});

// вызывает метод hide
$('div').tooltip('hide'); 

// вызывает метод update
$('div').tooltip('update', 'Теперь тут новое содержимое'); 

Этот тип архитектуры плагинов позволяет вам инкапсулировать все ваши методы в родительском по отношению к плагину замыкании (closure), и вызывать их, сперва передавая имя метода как строку, а затем передавая любые дополнительные параметры для этого метода. Этот подход к инкапсуляции методов является стандартом в сообществе разработчиков jQuery-плагинов и применяется в бесчисленном множестве плагинов и виджетов в jQueryUI.

События

Малоизвестная особенность метода bind заключается в том, что он позволяет определять пространства имён для связанных событий. Если ваш плагин связывает некую функциональность с каким-нибудь событием, то хорошим тоном будет задать пространство имён для этого события. И если позднее вам потребуется отвязать эту функциональность от события, то вы сможете это сделать, не затрагивая функциональность, которая может быть прикреплена к этому-же типу события.

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

(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 проинициализировался с помощью метода init, он связывает метод reposition с событием resize (изменение размеров) окна, с указанием пространства имён 'tooltip'. Позднее, когда разработчик намерен разрушить объект tooltip, он может отвязать все прикреплённые к плагину обработчики путём указания соответствующего пространства имён. В данном случае — 'tooltip' для метода unbind. Это позволяет безопасно отвязать обработчики от событий без риска случайно отвязать событие, связанное с обработчиком вне данного плагина.

Данные

Зачастую во время разработки плагинов, вы можете столкнуться с необходимостью сохранения состояний или проверки, был-ли плагин уже проинициализирован для указанного элемента. Использование метода data из jQuery — это хороший способ отслеживать состояние переменных для каждого элемента. Однако вместо того, чтобы отслеживать множество отдельных вызовов 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');

         // пространства имён рулят!!11
         $(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 позволяет отслеживать состояние переменных между вызовами вашего плагина. Определение пространства имён для data в одном объектном литерале, обеспечивает, как простой централизованный доступ к свойствам плагина, так и сокращает пространство имён data, что позволяет просто удалять ненужные данные по мере необходимости.

Заключение и полезные советы

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

  • Всегда оборачивайте свой плагин в конструкцию:
               (function( $ ){ 
                          /* тут пишем код плагина */ 
                })( jQuery );
    

    Примечание переводчика: в оригинальной статье эта синтаксическая конструкция названа замыканием (closure), но это не замыкание, а непосредственно вызываемая функция (IIFE).

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

Автор: Rafael

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js