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


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

(function ($) {
    $.fn.tooltips = function (opt) {
        var options = $.extend(true, {
            position: 'top'
        }, opt);
        return this.each(function () {
            $(this).after('<div class="xd_tooltips ' + options.position + '">' + $(this).data('title') + '</div>');
        });
    };
}(jQuery));

Это весь функционал нашего плагина. Он добавляет после каждого элемента выборки, div.xd_tooltips новый элемент. Это все, что он делает.

Все операции надо делать в отдельном классе


Задумав серьезный плагин, который будет расти, не нужно всю логику засовывать в анонимную функцию в метод each, как в примере. Создайте отдельную функцию конструктор (класс). Затем для каждого элемента из выборки, создавайте отдельный экземпляр этого класса, и сохраняйте ссылку на него в data исходного элемента. При повторном вызове плагина проверяем data. Это решает проблему повторной инициализации.

(function ($) {
    var Tooltips = function (elm) {
        $(elm).after('<div class="xd_tooltips">' + $(elm).data('title') + '</div>');
    };
    $.fn.tooltips = function (opt) {
        var options = $.extend(true, {
            position: 'top'
        }, opt);
        return this.each(function () {
            if (!$(this).data('tooltips')) {
                $(this).data('tooltips', new Tooltips(this, options));
            }
        });
    };
}(jQuery));

Все общие операции hide,show,destroy делайте методами класса


Когда плагин будет большим, и вы захотите использовать часть его функционала в другом плагине/коде, то будет очень удобно если у класса будут доступны открытые методы. Методы можно делать через прототипы, однако тут есть минус — нельзя использовать приватные переменные. Лучшим вариантом будет использование локальных функций, которые затем вернутся в хеше.

Примерно так:
// лишний код, и нужно следить за this
var PluginName = function () {
    this.init = function () {
        // инициализация 
    };
};
// нормально, но нет локальных приватных переменных 
// и также есть проблема с this
var PluginName = function () {};
PluginName.prototype.init = function () {
    // инициализация 
};
// идеально
var PluginName = function () {
    var self,
        init = function () {
            // инициализация 
        };
    self = {
        init: init
    };
    return self;
};

Почему третий вариант лучший?
Во первых — теперь методы внутри класса, можно использовать без префикса this. this — в неумелых руках это зло. В JS он может быть чем угодно, и очень рекомендую использовать его как можно реже.

Во вторых, когда плагин будет большим, вы решите пропустить его через uglify. Тогда в последнем варианте, все упоминания init (кроме ключа хеша self.init) будут заменены на однобуквенную переменную, которая при частом использовании метода, прилично уменьшит код.

В третьих, вам доступны приватные методы и переменные, которые вы объявите в верхнем var. Это очень удобно. Что-то вроде private методов во «взрослых» ЯП.

Из последнего правила нужно выжать еще одно:

Все инициализацию объекта также надо держать в отдельном методе init


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

Тогда:

var tooltips = new Tooltips();
tooltips.parse(somedata);

Не будет ничего создавать в дереве DOM, а сделает ровно то, что нужно.

Делайте для каждого экземпляра класса отдельный экземпляр настроек


В первом примере мы сделали одну глобальную options переменную. А в scope jQuery могло попасть несколько объектов. Чувствуете чем это грозит? JS так работает, что если один из элементов, потом будет менять эти опции, то они изменятся у всех элементов. Это не всегда верно. Поэтому options подаем вторым элементом в конструктор нашего класса.

Из всего вышеизложенного, плагин будет выглядеть так:

(function ($) {
    var Tooltips = function (elm, options) {
        var self,
            init = function () {
                $(elm).after('<div class="xd_tooltips ' + options.position + '">' + $(elm).data('title') + '</div>');
            };
        self = {
            init: init
        };
        return self;
    };
    $.fn.tooltips = function (opt) {
        return this.each(function () {
            var tooltip;
            if (!$(this).data('tooltips')) {
                tooltip = new Tooltips(this, $.extend(true, {
                    position: 'top'
                }, opt));
                tooltip.init();
                $(this).data('tooltips', tooltip);
            }
        });
    };
}(jQuery));

