Pull to refresh

Основные паттерны плагинов jQuery

Время от времени я пишу статьи о паттернах в Javascript-е — современных методах решений стандартных проблем в разработке, в профите применений которых я абсолютно уверен. Общие паттерны хороши для «чистого» Javascript-а, но некоторые «темные стороны» веба имеют свои фишки. Сегодня мы поговорим о плагинах jQuery. Мануал по ним дает неплохой старт, но я предлагаю копнуть глубже.

За последние годы разработка плагинов под эту библиотеку продвинулась довольно далеко. Девелопер уже не ограничен строгим шаблоном, некоторые места подвластны его фантазии. Грубо говоря, у нас появилась возможность выбирать между паттернами для реализации того или иного функционала.

Многие предпочитают довольствоваться стандартной фабрикой jQuery UI, которой достаточно для общих, более менее обычных задач. Есть люди, содержащие свои плагины в виде модулей, или в более «правильном» виде AMD. Еще строят на прототипировании, событиях, и т.п.

Желание создать паттерн плагинов типа «все-в-одном» заставило меня задуматься. Теоретически это круто, и, как показывает практика, мы редко используем один единственный паттерн при создании плагинов. Т.е., должно взлететь.

Вспомните — вы решили написать свой плагин, и «вы это сделали!», он работает. Он полностью удовлетворяет требованиям, делает, что было задумано. Но в какой-то момент вам кажется, что «он» мог быть и лучше. Можно было сделать его чуть гибче, чуть функциональнее. Если все это знакомо, вам будет интересна данная статья.

Дисклаимер


Мои рекомендации не претендуют на идеальные решения, они всего лишь раскроют разновидность этих решений, применяемых в мире.

Данная статья нацелена на людей, владеющих предметной областью от среднего до продвинутого уровня. Если вам сложно, рекомендую начать со стандартного манула, продолжить авторитетными статьями Бена Алмана и Реми Шарпа.

От переводчика: данная статья является переводом замечательного обзора паттернов от Addy Osmani, Essential jQuery Plugin Patterns.

Паттерны


В настоящее время плагины jQuery имеют небольшой список правил реализации, которые способствуют их нынешнему разнообразию. Самый простой способ – это расширить объект fn новым свойством, описанным в виде функции

$.fn.myPluginName = function() {
    // код плагина
};

Но, очевидно, лучше вот так

(function( $ ){
  $.fn.myPluginName = function() {
    // код плагина
  };
})( jQuery );

Тут мы обернули плагин в анонимную функцию, предотвращая конфликты между jQuery, и другими библиотеками, возможно используемыми в проекте. «Так» — правильно!

Можно использовать метод $.extend для расширения fn, если нам нужно определить несколько плагинов в одном месте


(function( $ ){
    $.extend($.fn, {
        myplugin: function(){
            // код плагина
        }
    });
})( jQuery );

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

Конечно, я постараюсь подробно пояснять все паттерны, но настоятельно прошу вас уделить внимание комментариям в коде – в них более ясно раскрыта суть рациональности применяемых решений.

Ну, и, конечно, все что я здесь пишу, было бы невозможным без предыдущего опыта, инпута и советов всего сообщества jQuery. Я отдельно добавил некоторые копирайты, по которым вы сможете более детально ознакомиться с работами авторов. Риспект им, и спасибо.

Базовый паттерн


Итак, начнем с простого паттерна, включающего в себя стандартные рекомендации официального манула jQuery. Он идеален для начинающих разработчиков плагинов, или же для реализации элементарного функционала. Данный паттерн содержит в себе:
  • Общепринятые решения, такие как «предваряющие точка с запятой», страховочные аргументы window, document и undefined, и гайдлайны синтаксиса написания плагинов jQuery.
  • простое определение объекта.
  • Простой конструктор с начальной логикой создания и определения элементов плагина.
  • Расширение входных параметров параметрами по умолчанию.
  • Небольшой декоратор конструктора, предотвращающий дублирование плагинов.


/*!
 * паттерн простого плагина jQuery
 * автор: @ajpiano
 * дополнения: @addyosmani
 * лицензия MIT
 */

// предваряющие точка с запятой предотвращают ошибки соединений
// с предыдущими скриптами, которые, возможно
// не были верно «закрыты».
;(function ( $, window, document, undefined ) {

    // т.к. undefined, по определению ECMAScript 3, не является константой
    // здесь мы явно задаем неопределенную переменную
    // убеждаясь в ее действительной неопределенности
    // В стандарте ES5, undefined уже точно константа

    // window и document передаются локально, вместо глобальных
    // переменных, что немного ускоряет процесс определения
    // и позволяет более эффективно минифицировать
    // ваш код, если эти переменные часто используются
    // в вашем плагине

    // определяем необходимые параметры по умолчанию
    var pluginName = 'defaultPluginName',
        defaults = {
            propertyName: "value"
        };

    // конструктор плагина
    function Plugin( element, options ) {
        this.element = element;

        // в jQuery есть метод extend, который
        // объединяет несколько объектов в один,
        // сохраняя результат в первый объект.
        // зачастую первый объект является пустым,
        // предотвращая затирание дифолтных значений
        this.options = $.extend( {}, defaults, options) ;

        this._defaults = defaults;
        this._name = pluginName;

        this.init();
    }

    Plugin.prototype.init = function () {
        // Тут пишем код самого плагина
        // Здесь у нас уже есть доступ к DOM, и входным параметрам
        // через объект, типа this.element и this.options
    };

    // Простой декоратор конструктора,
    // предотвращающий дублирование плагинов
    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if (!$.data(this, 'plugin_' + pluginName)) {
                $.data(this, 'plugin_' + pluginName,
                new Plugin( this, options ));
            }
        });
    }

})( jQuery, window, document );

