Писать расширения на 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)
Akuma
20.02.2016 12:29+4jQuery сейчас… не в моде, чтоли. Посмотрите на ваши же проекты, скорее всего вы используете его только для навешивания событий, show/hide и ajax запросов. В 99% обычных проектов большего и не нужно. Даже анимации, которыми раньше пользовались все, сейчас делают на CSS.
Плагины для jQuery тоже устарели. Но если уж пишите туториал по ним, то к «шаблону» добавьте поддержку AMD/CommonJs (или лучше обоих сразу), чтобы при подключении плагинов, например webpack-ом, не приходилось что-то дописывать.stardust_kid
20.02.2016 12:47+1Для ajax уже давно есть window.fetch.
RUQ
20.02.2016 15:16-3Жаль его нет в IE, можно конечно использовать полифил, но это опять значит какой-то доп.инструмент.
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); }); } }
А это уже лишний не нужный код. Безусловно уже есть множество удобный библиотек для этих целей, но зачем использовать их все, если есть однаAkuma
20.02.2016 13:53Да я и сам им пользуюсь, уже скорей по привычке, да и у пользователей он скорее всего закеширован. Хотя для мобильников когда что-то делаю, стараюсь вообще отказываться от библиотек/фреймворков по возможности.
dshster
20.02.2016 15:15+2Избавляясь от 3 строчек «лишнего» кода вы тянете целую библиотеку и ставите свой скрипт в зависимость от неё. Потом другой разработчик на каком-нибудь, например, Ангуляр, захочет использовать ваш плагин и ему придётся тянуть лишнюю зависимость, которая в самом Ангуляре ни к чему. Так и получается, что веб пухнет от всех зависимостей, потому что авторы считают нативные реализации «лишним кодом».
В вашем коде нет ничего, чего нельзя или очень затратно сделать не используя jQuery.vshemarov
20.02.2016 15:38+5Хм, написал автор плагин с использованием jQuery, а потом "другой разработчик на каком-нибудь, например, Ангуляр, захочет использовать ваш плагин и ему придётся тянуть лишнюю зависимость"…
Если я пишу с использованием jQuery, то и готовые решения подбираю на jQuery, если пишу на Angular, то и решения подбираю соответствующие. А если я начинаю ради пары-тройки плагинов тянуть в один проект несколько фреймворков и больших библиотек — это однозначно проблема во мне, а не в тех, кто писал хорошие плагины под конкретные фреймворкиrznELVIS
20.02.2016 15:53+1. Всему свое место. Вообще Ангуляр просто так не вводится, т.е. если вы уж решили вводить ангуляр то стройте под него приложение, а не цепляйте на ходу, а используйте jquery
dshster
20.02.2016 16:11+3А что заставляет разработчиков плагинов под Ангуляр использовать jQuery? Может, как автор выше прокомментировал: «без раздумий» подключать jQuery везде?
dshster
20.02.2016 15:53Вы попробуйте на ng-modules поискать UI плагины под Ангуляр — большая часть из них зависит от Bootstrap и от jQuery.
Я конечно понимаю, что выбор разработчика под какие библиотеки и фреймворки выбирать плагины, но ведь ничто не мешает разрабатывать компоненты без зависимостей и использовать их с любым фреймворком? Современные браузеры уже давно решили проблему работы с DOM.boodda
20.02.2016 22:09+1Тут как бы и зарыта проблема, что вот никак не получится в нормальном проекте без НЕ современных браузеров. Пишу полгода уже новую версию проекта(сервис), с нуля. И вот казалось бы сейчас развернусь, а тут говорят что у IE8 есть достаточная доля рынка и мы не можем его игнорировать. И все классные разработки идут лесом
Mithgol
20.02.2016 17:49+2Насчёт npm подскажу, что можно немного упростить себе жизнь, если из корня проекта вместо команды
«npm publish ./» (которую Вы порекомендовали) подавать команду«npm publish» без дополнительных уточнений. (Эта команда по умолчанию работает с текущим каталогом, поэтому его явное указаниев виде «./» — это лишнее.)
gvozd1989
20.02.2016 14:48А что в моде?
Fen1kz
20.02.2016 14:53-2(Дисклеймер: это ответ на вопрос "что в моде", а не холивар "х лучше jQuery")
Angular (уже выходит из моды), React (подходит к закату). Angular 2 и его конкуренты.Akuma
20.02.2016 15:06+1Ну на счет реакта я бы поспорил, а ангуляр просто обновится до второй версии. Да и ни то ни другое заменой JQ не является: фреймворк библиотеке не замена.
По «что в моде» — ES6-7 и современные браузеры. Ведь изначально jQuery создавалось именно что-бы избавить программистов от плясок вокруг кучи браузеров.dshster
20.02.2016 23:34Ангуляр1 и Ангуляр2 абсолютно разные фреймворки, это не обновление и даже не расширение. Возможно Ангуляр2 следовало бы назвать другим именем, но видимо хотелось использовать прежний бренд для узнаваемости.
В Ангуляре2 есть проблема (на мой взгляд проблема), то, что он под 3 платформы сразу — JavaScript, TypeScript и Dart. Я не знаю как комьюнити приспособится к такому делению.
> Ведь изначально jQuery создавалось именно что-бы избавить программистов от плясок вокруг кучи браузеров
Всё верно, но технологии не стоят на месте — если что-то было удобно во времена IE6(7), то это не значит, что нужно тянуть технологию до IE12 (Edge, условно).
jQuery сыграл большую роль в веб-разработке и вообще развитии фронтенда, благодаря ему браузеры получили очень много нативных технологий, но пора развиваться дальше.
AlexPTS
20.02.2016 16:43Для своих классов можно использовать паттерн модуля и паттерн открытия модуля, когда функция конструктор возвращает явно объект. Который является по сути маппингом на методы и свойства замыкания, чтобы сделать из публичными.
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 и тому подобное, уже есть, но они оставлены в замыкании из-за чего приходится писать ровно тот же код, неуклонно увеличивая энтропию Вселенной.
Igogo2012
22.02.2016 13:45Могу конечно ошибаться но конструкция
вроде бы считается антипаттерном, так как давно существует .bind(this) в прототипе Functionvar self = this;
RubaXa
22.02.2016 14:17bind
— полифилить нужно.
А вотself
,that
,t
и т.п. хорошо бы заменить на четкое и понятное_this
.
YNile
22.02.2016 14:32Сравнивать не совсем корректно :)
Это разный подход.
self — создает замыкание.
.bind(this) — возвращает новую функцию.
mrsum
Хороший туториал, но содержание устарело. Лет на 5.