0,4
рейтинг
26 мая 2011 в 23:25

Разработка → jQuery UI как инфраструктура для плагинов

Введение


jQuery UI больше всего известен как набор готовых виджетов. Главное их преимущество, на мой взгляд, — консистентное API: каждый виджет управляется одинаково. Второе их преимущество — они хранят свое состояние: если повторно навесить виджет на элемент, то результатом будет уже существующий инстанс виджета.
Но jQuery UI — это не только набор окошечек и табов (далеко не всеми любимых). Это еще целая инфраструктура для создания своих виджетов: с удобным консистентным API, с хранением состояния и с возможностью наследования. Как ни странно, это для многих новость, в результате чего и появилась эта статья — так же, как это было новостью для меня всего несколько месяцев назад.


Лирическое отступление


Дабы не тратить время и место, везде ниже по коду подразумевается, что window.$ == window.jQuery, undefined никто не испортил, и что мы подключаем jQuery, только jQuery и ничего, кроме jQuery, и что все объявления обернуты в нечто вроде этого:
(function($) {
  // наш код
})(jQuery)

* This source code was highlighted with Source Code Highlighter.

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

Магия $.widget


Вся магия заключается в методе $.widget. Он принимает 2 (или 3 — в случае наследования) параметра. Официально этот метод называется «фабрика виджетов».
Первый параметр — строка, она содержит неймспейс и собственно имя виджета, разделенные точкой. Например, "my.myWidget". Неймспейс обязателен; вложенность не поддерживается. Второй параметр — литерал объекта, который, собственно, и описывает наш виджет:
$.widget("my.myWidget", {
  options: {
    greetings: "Hello"
  },
_create: function() {
    this.element.html(this.options.greetings);
  }
})

* This source code was highlighted with Source Code Highlighter.

Функция, лежащая в поле под именем _create, служит конструктором, и будет вызвана при создании инстанса виджета; на этот инстанс и указывает this.
this.element — это элемент, на который был навешен виджет. Это всегда одиночный элемент, а не коллекция (как в случае обычных плагинов); если навешивать виджет на jQuery-объект, который содержит больше одного элемента, то будет создано столько инстансов, сколько элементов.
В поле options хранятся дефолтные настройки виджета. Это поле наследуется, так что оно всегда будет в виджете, даже если не объявлять его явно.
Если при вызове виджета передать объект, то переданный объект будет «смерджен» (с помощью метода $.merge) с дефолтными настройками еще до вызова _create.
За работу с настройками отвечает метод setOption:
$.widget("my.myWidget", {
  options: {
    greetings: "Hello"
  },
  _create: function() {
    this._render();
  },
  _render: function() {
    this.element.html(this.options.greetings);
  },
  setOption: function(key, value) {
    if (value != undefined) {
      this.options[key] = value;
      this._render();
      return this;
    }
    else {
      return this.options[key];
    }
  }
})

* This source code was highlighted with Source Code Highlighter.

Используется это так же, как в любом стандартном виджете:
var mw = $('.mywidget').myWidget({greeting: 'Hi there!'})
console.log(mw.myWidget('option', 'greeting')); // 'Hi there!'
mw.myWidget('option', 'greeting', 'O HAI CAN I HAZ CHEEZBURGER?');

* This source code was highlighted with Source Code Highlighter.


Приватные и публичные методы


К методу виджета можно обратиться примерно так же, как мы обращаемся к настройкам:
$.widget("my.myWidget", {
    options: {
        greetings: "Hello"
    },
    _create: function() {
        this._render();
    },
    _render: function() {
        this.element.html(this.options.greetings);
    },
    sayHello: function(saying) {
        alert(saying);
    },
    _setOption: function(key, value) {
        if (arguments.length == 1) {
            this.options[key] = value;
            this._render();
            return this;
        }
        else {
            return this.options[key];
        }
    }
})
// …
mw.myWidget("sayHello", 42);

* This source code was highlighted with Source Code Highlighter.

Но для этого этот метод должен быть публичным. Как сделать метод публичным в парадигме UI-ных плагинов? Это просто: публичными методами движок виджетов считает те, имена которых не начинаются с подчеркивания. Все остальные методы — приватные. Поля виджетов, не являющиеся функциями, всегда только приватные.
Это, конечно, не в полном смысле public и private методы, а их эмуляция, впрочем, достаточная для того, чтобы разграничить доступ.