Настройки по умолчанию должны быть видны глобально


В примере выше, мы соединяем настройки opt и настройки по умолчанию в виде хеша {}. Это не верно — всегда найдется пользователь, которому такие настройки не подойдут, а вызывать каждый раз плагин со своими настройками будет накладно. Конечно, он может залезть в код плагина и исправить настройки на свои,  но тогда он потеряет обновление плагина. Поэтому настройки по умолчанию должны быть видны глобально, и их можно было изменить также глобально.

Для нашего плагина можно использовать это так:

$.fn.tooltips = function (opt) {
    return this.each(function () {
        var tooltip;
        if (!$(this).data('tooltips')) {
            tooltip = new Tooltips(this, $.fn.tooltips.defaultOptions, opt);
            tooltip.init();
            $(this).data('tooltips', tooltip);
        }
    });
};
$.fn.tooltips.defaultOptions = {
    position: 'top'
};

Тогда любой разработчик сможет у себя в скрипте объявить:

$.fn.tooltips.defaultOptions.position = 'left';

А не делать это при каждой инициализации плагина.

Не всегда возвращайте this


Во всех примерах выше, код инициализации сводился к тому, что мы сразу же возвращали this.each. На самом деле почти все методы (плагины) jQuery возвращают this.

Поэтому возможны такие вещи как:

$('.tooltips')
    .css('position', 'absolute')
    .show()
    .append('<div>1</div>');

Это очень удобно. Но к примеру метод val, если нет параметров, возвращает значение элемента. Так и у нас, надо предусмотреть способ, по которому, наш плагин мог бы возвращать какие-то значения.

$.fn.tooltips = function (opt, opt2) {
    var result = this;
    this.each(function () {
        var tooltip;
        if (!$(this).data('tooltips')) {
            tooltip = new Tooltips(this, $.fn.tooltips.defaultOptions, opt);
            tooltip.init();
            $(this).data('tooltips', tooltip);
        } else {
            tooltip = $(this).data('tooltips');
        }
        if ($.type(opt) === 'string' && tooltip[opt] !== undefined && $.isFunction(tooltip[opt])) {
            result = tooltip[opt](opt2);
        }
    });
    return result;
};

Обратите внимание, мы добавили второй параметр opt2. Он нам будет нужен именно для случаев вызова каких-то методов.
К примеру, для плагинов ввода/вывода актуально изменить исходное value.

$('input[type=date]').datetimepicker();
$('input[type=date]').datetimepicker('setValue', '12.02.2016');
console.log($('input[type=date]').datetimepicker('getValue'));

Используйте 'use strict'


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

(function ($) {
	'use strict';
	//...
} (jQuery));

Кто-то скажет, что можно объявить 'use strict' в самом начале файла, но я не рекомендую этого делать. Когда проект вырастет, и его будут использовать другие плагины. А создатели тех плагинов не использовали 'use script'. Когда grunt/gulp/npm соберет все пакеты в один build файл, то вас будет ждать неприятный сюрприз.

Очень рекомендую еще JSLint для валидации кода. Я много лет использую notepad++ для разработки, и в нем нет подсветки ошибок. Может это не актуально для IDE, но мне JSLint позволяет писать более качественный код.

Используйте свой внутренний reset CSS


В начале своего CSS файла, обнуляйте весь CSS для всего плагина. Т.е. нужен какой-то главный класс, под которым будут все остальные. При использовании Less, это придаст дополнительную читабельность:

.jodit {
  font-family: Helvetica, Arial, Verdana, Tahoma, sans-serif;
  &, & *{
    box-sizing: border-box;
    padding:0;
    margin: 0;
  }
}

Используйте префиксы в названиях CSS классов


CSS в браузерах вещь глобальная. А предугадать в каком окружении будет ваш плагин, невозможно. Поэтому все, даже малозначительные классы, пишите с префиксом. Я очень часто видел, как вспомогательные классы disable,active,hover, полностью ломают внешний вид плагина.

Используйте современные способы сборки проекта