Стоит почитать:

Фабрика виджетов


Хотя стандартная документация и покрывает многие аспекты написания плагинов, она не предлагает решений задач, с которыми мы зачастую сталкиваемся в реальной жизни.

Фабрика виджетов jQuery является одним из решений для создания сложных, хранящих состояние плагинов, основанных на ООП. Она также упрощает взаимодействие с объектом плагина, унифицируя общие задачи, решение которых бы вам пришлось отдельно реализовывать в обычных плагинах.

Хранимое (stateful) состояние позволяет отслеживать текущие свойства плагина, с возможностью их изменения, даже после полной инициализации плагина.

Фишкой фабрики является то, что она используется практически всеми плагинами пакета jQuery UI. Т.е. в рамках пакета вам не придется ковырять ничего, кроме данного паттерна – все стандартно.
Смотрите
  • Паттерн покрывает все стандартные методы, включая события
  • Код покрыт обширными комментариями, облегчая процесс написания плагина

/*!
 * шаблон фабрики плагина jQuery UI (версий 1.8/9+)
 * автор: @addyosmani
 * дополнения: @peolanha
 * лицензия MIT
 */

;(function ( $, window, document, undefined ) {

    // можно определять неймспейс плагина, 
    // со своими параметрами, к примеру
    // $.widget( "namespace.widgetname", (optional)) - наследуемый
    // прототип виджета, и прототипирование нового виджета 

    $.widget( "namespace.widgetname" , {

        // Дифолтные значения
        options: {
            someValue: null
        },

        // Создание виджета
        // (создание элементов, назначение стилей, навешивание событий)
        _create: function () {

            // метод _create будет вызван автоматически
            // при первом вызове виджета.
            // здесь можно обращаться к элементу,
	    // на котором создается виджет, через this.element.
            // Свойства и методы доступны через
            // this.options и this.element.addStuff();
        },

        // деструктор плагина и
        // очистка изменений в DOM, сделанных плагином
        destroy: function () {

            // this.element.removeStuff();
            // для версий jQuery UI 1.8, нужно вызывать 
            // метод destroy базового виджета 
            $.Widget.prototype.destroy.call(this);
            // для версий UI 1.9 достаточно определить
            // метод _destroy, и можно не вызывать базовый деструктор
        },

        methodB: function ( event ) {
            // метод _trigger вызывает события
            // на которые можно подписываться
            // сигнатура: _trigger( "callbackName" , [eventObject],
            // [uiObject] )
            // например this._trigger( "hover", e /*where e.type ==
            // "mouseenter"*/, { hovered: $(e.target)});
            this._trigger('methodA', event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger('dataChanged', event, {
                key: value
            });
        },

        // Метод переопределения свойств плагина
        _setOption: function ( key, value ) {
            switch (key) {
            case "someValue":
                //this.options.someValue = doSomethingWith( value );
                break;
            default:
                //this.options[ key ] = value;
                break;
            }

            // для версий UI 1.8 приходится
            // вызывать метод базового виджета
            $.Widget.prototype._setOption.apply( this, arguments );
            // для UI 1.9 достаточно вызвать метод _super
            // this._super( "_setOption", key, value );
        }
    });

})( jQuery, window, document );

Стоит почитать:

Организация пространством имен


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

К сожалению, в Javascript-е нет нативной поддержки пространств, как в других языках, но благодаря структуре объявления объектов, можно достичь почти такого же результата. Обрамляя ваш код в объект, который будет реализовывать ваше пространство, вы можете с легкостью удостовериться в отсутствии конфликтов с другими, одноименными объектами. Т.е. если такого объекта нет, мы его объявляем, если же есть – расширяем своим плагином.

Те же объекты, или, вернее – литералы, можно использовать для построения вложенных пространств, типа namespace.subnamespace.pluginName. Чтобы вам стало понятнее, вот шаблон, которого должно быть достаточно для начала использования данного паттерна.


/*!
 * базовый шаблон неймспейсового паттерна jQuery
 * автор: @dougneiner
 * дополнения: @addyosmani
 * лицензия MIT
 */

;(function ( $ ) {
    if (!$.myNamespace) {
        $.myNamespace = {};
    };

    $.myNamespace.myPluginName = function ( el, myFunctionParam, options ) {
        // Для предотвращения конфликтов контекста
	// используем 'base' вместо 'this'
        // для дальнейших обращений к объекту из методов и событий
        var base = this;

        // Объявляем jQuery и DOM версии элемента
        base.$el = $(el);
        base.el = el;

        // Добавим обратную связь к DOM объекту
        base.$el.data( "myNamespace.myPluginName" , base );

        base.init = function () {
            base.myFunctionParam = myFunctionParam;

            base.options = $.extend({},
            $.myNamespace.myPluginName.defaultOptions, options);

            // Тут пишем наш код
        };

        // Пример функции
        // base.functionName = function( paramaters ){
        //
        // };
        // Инициируем плагин
        base.init();
    };

    $.myNamespace.myPluginName.defaultOptions = {
        myDefaultValue: ""
    };

    $.fn.mynamespace_myPluginName = function
        ( myFunctionParam, options ) {
        return this.each(function () {
            (new $.myNamespace.myPluginName(this,
            myFunctionParam, options));
        });
    };

})( jQuery );

Стоит почитать:

Частные события паттерна Pub/Sub (с фабрикой виджетов)


Возможно, вы уже сталкивались с паттерном «наблюдателя» для обеспечения асинхронного кода в Javascript-e. Основная идея в том, что элементы вашего кода время от времени вещают те или иные события, происходящие в вашей программе. Другие элементы могут подписываться на эти оповещения, и реагировать соответственно. Как результат, вы обеспечиваете более слабое связывание кода, что всегда есть очень хорошо.

Данная задумка реализована в jQuery «нативно», практически не отличаясь от базовых тезисов паттерна событий – bind(“eventType”) равнозначно subscribe(“eventType”), а trigger(“eventType”) выполняет тоже самое, что и publish(“eventType”).

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


/*!
 * событийный шаблон jQuery
 * автор: DevPatch
 * дополнения: @addyosmani
 * лицензия MIT 
 */

// В этом паттерне используем события jQuery
// для обеспечения возможностей pub/sub (publish/subscribe).
// Каждый виджет будет порождать и подписываться на разные события
// Данный подход позволит разграничить виджеты, 
// обеспечивая их независимое существование

;(function ( $, window, document, undefined ) {
    $.widget("ao.eventStatus", {
        options: {

        },

        _create : function() {
            var self = this;

            //self.element.addClass( "my-widget" );

            //подписываемся на событие 'myEventStart'
            self.element.bind( "myEventStart", function( e ) {
                console.log("event start");
            });

            //на событие 'myEventEnd'
            self.element.bind( "myEventEnd", function( e ) {
                console.log("event end");
            });

            //отписываемся от события 'myEventStart'
            //self.element.unbind( "myEventStart", function(e){
                ///console.log("unsubscribed to this event");
            //});
        },

        destroy: function(){
            $.Widget.prototype.destroy.apply( this, arguments );
        },
    });
})( jQuery, window , document );

//Публикация событий
//пример:
// $(".my-widget").trigger("myEventStart");
// $(".my-widget").trigger("myEventEnd");

Стоит почитать:

Паттерн наследования через прототип, со связкой DOM-к-объекту


В Javascript-e, как нам известно, нет понятия классов, как в других классических языках. Но зато есть прототипированное наследование, которое позволяет одни объекты наследовать другими. Данный метод также можно с успехом применить и на плагинах jQuery.

Alex Sexton и Scott Gonzlez в свое время подробно раскрывали суть этого подхода. Грубо говоря, они донесли народу, насколько удобно организовывать процесс модулями, разделяя логику плагина, и код его создания. Это позволяет более качественно тестировать ваш код, и вносить изменения в сам плагин без нарушений работы его внешних пользователей.


/*!
 * шаблон прототипирования плагинов jQuery 
 * авторы: Alex Sexton, Scott Gonzalez
 * дополнения: @addyosmani
 * лицензия MIT
 */

// myObject – объект реализуемой модели (например, машина)
var myObject = {
  init: function( options, elem ) {
    // объединяем входные параметры с дифолтными
    this.options = $.extend( {}, this.options, options );

    // Сохраняем указатели на элементы
    this.elem  = elem;
    this.$elem = $(elem);

    // реализуем базовую структуру DOM
    this._build();

    // возвращаем this для более простого обращения к объекту
    return this;
  },
  options: {
    name: "No name"
  },
  _build: function(){
    //this.$elem.html('<h1>'+this.options.name+'</h1>');
  },
  myMethod: function( msg ){
    // тут у нас есть прямой доступ к сохраненным ранее указателям
    // this.$elem.append('<p>'+msg+'</p>');
  }
};

// проверим наличие метода Object.create
// создадим, если отсутствует
if ( typeof Object.create !== 'function' ) {
    Object.create = function (o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}

// создание плагина на основе описанного объекта
$.plugin = function( name, object ) {
  $.fn[name] = function( options ) {
    return this.each(function() {
      if ( ! $.data( this, name ) ) {
        $.data( this, name, Object.create(object).init(
        options, this ) );
      }
    });
  };
};

// Пример использования:
// превращаем myObject в плагин
// $.plugin('myobj', myObject);

// и используем, как обычно
// $('#elem').myobj({name: "John"});
// var inst = $('#elem').data('myobj');
// inst.myMethod('I am a method');

Стоит почитать:

Мост фабрики виджетов jQuery UI


Если вам понравилась идея создания плагинов на основе моделей, вам стоит обратить внимание на метод фабрики виджетов jQuery UI — $.widget.bridge. Это мост, связывающий ваш объект виджета с API jQuery UI, позволяя использовать предыдущий паттерн. Фактически, отдельным конструктором, мы можем создавать плагины с хранимым состоянием.

Более того, метод $.widget.bridge предоставляет дополнительные возможности:
  • разделение методов на приватные и общие
  • защита от дублирования экземпляров
  • автоматическое создание экземпляра переданного объекта, с сохранением в кеш ($.data) элемента
  • изменяемые параметры плагина, даже после создания

Подробнее можно узнать из следующего паттерна


/*!
 * паттерн плагина на основе «моста» фабрики виджетов jQuery UI 
 * автор: @erichynds
 * дополнения: @addyosmani
 * лицензия MIT
 */

// конструктор объекта "widgetName"
// обязательны следующие аргументы
// options: объект опций плагина
// element: DOM элемент плагина
var widgetName = function( options, element ){
  this.name = "myWidgetName";
  this.options = options;
  this.element = element;
  this._init();
}

// прототипируем "widgetName"
widgetName.prototype = {

    // метод _create автоматически выполняется при первом 
    // вызове виджета
    _create: function(){
        // код создания плагина
    },

    // метод _init является обязательным методом фабрики
    // Он вызывается после создания 
    // и каждый раз, при каждой повторной его инициализации
    _init: function(){
        // код инициализации плагина
    },

    // метод 'option' также обязательный
    // в нем реализуется механизм работы с параметрами плагина
    option: function( key, value ){

        // ниже приведен пример изменения и получения параметров
        // сигнатура: $('#foo').bar({ cool:false });
        if( $.isPlainObject( key ) ){
            this.options = $.extend( true, this.options, key );

        // сигнатура: $('#foo').option('cool'); - геттер
        } else if ( key && typeof value === "undefined" ){
            return this.options[ key ];

        // сигнатура: $('#foo').bar('option', 'baz', false);
        } else {
            this.options[ key ] = value;
        }

        // обязательным условием метода «option»  
        // является возвращение текущего экземпляра.
        // при повторной инициализации плагина,
	// вызывается «option», и передается в метод _init
        return this;
    },

    // общие методы определяются без нижнего подчеркивания
    publicFunction: function(){
        console.log('public function');
    },

    // нижнее подчеркивание определяет приватные методы
    _privateFunction: function(){
        console.log('private function');
    }
};

// пример использования:

// соединяем нашу модель к API jQuery в пространство "foo"
// $.widget.bridge("foo", widgetName);

// создаем экземпляр виджета
// var instance = $("#elem").foo({
//     baz: true
// });

// экземпляр всегда доступен через кеш элемента
// instance.data("foo").element; // => #elem element

// мост позволяет вызывать общие методы...
// instance.foo("publicFunction"); // => "public method"

// но не даст вам вызвать приватные
// instance.foo("_privateFunction"); // => #elem element

Стоит почитать:

Фабрика виджетов jQuery Mobile


Многим, наверное, знакома библиотека jQM, позволяющая разрабатывать универсальные интерфейсы, которые работают и на популярных мобильных устройствах, и на десктопных браузерах одинаково хорошо. Т.е., вместо поддержания нескольких версий вашего приложения, вы просто пишете код, который работает на многих браузерах A, B и C классов.

Основы jQM широко применимы и в разработке плагинов/виджетов, что можно увидеть из некоторых встроенных мобильных виджетов в официальном пакете. Интересно, что даже не смотря на некоторые отличия написания мобильных виджетов, если вы владеете навыками написания виджетов используя фабрику jQuery UI, вам можете с легкостью написать его мобильную версию.

В коде ниже мы рассмотрим некоторые особенности мобильных виджетов:
  • $.mobile.widget указывает на прототип текущего виджета, доступный для наследия. Такой вариант прототипирования не требуется при разработке стандартных виджетов, но в случае мобильной версии – это дает возможность дополнительной обработки входных параметров options.
  • Выборка элементов в мобильных виджетах основана на role-атрибутах, что более соответствует разметке jQM. Это совсем не значит, что обычные селекторы хуже, просто такой поход более оправдан при создании страниц на jQM.


/*!
 * (jQuery mobile) паттерн виджетов на основе фабрики jQuery (1.8/9+)
 * автор: @scottjehl
 * дополнения: @addyosmani
 * лицензия MIT
 */

;(function ( $, window, document, undefined ) {

    //определяем наш виджет в отдельном пространстве
    //в примере указан неймспейс 'mobile' 
    $.widget( "mobile.widgetName", $.mobile.widget, {

        //дефолтные параметры
        options: {
            foo: true,
            bar: false
        },

        _create: function() {    
	    // этот метод выполняется автоматически при первом вызове виджета
	    // доступ к элементу виджета через this.element
            //var m = this.element,
            //p = m.parents(":jqmData(role='page')"),
            //c = p.find(":jqmData(role='content')")
        },

        // приватные методы/свойства определяются
	// с начальным нижним подчеркиванием
        _dosomething: function(){ ... },

        // общие методы типа такого доступны снаружи
        // $("#myelem").foo( "enable", arguments );

        enable: function() { ... },

        // уничтожаем экземпляр виджета
	// и очищаем DOM от проделанных изменений
        destroy: function () {
            //this.element.removeStuff();
            // для версии UI 1.8, вызываем метод destroy родителя
            $.Widget.prototype.destroy.call(this);
            // для UI 1.9, достаточно определить метод _destroy
        },

        methodB: function ( event ) {
            //метод _trigger публикует события, на которые можно подписываться
            //сигнатура: _trigger( "callbackName" , [eventObject],
            //  [uiObject] )
            // пример: this._trigger( "hover", e /*where e.type ==
            // "mouseenter"*/, { hovered: $(e.target)});
            this._trigger('methodA', event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger('dataChanged', event, {
                key: value
            });
        },

        //логика управления параметрами плагина
        _setOption: function ( key, value ) {
            switch (key) {
            case "someValue":
                //this.options.someValue = doSomethingWith( value );
                break;
            default:
                //this.options[ key ] = value;
                break;
            }

            // для UI 1.8, нужно вызывать метод _setOption базового виджета
            $.Widget.prototype._setOption.apply(this, arguments);
            // для UI 1.9 нужно использовать метод _super
            // this._super( "_setOption", key, value );
        }
    });

})( jQuery, window, document );

//usage: $("#myelem").foo( options );

/* Дополнительные рекомендации – удалите их перед началом использования паттерна

Можно автоматически запускать этот виджет при каждом создании нового "page" в jQuery Mobile. Плагин "page" из jQuery Mobile публикует событие "create" при каждом его создании (атрибут data-role=page).

Мы можем подписаться на это событие ("pagecreate") и автоматически создать наш виджет.

$(document).bind("pagecreate", function (e) {
    // здесь, e.target указывает на создаваемый элемент page
    // таким образом мы просто ищем нужный нам элемент в рамках страницы
    // и применить к нему виджет.
    // ниже пример применения плагина "foo" ко всем элементам
    // с атрибутом data-role равным "foo":
    $(e.target).find("[data-role='foo']").foo(options);

    // но лучше указывать настраиваемый селектор атрибута data
    $(e.target).find(":jqmData(role='foo')").foo(options);
});

Как-то так. Теперь пихаем код вашего плагина на основе этого паттерна в страницу, написанную на jQuery Mobile, и он будет работать как любой другой плагин jQM.
 */

RequireJS и фабрика виджетов jQuery UI


RequireJS – лоадер скриптов, позволяющий разделить ваш код на модули. Он может соблюдать порядок загрузки модулей, существенно упрощает процесс соединения скриптов своим оптимайзером, и позволяет задавать зависимости модулей.

James Burke в свое время написал исчерпывающий мануал по RequireJS, и если вдруг вы уже имеете опыт его использования – у вас есть возможность декорировать ваши виджеты/плагины в модули RequireJS.

В следующем паттерне мы рассмотрим пример такой декорации, в котором предоставлены такие возможности:
  • Возможность указания зависимостей виджета, используя паттерн рассмотренный ранее
  • Вариант передачи HTML тимплейтов в виджет, для создания виджетов в связке с плагином tmpl
  • Совет по настройке модуля для дальнейшей оптимизации средствами RequireJS


/*!
 * паттерн модульного виджета jQuery UI + RequireJS (версий 1.8/9+)
 * авторы: @jrburke, @addyosmani
 * лицензия MIT
 */

// замечание от James:
//
// Данный паттерн предполагает, что вы используете RequireJS+jQuery,
// и следующие файлы размещены в одной папке:
//
// - require-jquery.js
// - jquery-ui.custom.min.js (билд jQuery UI с фабрикой виджетов)
// - templates/
//    - asset.html
// - ao.myWidget.js 

// После этого вы можете строить ваши виджеты примерно вот так: 

//ao.myWidget.js file:
define("ao.myWidget", ["jquery", "text!templates/asset.html", "jquery-ui.custom.min","jquery.tmpl"], function ($, assetHtml) {

    // для примера заключаем виджет в пространство 'ao'
    $.widget( "ao.myWidget", { 

        // параметры по умолчанию
        options: {}, 

        // создаем виджет (создаем элемент, применяем стили
        // вешаем обработчики событий и т.п.)
        _create: function () {

            	         // этот метод выполняется автоматически при первом вызове виджета
	    // доступ к элементу виджета через this.element,
            // к параметрам - this.options

            //this.element.addStuff();
            //this.element.addStuff();
            //this.element.tmpl(assetHtml).appendTo(this.content);
        },

        	// уничтожаем экземпляр виджета
	// и очищаем DOM от проделанных изменений
        destroy: function () {
            // this.element.removeStuff();
            	          // для версии UI 1.8, вызываем метод destroy родителя
            $.Widget.prototype.destroy.call(this);
            // для UI 1.9, достаточно определить метод _destroy
        },

        methodB: function ( event ) {
                            //метод _trigger публикует события, на которые можно подписываться
            //сигнатура: _trigger( "callbackName" , [eventObject],
            // [uiObject] )
            this._trigger('methodA', event, {
                key: value
            });
        },

        methodA: function ( event ) {
            this._trigger('dataChanged', event, {
                key: value
            });
        },

        	//логика управления параметрами плагина
        _setOption: function ( key, value ) {
            switch (key) {
            case "someValue":
                //this.options.someValue = doSomethingWith( value );
                break;
            default:
                //this.options[ key ] = value;
                break;
            }

            // для UI 1.8, нужно вызывать метод _setOption базового виджета
            $.Widget.prototype._setOption.apply(this, arguments);
            // для UI 1.9 нужно использовать метод _super
            // this._super( "_setOption", key, value );
        }

        // ну и где-то в коде можно использовать переданный тимплейт
	// assetHtml.
    });
}); 

// Если вы планируете использовать оптимизатор RequireJS для соединения
// файлов, вы можете опустить неймспейс в аргументах:
// define(["jquery", "text!templates/asset.html", "jquery-ui.custom.min"], …

Стоит почитать:

Динамически настраиваемые параметры


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

Но что, если у нас плагин напыщен параметрами, которые нужно/можно изменять в любой момент? Тут можно подойти креативно.

Объявим объект параметров доступным глобально, в неймспейсе нашего плагина, и будем объединять его с параметрами по умолчанию. Это даст возможность указывать параметры на старте плагина, как обычно, и в добавок – мы сможем изменять объект параметров снаружи, когда нам это понадобится.


/*!
 * паттерн динамических параметров плагина jQuery 
 * автор: @cowboy
 * дополнения: @addyosmani
 * лицензия MIT
 */

;(function ( $, window, document, undefined ) {

    $.fn.pluginName = function ( options ) {

        // здесь привычный метод объединения входящих параметров
	// с параметрами по умолчанию.
        // но обратите внимание, что вместо обычного объекта параметров
        // мы используем $.fn.pluginName.options, объединяя его с
        // входными параметрами.
        // это дает возможность указывать параметры как на входе
        // так и глобально. 

        options = $.extend( {}, $.fn.pluginName.options, options );

        return this.each(function () {

            var elem = $(this);

        });
    };

    // литерал глобальных параметров по умолчанию
    // который доступен снаружи    
    // пользователь имеет возможность переопределять те
    // или иные параметры плагина
    // например: $fn.pluginName.key ='otherval';

    $.fn.pluginName.options = {

        key: "value",
        myMethod: function ( elem, param ) {

        }
    };

})( jQuery, window, document );

Стоит почитать:

Самонастраиваемый плагин


Как и в паттерне Алекса Секстона, в нашем следующем паттерне, логика плагина вынесена за пределы самого плагина. Здесь мы используем сторонний конструктор, и его прототип, а инициализация плагина осуществляется силами jQuery.

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

Обычно такой подход мало применяется в реале, но на практике это позволит получить более чистую реализацию (если вы не паритесь на счет инлайн-параметров). Давайте рассмотрим, где такой подход будет иметь смысл.

Представьте, что вам нужно навесить плагин draggable на кучку элементов, с разными параметрами. Обычно это делается как-то так:

javascript

$('.item-a').draggable({'defaultPosition':'top-left'});
$('.item-b').draggable({'defaultPosition':'bottom-right'});
$('.item-c').draggable({'defaultPosition':'bottom-left'});
//и т.д.

Но, можно и вот так:

javascript

$('.items').draggable();

html

<div class="item" data-plugin-options='{"defaultPosition":"top-left"}'></div>
<div class="item" data-plugin-options='{"defaultPosition":"bottom-left"}'></div>

Конечно, у каждого из способов есть свои преимущества, но все же данный паттерн стоит внимания.


/*
 * паттерн самонастраиваемого плагина
 * автор: @markdalgleish
 * дополнения: @addyosmani
 * лицензия MIT
 */

;(function( $, window, document, undefined ){

  // итак, объявляем конструктор
  var Plugin = function( elem, options ){
      this.elem = elem;
      this.$elem = $(elem);
      this.options = options;

      // следующая строка использует
      // всю мощь дата-атрибутов HTML5,
      // позволяя нам указывать параметры плагина для 
      // текущего элемента, к примеру
      // <div class=item' data-plugin-options='{"message":"Goodbye World!"}'></div>
      this.metadata = this.$elem.data( 'plugin-options' );
    };

  // прототипируем конструктор
  Plugin.prototype = {
    defaults: {
      message: 'Hello world!'
    },

    init: function() {
      // параметры по умолчанию, которые можно задать на старте
      // или позже, через глобальный метод
      this.config = $.extend({}, this.defaults, this.options,
      this.metadata);

      // например:
      // изменяем сообщение для текущего экземпляра:
      // $('#elem').plugin({ message: 'Goodbye World!'});
      // или же
      // var p = new Plugin(document.getElementById('elem'),
      // { message: 'Goodbye World!'}).init()
      // или еще вариант, задать глобальное сообщение:
      // Plugin.defaults.message = 'Goodbye World!'

      this.sampleMethod();
      return this;
    },

    sampleMethod: function() {
      // выведем текущее сообщение
      // console.log(this.config.message);
    }
  }

  Plugin.defaults = Plugin.prototype.defaults;

  $.fn.plugin = function(options) {
    return this.each(function() {
      new Plugin(this, options).init();
    });
  };

  //по желанию: window.Plugin = Plugin;

})( jQuery, window , document );

Стоит почитать:

Универсальные модули, совместимые с AMD и CommonJS


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

Именно поэтому, многие разработчики, включая меня, сопроводителя CDNjsThomas Davis и RP Florence, стали интересоваться спецификациями модулей AMD (Asynchronous Module Definition) и CommonJS в надежде расширить наши паттерны для работы с пакетами, и зависимостями. John Hann и Kit Cambridge также провели некоторую работу в этом направлении.

AMD

Спецификация модулей AMD (когда модули и их зависимости могут подгружаться асинхронно) имеет ряд преимуществ, включая асинхронную природу существования кода, лишая нас недостатка тесного связывания, присущего разработке модулей. Данная спецификация считается важной ступенью развития модульной системы, предложенной в ES Harmony.

Работа с анонимными модулями подразумевает идею DRY, помогая избежать повторений кода и названий файлов. Благодаря атомарности кода, модули могут быть спокойно внедрены в другие системы без изменений. Появляется возможность запускать один и тот же код в разных средах, используя оптимизатор AMD. Один из примеров — r.js. утилита с поддержкой спецификаций CommonJS.

Принимаясь за разработку AMD, вы будете опираться на два основных метода – require и define, которые, фактически, и определяют порядок загрузок модулей, и организацию их зависимостей.

Метод define используется для объявления модуля в системе. Имеет такую сигнатуру


define(module_id /*опционально*/, [dependencies], definition function /*функция инициализации модуля, или объекта*/);

Как видно из комментариев, идентификатор модуля можно не указывать. Он используется только в тех случаях, когда нам нужно соединить код средствами, не совместимыми со спецификацией AMD. В остальных случаях, не указывая идентификатор модуля, мы получаем возможность перемещать наш модуль в файловой системе, без изменений его названия. Т.е. идентификатор модуля является его путем в файловой системе.

Второй аргумент задает зависимости вашего модуля от других, необходимых для корректной его работы. Третий аргумент является фабрикой, которая создает ваш модуль. Взгляните на примеры:


// заметьте, что в данном коде название модуля (myModule) 
// использовано только для примера

define('myModule', ['foo', 'bar'], function ( foo, bar ) {
    // возвращаем значения для экспорта
    // т.е. функционал, который мы ходим реализовать
    return function () {};
});

// можно реализовать чуть лучше:
define('myModule', ['math', 'graph'], function ( math, graph ) {
    return {
            plot: function(x, y){
                    return graph.drawPie(math.randomGrid(x,y));
            }
    };
});

Метод require, в свою очередь, используется для подгрузки вашего модуля в код приложения:


// в данном примере экспортированный код из двух подгруженных модулей
// передается аргументами в функцию

require(['foo', 'bar'], function ( foo, bar ) {
        // rest of your code here
});

// здесь пример AMD, показывающий динамически подгружаемые зависимости
define(function ( require ) {
    var isReady = false, foobar;

    require(['foo', 'bar'], function (foo, bar) {
        isReady = true;
        foobar = foo() + bar();
    });

    // можно также вернуть модуль
    return {
        isReady: isReady,
        foobar: foobar
    };
});

Представленный выше код является очень тривиальным, но достаточным для понятия основ спецификаций AMD. Этот подход используют многие крупные компании в своих разработках. Спецификация активно развивается последние годы, поддерживаясь кругами Dojo и CommonJS. Для более детального ознакомления, рекомендую почитать статью James Burke On Inventing JS Module Formats and Script Loaders.

Далее мы рассмотрим примеры написания общеприменимых модулей, работающих на паттерне AMD, и других форматах, иногда более мощных. Но сначала давайте разберемся с форматом модулей CommonJS.

CommonJS

Если это слово вам не знакомо — CommonJS является группой людей, разрабатывающих и стандартизирующих API Javascript-a. На сегодня уже сделаны попытки утвердить стандарты для модулей и пакетов. Предлагаемый стандарт модулей CommonJS описывает API для серверных модулей, но, как в свое время верно заметил Джон Ханн – на практике существует всего два способа использования модулей в браузере – это или декорировать их, или же – декорировать. Это значит, что модули можно обернуть или в самом браузере (что может быть довольно ресурсоемко), или оборачивать их на этапе билда (это, разумеется, быстрее – но требует процесса билда).

Некоторые разработчики твердо уверены, что спецификация CommonJS применима только к серверным скриптам, что является одной из причин разногласия в выборе формата модулей в пре-релизе Harmony.Одним из ключевых аргументов против CommonJS является то, что многие API CommonJS реализуют серверную логику, которая просто невозможна на клиентском Javascript-e. К примеру io>, system и js можно рассматривать, как нереализуемые на клиенте, из-за природы их функциональности.

Тем не менее, учитывая все плюсы организации CommonJS, не следует недооценивать ее возможности в разработке универсальных модулей. Модулей, используемых как на клиенте, так и на сервере, таких как валидация, конвертация, шаблонизация. Зачастую разработчики выбирают CommonJS для серверных, и AMD для клиентских модулей.

Это оправдано, т.к. AMD позволяет использовать плагины, с более детальной реализацией конструкторов и функций. Получение одних только конструкторов из модулей CommonJS может обернуться довольно утомительным занятием.

Если смотреть на архитектуру, CommonJS реализует куски Javascript-a, которые экспортят объекты, предоставляя их многоразовое использование в зависимых от них коде. И обычно в них нет функциональных слоев. Теория создания модулей CommonJS покрыта обширными материалами, но по сути, все что мы имеем – это переменная exports, которая предоставляет наш объект для других модулей, и функцию require, которая позволяет принимать объекты из других модулей.


// простой модуль 'foobar'
function foobar(){
        this.foo = function(){
                console.log('Hello foo');
        }

        this.bar = function(){
                console.log('Hello bar');
        }
}

exports.foobar = foobar;

// Приложение, использующее наш модуль 'foobar'

var foobar = require('./foobar').foobar,
    test   = new foobar.foo();

test.bar(); // 'Hello bar'

Существует много разных библиотек, реализующих подгрузку модулей в форматах AMD и CommonJS, но я предпочитаю RequireJS (curl.js тоже достойный). Их описание выходит за рамки данной статьи, но я рекомендую прочитать статью John Hann-а curl.js: Yet Another AMD Loader, и дополнительно статью James Burke LABjs and RequireJS: Loading JavaScript Resources the Fun Way.

После всего вышесказанного, будет интересно применить теорию AMD и CommonJS для написания плагинов, которые могут существовать как на сервере, так и на клиенте. Наша работа над плагинами и виджетами AMD и UMD (Universal Module Definition) все еще продолжается, но мы надеемся создать решение, покрывающее все эти аспекты.

Один из таких паттернов, над которым мы работаем, представлен ниже. Он предлагает следующие возможности:
  • Ядро плагина объявляется в своем неймспейсе $.core, которое можно позже расширить, используя неймспейсовый паттерн. Плагины, которые грузятся через теги script, автоматически создают свой неймспейс plugin в неймспейсе core ($.core.plugin.methodName()).
  • Паттерн очень удобен в использовании, т.к. позволяет расширениям легко обращаться к методам и свойствам ядра, или, если чуть его изменить – сможет даже изменять поведения ядра, предоставляя более гибкое решение.
  • Можно не использовать никаких подгрузчиков для полноценной работы паттерна

usage.html

<script type="text/javascript" src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
<script type="text/javascript" src="pluginCore.js"></script>
<script type="text/javascript" src="pluginExtension.js"></script>

<script type="text/javascript">

$(function(){

    // ядро объявлено в пространстве core,
    // которое мы сперва кешируем
    var core = $.core;

    // затем вызовем встроенную функцию,
    // и покрасим все дивы желтым
    core.highlightAll();

    // вызовем функцию плагина ядра,
    // который подгружен в пространство plugin:

    // покрасим бэкграунд первого дива зеленым.
    core.plugin.setGreen("div:first");
    // здесь используется метод ядра 'highlight'
    // из самого плагина

    // красим последний див в цвет свойства 'errorColor',
    // которое определено в ядре
    core.plugin.setRed('div:last');
});

</script>

pluginCore.js

// Ядро модуля
// замечание: обертка вокруг кода ядра позволяет нам работать с разными форматами
// и спецификациями, предоставляя аргументы в требуемом виде.
// код реализации ядра находится ниже обертки, в нем представлены
// примеры организации свойств и экспорта модуля
// Обратите внимание, что зависимости модуля могут быть с лёгкостью объявлены
// при необходимости, и будут работать, как в предыдущих примерах AMD реализаций.

(function ( name, definition ){
  var theModule = definition(),
      // данный подход считается "безопасным":
      hasDefine = typeof define === 'function' && define.amd,
      // hasDefine = typeof define === 'function',
      hasExports = typeof module !== 'undefined' && module.exports;

  if ( hasDefine ){ // модуль AMD
    define(theModule);
  } else if ( hasExports ) { // модуль Node.js
    module.exports = theModule;
  } else { // Объявляем модуль в нашем пространстве, или же в глобальном (window)
    (this.jQuery || this.ender || this.$ || this)[name] = theModule;
  }
})( 'core', function () {
    var module = this;
    module.plugins = [];
    module.highlightColor = "yellow";
    module.errorColor = "red";

  // далее идет реализация логики модуля, и экспорт его API

  // этот метод используется ядровым методом highlightAll()
  // и другими расширениями ядра, раскрашивая наши элементы
  // разными цветами
  module.highlight = function(el,strColor){
    if(this.jQuery){
      jQuery(el).css('background', strColor);
    }
  }
  return {
      highlightAll:function(){
        module.highlight('div', module.highlightColor);
      }
  };

});

pluginExtension.js

// Расширение ядра

(function ( name, definition ) {
    var theModule = definition(),
        hasDefine = typeof define === 'function',
        hasExports = typeof module !== 'undefined' && module.exports;

    if ( hasDefine ) { // модуль AMD
        define(theModule);
    } else if ( hasExports ) { // модуль Node.js
        module.exports = theModule;
    } else { // Объявляем модуль в нашем пространстве, или же в глобальном (window)

        // распихиваем расширения по нужным неймспейсам
        var obj = null;
        var namespaces = name.split(".");
        var scope = (this.jQuery || this.ender || this.$ || this);
        for (var i = 0; i < namespaces.length; i++) {
            var packageName = namespaces[i];
            if (obj && i == namespaces.length - 1) {
                obj[packageName] = theModule;
            } else if (typeof scope[packageName] === "undefined") {
                scope[packageName] = {};
            }
            obj = scope[packageName];
        }

    }
})('core.plugin', function () {

    // Здесь опишем логику модуля, и вернем его API.
    // Данный код может с легкостью пересекаться с кодом ядра, расширяя
    // и переопределяя его методы при необходимости
    return {
        setGreen: function ( el ) {
            highlight(el, 'green');
        },
        setRed: function ( el ) {
            highlight(el, errorColor);
        }
    };

});

Хоть это и не тема данной статьи, вы наверное заметили упоминание разных методов require в описании AMD и CommonJS.

Это, естественно, вызывает некую путаницу, и в данный момент сообщество не уверено в достоинствах глобального метода require. Джон Ханн предлагает вместо одноименного метода (который не дает четкого представления, с каким методом мы работаем), называть глобальный require другим, более понятным именем (к примеру, названием библиотеки). Именно поэтому, curl.js использует curl, а RequireJS – requirejs.

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

Стоит почитать:

Что же делает плагин jQuery хорошим?


Как видите, паттерны все лишь один из аспектов разработки плагинов. И в качестве эпилога, ниже я опишу свои критерии определения качества плагина. Возможно они помогут вам в разработке.

Качество

Старайтесь придерживаться передовых подходов в разработке приложений на Javascript-е и jQuery. Всегда спрашивайте себя, являются ли ваши решения оптимальными? Соблюдают ли они гайдлайны написания кода jQuery? Если нет, то можно ли хотя бы назвать ваш код относительно чистым и понятным?

Совместимость

С какими версиями jQuery совместим ваш код? Проверяли ли вы его на последних билдах библиотеки? Если плагины были написаны до версии 1.6, у вас могут быть проблемы с атрибутами, потому что в этом релизе была изменена логика обращения к ним. Новые релизы jQuery предлагают новые улучшения и возможности, увеличивая производительность вашего проекта. Но они же могут быть и причинами случайных несоответствий и ошибок, т.к. код не стоит на месте. Я бы хотел, чтобы авторы плагинов поддерживали свой код с актуальными версиями jQuery, или, как минимум, проводили тестирование на работоспособность.

Надежность

Ваш плагин должен поставляться со своим набором тестов. Они являются не просто доказательством исправности вашего кода, но также способствуют возможности улучшения плагина без последствий для конечных пользователей. Я считаю, что юнит-тесты являются обязательными для любого серьезного плагина, предполагаемого для продакшена, тем более, что их не так сложно реализовать. Jorn Zaefferer написал хорошую инструкцию по автоматическому тестированию Javascript-a средствами QUnit.

Производительность

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

Документация

Если ваш код предполагает использование другими разработчиками, постарайтесь обеспечить его хорошей документацией. Опишите ваш API. Какие методы и параметры реализует плагин? Есть ли в нем моменты, которые нужно учитывать при использовании? Если люди не смогут понять ваш плагин, они, вероятнее всего, поищут альтернативу. Уделайте должное внимание комментариям самого кода — это будет настоящим подарком для остальных разработчиков. Если человек с легкостью может разобраться в вашем коде, и без проблем сможет его расширять – значит ваш код замечателен.

Возможность сопровождения

Предоставляя плагин, определитесь, как долго вы сможете поддерживать ваш код. Всем нравится выкладывать код на всеобщее обозрение, но не все готовы отвечать на вопросы, решать проблемы и улучшать этот код дальше. Обычно достаточно четко описать это в файле README, предлагая пользователям самим управлять вашим кодом.

Заключение


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

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

Ну вот и все. Если у вас есть примеры паттернов, не рассмотренных в этой статье, я думаю, все буду благодарны, если вы выложите его куда либо на гитхаб, и поделитесь им в комментариях.
До следующих встреч, и приятного кодинга!

Отдельная благодарность John Hann, Julian Aubourg, Andree Hanson и всех остальным, за их советы и комментарии.

Оригинал: Essential jQuery Plugin Patterns, Addy Osmani
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.