Коллбэки


По сути, это просто шорткаты для привязки к пользовательским событиям внутри виджета. В виджет они передаются так же, как и настройки.
$.widget("my.myWidget", {
  options: {
    greetings: "Hello"
  },
  _create: function() {
    this._render();
  },
  _render: function() {
    this.element.html(this.options.greetings);
    this._trigger("onAfterRender", null, {theAnswer: 42})
  }
})
// …
var mw = $(".mywidget").myWidget(
  {
    greeting: "Hi there!",
    onAfterRender: function(evt, data) {
      console.log(data.theAnswer)
  }
})


* This source code was highlighted with Source Code Highlighter.

Это эквивалентно старому доброму .bind в таком виде:
mw.bind('onAfterRender.myWidget', function(evt, data) {console.log(data.theAnswer)})
* This source code was highlighted with Source Code Highlighter.


Деструкторы


Идущие «из коробки» виджеты имеют обыкновение генерировать кучу разметки. Хорошо это или плохо — вопрос дискусионный. Но, отчасти поэтому, отчасти потому, что ссылка на инстанс виджета записывается в expando-атрибут DOM-элемента — надо вызывать деструктор, когда вы уничтожаете виджет.
В качестве деструктора вызывается метод под названием destroy. К сожалению, его всегда надо вызывать явно. Чтобы было полное счастье, внутри деструктора должен быть следующий вызов:
$.Widget.prototype.destroy.call(this);
* This source code was highlighted with Source Code Highlighter.


Наследование


Одна из самых вкусных вещей, хотя по ней практически нет информации.
Если вторым аргументом передать какой-то другой виджет A (наш виджет в этом случае идет третьим аргументом), новый виджет B будет его потомком.
Предположим, в нашем приложении — куча диалоговых окон, и все — модальные. При этом они не должны закрываться по Esc. Писать каждый раз вот такое совсем не хочется:
$('.dialog').dialog({
  modal: true,
  closeOnEscape: false,
  // … еще куча настроек, которые тоже могут быть
  // одинаковы для всех диалогов…
})

* This source code was highlighted with Source Code Highlighter.


Мы можем отнаследоваться от стандартного диалога и переопределить дефолтные настройки:
$.widget("my.mydlg", $.ui.dialog, {
  options: {
      modal: true,
     closeOnEscape: false,
  },
  _create: function() {
      $.ui.dialog.prototype._create.call(this);
  }
})

* This source code was highlighted with Source Code Highlighter.

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

Заключение


Мне кажется, что UI-ные виджеты — это неплохое средство модуляризации кода. В небольших и средних проектах они сами себе могут обеспечить достаточную инфраструктуру приложения.
При этом не надо тащить весь, довольно увесистый, jQueryUI — достаточно компонента core.
Паттерн, который лежит в основе этого виджетного движка, создатели называют bridge (хотя, конечно, метод $.widget — это фабрика). Допилив метод $.widget напильником, можно получить виджеты, которые сами читают свои настройки из разметки, сами находят витальные для себя элементы, и автоматически организовываются в иерархическую структуру. Но это явно тема для отдельной статьи.
Константин Китманов @k12th
карма
86,2
рейтинг 0,4
JS

Похожие публикации

Самое читаемое Разработка