В комментариях верно подсказали, что ни один современный JS проект уже не живет без обновлений, автоматических сборщиков, и отслеживания зависимостей.
В коде мы наивно полагали, что jQuery к проекту уже подключен. А что если это не так. Все сломается.
;(function (factory) {
	if (typeof define === 'function' && define.amd) {
		// AMD
		define(['jquery'], factory);
	} else if (typeof exports === 'object') {
		// Node/CommonJS для Browserify
		module.exports = factory;
	} else {
		// Используя глобальные переменные браузера
		factory(jQuery);
	}
}(function ($) {
    'use script';
    // код плагина
}));

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

Регистрируйте свой проект в bower и npm


Серьезные ребята не будут использовать ваш плагин, если он не будет иметь возможность обновления. Качать плагин архивом с github это уже вчерашний день. Сейчас, достаточно зарегистрироваться на npmjs.com
Затем в корне своего проекта выполнить
npm init

И следовать инструкциям. В корне проекта появится package.json
После чего команда
npm publish ./

Из того же места, опубликует плагин в npm.

И конечные пользователи смогут устанавливать его себе так:
npm install tooltips

Выполнять npm publish ./ надо будет при каждой новой версии. Это не очень то удобно, а до этого еще нужно набрать кучу «git» команд.

Для себя я автоматизировал этот процесс через package.json. Добавил в поле scripts , такие команды:
"version": "1.0.0",
"scripts": {
    "github": "git add --all  && git commit -m \"New version %npm_package_version%\" && git tag %npm_package_version% && git push --tags origin HEAD:master && npm publish",
  },

А далее просто запускаю:
npm run github

Оно добавляет все файлы в git, делает commit, создает тег текущей версии, заливает все это на github, а потом еще и обновляет версию на npmjs.
Версию оно берет из package.json, и ее перед каждым вызовом нужно обновлять. Но можно и это автоматизировать, просто добавить в начало, перед git add:
npm version patch

Я работаю по windows, поэтому переменные package.json используются так — %npm_package_version%, в других ОС нужно использовать $npm_package_version .
Для bower ситуация почти аналогичная. Только нигде регистрироваться не надо.
Если у вас еще не установлен bower.
npm install -g bower

Затем
bower init

В корне проекта.
Когда будет создан bower.json, опубликуйте все на github, или другой хостинг.

После этого можно регистрировать пакет:
bower register jodit https://github.com/xdan/jodit.git