Комментарии (51)

  • 0
    Шикарная статья, спасибо, особенно понравилось «наследование» :)
  • –1
    Плохая статья, слишком маленькая. Пишите еще, и побольше.
    • 0
      Мне показалось, что вдаваться в дальнейшие подробности будет уже суесловием.
      • +1
        Я имел ввиду, что статей по Jquery UI побольше хочется видеть :-)
        • +2
          Собственно, про то, как это устроено, я рассказал:) Можно, конечно, написать какой-нибудь виджет и подробно его разжевать… Писать про «коробочные» — неинтересно, есть документация с демо и кодом.
          • +1
            По коробочным действительно все ясно, а вот написать свой и рассказать вообще про какие-нибудь интересные кейсы использования — я бы почитал) но это так… мнение одного человека.
            • +2
              Про интересный кейс — будет, см. секцию «Заключение»;)
              Написание своего тоже разжую, есть идея.
      • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Имхо mootools лучше плюшку для этого реализовала, создание класса более наглядное, как и наследование, в jq с этим проблема. UI мне не особо нравится касательно кода. Обычно я юзаю jquery tools там реализация более грамотная и гибкая. Если нужен дополнительный функционал или сам пишу или ищу адекватный плагин.
    А переопределение функций — сомнительное решение
    • 0
      Переопределение?
    • +1
      Статья не про mootols. И даже не про создание классов.

      Существует много проектов, написанных на jQuery + UI, с этим приходится считаться и использовать то, что есть.
      • 0
        ну да там пара функций стандартный jq переопределяются. Статья не о мутулс, я высказывал своё мнение, что если сравнивать создание классов и наследование то мутулс лучше. виджет — это класс, разве создавая виджет вы не класс создаете?
        давно не использую ui как основу, разве что datepiker и так по мелочи если функционал 1 в 1. Проектов то много но инструменты то надо использовать с умом! Я же не критиковал вашу статью, что она не правильная, просто предложил на мой взгляд более удобные альтернативы.

        Главный плюс что структура ui заставляет вас хоть как то использовать грамотно структурированный код. А не тупо куча кала в jq функционале )) к слову щас работаю с проектом, там вторую неделю хочу сломать нос создателю fancybox за криворукость, плагин из разряда я вообще прогить не умею поэтому подключу чтоб картинки показывал, вот верстальщик с ним наверстал кучу попапов, а теперь связать их — гемморой
        • 0
          Плагинов к jQuery много кривых, это да.
  • 0
    Статья хорошая, объясняет не совсем нормальную доку по написанию виджета на сайте jQueryUI.

    По поводу наследования, на сколько я помню, там оно очень бедненькое и методы не наследует в классическом смысле, а делает тупо $.extend, что не дает достучаться до методов предка. (Во всяком случае я способа не нашел)
    • 0
      Ох, напутал я там в коде, прощу прощения. Поправил секцию про наследование.

      Там да, $.extend, но не самого наследуемого виджета, а его prototype. Так что достучаться можно.
  • 0
    Да, делал в одном из своих проектов свои виджеты. Весьма удобно. В частности я делал простую таблицу, а потом расширял ее до таблицы с сортировкой.
  • 0
    Помнится Илья Кантор проводил различие между плагинами и виджетами, я с ним в принципе согласен. Виджеты это блок, а плагин это функция. Соответственно плагины делаем на основе jQuery, виджеты на основе jQuery UI.
    Это так, к слову пришлось=)
    • 0
      Я согласен. Но про плагины уж не стал писать, избитая тема, имхо:)
      • 0
        Это да, я про заголовок=)
        • 0
          Ах да, действительно.
          Объясню почему. Во-первых, навешиваются они так же, как плагины. Во-вторых, граница размыта — «большой плагин или маленький виджет?». В-третьих, не хотелось начинать с грозного «виджеты» в заголовке — народ обычно думает об этих штуках, как о плагинах. В-четвертых, определение все-таки не общепринятое.
          В следующей статье обязательно расскажу, в чем разница:)
    • 0
      это он о логическом скорее разделении. Все равно с програмной точки зрения плагин и виджет совершенно одно и тоже ибо к jq крепятся одинаково. просто существует плюшка $.widget котрая помогает структурировать код и делает некую часть работы за вас
  • 0
    Было бы не плохо, показать пример того, зачем все это нужно.

    Например, написать свой простой полезный виджет на jQuery UI.
    • 0
      Затрудняюсь объяснить, зачем это нужно. Зачем вообще модульность в коде?:)

      Напишу, есть планы.
      • +1
        Вот я не мега программист, мне пока не понятно. Без подколов, серьезно.

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

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

        Напишите пожалуйста подробнее, на примере. Типа, стандартным путем тут 10 строк кода, а если вот так — то 5 :-)
        • +1
          Прочтите «Совершенный код» Макконела. Вкратце, модульность дает несколько вещей.
          Во-первых, возможность сосредоточиться на одной задаче за раз.
          Во-вторых, можно распаллалелить задачи.
          В-третьих, она упрощает поддержку — проще локализовать баг, проще расширять функциональность.

          Вы используете виджеты из комплекта — прекрасно, в секции «Наследование» вы найдете то, что может пригодиться.
          Я не призываю прямо щас сесть и все переписать на виджеты. Но лучше знать, что такая возможность есть, не правда ли?
          • 0
            Опять общие фразы.
            Я же просил на примере показать. А в ответ — прочти книгу и все поймешь. Окей, наверное, мне пока рано в это лезть :-)
            • 0
              Ну потому что это действительно общая вещь. Это просто хорошая практика. Это не артефакт, который дает +10 INT + 20 WIS:)
              • 0
                Поясню на примере.
                Почему Объектно ориентированное программирование — хорошо?
                Потому что, ты можешь написать одну функцию, которая будет выполнять какое-то действие, и вызывать ее в любом месте своей программы. К примеру, функция форматирования времени:

                  function timeFormat($time) {
                    return strftime('%d.%m.%Y', strtotime($time));
                  }
                

                Теперь, когда нужно вывести в привычном виде дату, можно просто выполнить
                echo $this->timeFormat('2011-05-27');
                

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

                А еще, можно задать переменную для формата даты, и просто меняя ее получать другой результат.

                  var $time_format = '%d.%m.%Y'; // Это шаблон для даты, можно поменять когда угодно
                
                  function timeFormat($time) {
                    return strftime($this->time_format, strtotime($time));
                  }
                


                Вот в таком виде лично мне бы хотелось увидеть топик про модульность.
                • +1
                  Вы немного перепутали ООП с функциональным программированием. Но автор, думаю, поймет аналогию)
                  • 0
                    ФП тут нет, вторая функция не является чистой:) ООП тут тоже не густо:)
                    • 0
                      Ну да, я видел "$this". Просто немного удивило это: «Почему Объектно ориентированное программирование — хорошо?
                      Потому что, ты можешь написать одну функцию, которая будет выполнять какое-то действие, и вызывать ее в любом месте своей программы.» Но наверно просто не выспался и придираюсь)
                • 0
                  Ну ей богу. ООП — это один частных случаев модульности. Функции — тоже.
      • 0
        Я согласен, что пример «на пальцах», например как во всеобщеизвестной доке в двух частях по написанию виджета (где также рассказывается почему нужно использовать виджеты), было бы ОЧЕНЬ хорошо иметь. Как раз для продолжения подоходит. ;)
  • 0
    спасибо большое, очень не хватает статей на русском по jQuery UI

    с недавних пор успешно используем UI как стандарт кодирования в наших проектах, всё очень нравится, но вот о том, что на каждый элемент в коллекции (к которой применяется виджет) создается по отдельному экземпляру — не задумывался. теперь надо будет переписать старый код некоторый :)
  • +1
    В целом статья хорошая как вводная, но в ней есть ряд ошибок:

    1) В разделе «Магия $.widget» про options:
    Если при вызове виджета передать объект, то переданный объект будет «смерджен» (с помощью метода $.merge) с дефолтными настройками еще до вызова _create.

    На самом деле используется $.extend(), ну и тогда уж объект будет «расширен».

    2) Далее там же:
    За работу с настройками отвечает метод setOption

    Если это метод именно этого «нового» виджета — то вопросов нет, но если говорится о базовом методе (судя по примеру вызова) — то это не так. В виджетах есть public метод options( key, [ value ] ) и private методы _setOption, _setOptions.

    И небольшое замечание по поводу раздела «Коллбэки»:
    1) Если вызывается .bind() на элементе, то тип события надо указывать с префиксом в виде имени виджета, т.е. для примера в тексте должно быть: mw.bind('mywidgetonafterrender', function(evt, data) {console.log(data.theAnswer)});
    2) В методе _trigger() тип события (строковый) всегда приводится к нижнему регистру, поэтому и в .bind() тип события должен быть в нижнем регистре полностью, но при этом для обработчика в опциях регистр учитывается — поэтому лучше для единообразия всё указывать в нижнем регистре.
    • 0
      Ещё забыл про раздел «Наследование»:
      К сожалению, приходится явно указывать предка и вручную вызывать его конструктор.

      Ну без указания предка никуда, а вот если _create() пустой, то и объявлять его не надо и конструктор вручную вызывать тогда не придётся.
      • 0
        Ну, это-то понятно, что если конструктор тривиальный, то зачем его вызывать:)
        • 0
          Тут дело не в вызове, а в том, что если в вашем прототипе (описании виджета) не будет метода _create — то останется метод базового виджета. Т.о. если в вашем конструкторе нет ничего, кроме вызова конструктора базового виджета, то и писать его не надо, в таком случае останется конструктор базового виджета. Хотя и конструктором-то его можно назвать с большой натяжкой — это просто метод, который вызывается при создании инстанса виджета.
          • 0
            А, вот оно что… Спасибо, это я как-то не сообразил.
    • 0
      1) Да, конечно. У меня какое-то помрачение было:(

      2) Странно, у меня было другое ощущение… да и setOption вроде бы вполне работает.
      Я проверю и поправлю статью соответствующим образом.

      3) Вот это я не учел, спасибо.

      С вашего позволения, я внесу исправления в соответствующих местах, и упомяну вас в пост-скриптуме с ссылкой на этот комментарий. Не возражаете?
      • 0
        2) Да, ваш метод работает как метод, он же написан, почему бы ему не работать, я про то, что базового такого метода нет. Т.е. если убрать именно ваш метод, то работать будет нечему, а в пояснительном тексте говорится о базовом методе — возможно это просто неясно изложено было.

        Поправить, конечно, надо, на то и существует режим редактирования :) А уж упоминать ли мои комментарии в пост-скриптуме — это ваше право, комментарии всё равно никуда не денутся и редактировать их нельзя, но мне будет приятно.
        • 0
          Я имел ввиду, что если имплементировать метод setOption, то он будет отзываться на вызов .mywidget('option', …).
          • 0
            Сомнительный факт для меня. Вот если метод "_setOption" — то соглашусь, что он будет вызываться, но при этом надо учитывать, что метод базового виджета тоже не просто так и его либо надо вызывать, либо умышленно и осознанно не вызывать, отвечая за последствия.
            • 0
              Расскажите поподробнее, пожалуйста, какой механизм работает, когда мы делаем .mywidget('option', …)?
              • 0
                Вызывается метод виджета .option(), который в свою очередь если требуется чтение — то возвращает значение нужной опции (или всех), а если требуется запись — то вызывает метод _setOptions(), а тот в свою очередь вызывает уже _setOption().
                Детали можно посмотреть самому — там всё просто: github.com/jquery/jquery-ui/blob/1-8-stable/ui/jquery.ui.widget.js#L186
                • 0
                  Все, я понял вас, спасибо.

                  А как бы вы доопределили поведение виджета в ответ на изменение того или иного ключа, не трогая унаследованные функции?
                  • 0
                    Если речь про опции, то
                    1) не трогая можно сделать метод(ы), который будет что-то делать и менять опцию, а в описании опции тогда надо писать, что напрямую её менять не рекомендуется
                    2) либо (правильнее на мой взгляд) переопределить метод _setOption() и внутри нового метода обработать свой специфичный ключ опции и/или вызвать метод базового виджета.
                    • 0
                      Ок, так и запишем:)
  • 0
    И отдельно хочу заметить про заключение:
    Допилив метод $.widget напильником, можно получить виджеты, которые сами читают свои настройки из разметки, сами находят витальные для себя элементы, и автоматически организовываются в иерархическую структуру. Но это явно тема для отдельной статьи.

    Виджеты и так умеют читать свои настройки из разметки при наличии и соответствующей настройке плагина $.metadata.
    Если под «допилить напильником» имеется в виду «взять исходник и поправить его» — то, на мой взгляд, это плохая идея. Потому что того, что есть сейчас вполне хватает для тех задач, которые встают перед виджетами jQuery UI. А вот если автор хочет написать свою фабрику виджетов (вполне можно взять за отправную точку идеи и часть кода из существующей фабрики) — то это благородная цель, но важно помнить, что у этой фабрики будут свои цели, задачи и область применения, и они должны быть обоснованными. Т.е. я хочу сказать, что метод, который используется в 1-ом виджете из 10, не нужен в базовом классе, он просто будет загромождать код.
    • 0
      Ну, то, что я сделал в своем pet-project, на данный момент больше похоже на monkey-patсhing, это да. Но просто потому что поправить нужно было буквально только в двух местах; переписывать из-за этого весь код заново не хотелось. Связано это с тем, как работает фабрика виджетов с опциями и всем остальным.

      Кроме этого, мне хотелось сделать что-то все-таки цельное — уйти от концепции библиотеки методов, на каковую так ориентирована jQuery, и приблизиться к неким зачаткам фреймворка для построения приложений. При этом мне хочется описывать мои виджеты декларативно — отсюда желание автоматизации чтения настроек и т.д.
  • 0
    А почему статья не в блог jQuery?

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