На этом пока все, пишите свои советы в комментариях. Спасибо за внимание!

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


  1. mrsum
    20.02.2016 12:20
    +13

    Хороший туториал, но содержание устарело. Лет на 5.


  1. Akuma
    20.02.2016 12:29
    +4

    jQuery сейчас… не в моде, чтоли. Посмотрите на ваши же проекты, скорее всего вы используете его только для навешивания событий, show/hide и ajax запросов. В 99% обычных проектов большего и не нужно. Даже анимации, которыми раньше пользовались все, сейчас делают на CSS.

    Плагины для jQuery тоже устарели. Но если уж пишите туториал по ним, то к «шаблону» добавьте поддержку AMD/CommonJs (или лучше обоих сразу), чтобы при подключении плагинов, например webpack-ом, не приходилось что-то дописывать.


    1. stardust_kid
      20.02.2016 12:47
      +1

      Для ajax уже давно есть window.fetch.


      1. Akuma
        20.02.2016 12:48
        +1

        Я ж и не спорю. как раз объясняю, что время jQuery постепенно уходит.


      1. RUQ
        20.02.2016 15:16
        -3

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


        1. stardust_kid
          20.02.2016 18:43

          Так в полифиле тот же самый xmlhttprequest.


    1. skoder
      20.02.2016 13:30

      Спасибо, обновил статью про bower и npm.

      По поводу jQuery вы безусловно правы, его все меньше и меньше, но когда мне к примеру требуется навешать на элемент события или несколько событий. Я без раздумий пишу.

      $('#elm').on('click keydown mousedown', someHandler) 

      Все то же самое я могу сделать и на vanila.js, но мне как минимум нужно будет описать, что-то вроде

      function on(elm, events, handler) {
         if (elm) {
             events.split(' ').forEach(function (event) {
                 elm.addEventListener(event, handler);
             });
         }
      }

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


      1. Akuma
        20.02.2016 13:53

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


      1. dshster
        20.02.2016 15:15
        +2

        Избавляясь от 3 строчек «лишнего» кода вы тянете целую библиотеку и ставите свой скрипт в зависимость от неё. Потом другой разработчик на каком-нибудь, например, Ангуляр, захочет использовать ваш плагин и ему придётся тянуть лишнюю зависимость, которая в самом Ангуляре ни к чему. Так и получается, что веб пухнет от всех зависимостей, потому что авторы считают нативные реализации «лишним кодом».
        В вашем коде нет ничего, чего нельзя или очень затратно сделать не используя jQuery.


        1. vshemarov
          20.02.2016 15:38
          +5

          Хм, написал автор плагин с использованием jQuery, а потом "другой разработчик на каком-нибудь, например, Ангуляр, захочет использовать ваш плагин и ему придётся тянуть лишнюю зависимость"

          Если я пишу с использованием jQuery, то и готовые решения подбираю на jQuery, если пишу на Angular, то и решения подбираю соответствующие. А если я начинаю ради пары-тройки плагинов тянуть в один проект несколько фреймворков и больших библиотек — это однозначно проблема во мне, а не в тех, кто писал хорошие плагины под конкретные фреймворки


          1. rznELVIS
            20.02.2016 15:53

            +1. Всему свое место. Вообще Ангуляр просто так не вводится, т.е. если вы уж решили вводить ангуляр то стройте под него приложение, а не цепляйте на ходу, а используйте jquery


            1. dshster
              20.02.2016 16:11
              +3

              А что заставляет разработчиков плагинов под Ангуляр использовать jQuery? Может, как автор выше прокомментировал: «без раздумий» подключать jQuery везде?


          1. dshster
            20.02.2016 15:53

            Вы попробуйте на ng-modules поискать UI плагины под Ангуляр — большая часть из них зависит от Bootstrap и от jQuery.
            Я конечно понимаю, что выбор разработчика под какие библиотеки и фреймворки выбирать плагины, но ведь ничто не мешает разрабатывать компоненты без зависимостей и использовать их с любым фреймворком? Современные браузеры уже давно решили проблему работы с DOM.


            1. boodda
              20.02.2016 22:09
              +1

              Тут как бы и зарыта проблема, что вот никак не получится в нормальном проекте без НЕ современных браузеров. Пишу полгода уже новую версию проекта(сервис), с нуля. И вот казалось бы сейчас развернусь, а тут говорят что у IE8 есть достаточная доля рынка и мы не можем его игнорировать. И все классные разработки идут лесом


      1. Mithgol
        20.02.2016 17:49
        +2

        Насчёт npm подскажу, что можно немного упростить себе жизнь, если из корня проекта вместо команды «npm publish ./» (которую Вы порекомендовали) подавать команду «npm publish» без дополнительных уточнений. (Эта команда по умолчанию работает с текущим каталогом, поэтому его явное указание в виде «./» — это лишнее.)


        1. skoder
          21.02.2016 17:09

          Спасибо, дополнил статью про автоматизацию этого процесса.


    1. gvozd1989
      20.02.2016 14:48

      А что в моде?


      1. Fen1kz
        20.02.2016 14:53
        -2

        (Дисклеймер: это ответ на вопрос "что в моде", а не холивар "х лучше jQuery")

        Angular (уже выходит из моды), React (подходит к закату). Angular 2 и его конкуренты.


        1. Akuma
          20.02.2016 15:06
          +1

          Ну на счет реакта я бы поспорил, а ангуляр просто обновится до второй версии. Да и ни то ни другое заменой JQ не является: фреймворк библиотеке не замена.

          По «что в моде» — ES6-7 и современные браузеры. Ведь изначально jQuery создавалось именно что-бы избавить программистов от плясок вокруг кучи браузеров.


          1. Sirion
            20.02.2016 16:35

            Я бы сказал, ES6-7 и транспайлеры.


          1. dshster
            20.02.2016 23:34

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

            В Ангуляре2 есть проблема (на мой взгляд проблема), то, что он под 3 платформы сразу — JavaScript, TypeScript и Dart. Я не знаю как комьюнити приспособится к такому делению.

            > Ведь изначально jQuery создавалось именно что-бы избавить программистов от плясок вокруг кучи браузеров

            Всё верно, но технологии не стоят на месте — если что-то было удобно во времена IE6(7), то это не значит, что нужно тянуть технологию до IE12 (Edge, условно).

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


        1. Zetoke
          21.02.2016 14:52

          А какое основание насчёт того, что React "подходит к закату".


      1. rznELVIS
        20.02.2016 15:53
        -2

        какая разница что в моде. Главное что эффективно


        1. Akuma
          20.02.2016 16:36

          Это ИТ. Здесь то, что не эффективно — не в моде. И наоборот.


  1. AlexPTS
    20.02.2016 16:43

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


  1. RubaXa
    20.02.2016 17:52
    +3

    Ещё хороший тон, когда плагин поддерживает привычные интерфейс jQuery UI, а это:

    $('.js-target').plugin('widget'); // возвращает ссылку на инстанс
    $('.js-target').plugin('option', 'name'); // получить опцию
    $('.js-target').plugin('option', 'name', 'value'); // установить
    $('.js-target').plugin('destroy'); // уничтожает всё связанное

    Если плагин порождает какой либо html, нужно оформлять его в виде шаблона, хотя бы с простецкой заменой по регулярке /\{(.*?)\}/ или тот же MicroTemplate использовать, там пара строчек. А совсем хорошо, когда можно передать функцию, чтобы в зависимости от параметра вернуть нужны html. Тоже самое касается всех имен классов и селекторов, которые используют плагин. Всё должно быть конфигурируемо.

    Так же нужно корректно использовать data:

    // Плохо
    $(this).data('tooltips');
    
    // Плохо
    var NAMESPACE = 'tooltips';
    $(this).data(NAMESPACE);
    
    // Нормуль
    var NAMESPACE = 'tooltips:' + Math.random();
    $(this).data(NAMESPACE, instance);

    Ещё одно правило хорошего тона, которое в целом относиться к любой публичной разработке, это выносить все внутренние утилитные методы, если они не приватные, в публичный статик объект, например какой-нибудь utils. Очень часто бывает, что уже все нужные методы для работы, те же самые: extend, addEvent, each и тому подобное, уже есть, но они оставлены в замыкании из-за чего приходится писать ровно тот же код, неуклонно увеличивая энтропию Вселенной.


    1. maxlips
      21.02.2016 08:08

      // Плохо
      $(this).data('tooltips');
      

      Почему это плохо?


      1. skoder
        21.02.2016 17:11

        Я так понимаю, чтобы никто случайно не затер эти данные.


      1. RubaXa
        22.02.2016 10:04

        Чтобы ваш плагин не пересекся с другими пользовательскими данными или другим плагином.


  1. Igogo2012
    22.02.2016 13:45

    Могу конечно ошибаться но конструкция

    var self = this;
    
    вроде бы считается антипаттерном, так как давно существует .bind(this) в прототипе Function


    1. RubaXa
      22.02.2016 14:17

      bind — полифилить нужно.
      А вот self, that, t и т.п. хорошо бы заменить на четкое и понятное _this.


      1. mrsum
        23.02.2016 15:54

        bind не надо полифилить? Вот это поворот :)


        1. RubaXa
          23.02.2016 16:28
          +1

          Добро пожаловать в реальный мир, где есть такие браузеры как IE < 9 и PhantomJS.


          1. mrsum
            23.02.2016 16:40

            А простите, почему-то подумал как раз о том, что не нужно полифилить )
            Вы конечно правы, мое беглое чтение Вашего коммента привело к непониманию.


            1. RubaXa
              23.02.2016 16:50

              Ага, как мое вашего, так что норм.


    1. YNile
      22.02.2016 14:32

      Сравнивать не совсем корректно :)
      Это разный подход.

      self — создает замыкание.
      .bind(this) — возвращает новую функцию.