Комментарии 21
1. Вы опоздали с такой статьёй лет на 10 точно. На 10!!! Карл!!!
2. Это плагин? Я уж ожидал тут увидеть супер плагин, ну в 2020 году то. А тут hello world на jquery
3. Чего такого умеет jQuery в 2020, что вы предпочитаете его, а не ванильный JavaScript?
4. Сравните вашу статью и вот эту, от 2012 года. habr.com/ru/post/158235
«Slibox — it's a very fast, powerful, tiny slider. Mobile-friendly, easy to use», код — github.com/GitCodeDestroyer/Slibox/blob/master/src/slibox.js. Красивое описание и трешовая реализация — бич опенсорс разработки, которая по моему опыту на 98% состоит из подобного. А статья — бесполезный самопиар, минус.
Как я в 15 лет написал свой первый jQuery-плагин и как их создавать.
В статье я показал как написать jQuery-плагин, а не слайдер. Все честно?
По коду, можете показать что не так, как можно улучшить мой код и стиль?
С одной стороны честно, а с другой технический материал низкого качества. Думаю, вам стоит сначала поучиться несколько лет, а затем — продолжить писать статьи, чтобы приносить пользу сообществу, а не делиться своими первыми открытиями.
Написал несколько комментариев по общим темам, ведь данное сообщество — не только чтобы хейтить и пиариться, но и помогать друг другу) Небольшую часть отрефакторил, но полноценная переработка — уже ваша зона ответственности. Кроме этого можно написать еще про множество моментов, но по более чистой версии:
!function ($) {
/**
* Все эти функции получаются в глобальной области видимости jQuery - это ненужно и может вступить в конфликт
* с другими плагинами. Нужно оформить просто в функциях, кроме $.fn.slibox
* Также лучше по-максимуму избегать анонимных функций, чтобы была возможность делать рекурсию и видеть
* семантичный стек вызовов
*
*/
$.fn.slideTo = function (slideTo, fromTimer = false) {
this.each(function () {
let sliderId = '#' + this.id,
slidesCount = $(sliderId).data("sb-slides-count");
if ($(sliderId).data("sb-carousel") || fromTimer) {
if (slideTo > slidesCount) {
slideTo = 1;
} else if (slideTo < 1) {
slideTo = slidesCount
}
}
if (fromTimer) {
if ($(sliderId).data('sb-timer')) {
$(sliderId + ' .sb-timer').removeClass('sb-timer-animate');
setTimeout(function () {
$(sliderId + ' .sb-timer').addClass('sb-timer-animate');
}, 100);
clearInterval(this.slidingInterval);
this.slidingInterval = setInterval(function () {
if (!$(sliderId)[0].paused) {
// console.log($(sliderId).data('sb-timer-time') / 100, $(sliderId)[0].time);
if ($(sliderId).data('sb-timer-time') / 100 != $(sliderId)[0].time - 1) {
$(sliderId)[0].time++;
} else {
if (($(sliderId).data('sb-timer-carousel') || $(sliderId).data('sb-active-slide') < $(sliderId).data('sb-slides-count'))) {
$(sliderId).slideToNext();
} else {
$(sliderId + ' .sb-timer').css('animation-play-state', 'paused');
}
$(sliderId)[0].time = 0;
}
}
}, 100);
}
}
if (slideTo) {
$(sliderId).toggleClass('sb-last-slide', slideTo == $(sliderId).data('sb-slides-count'));
$(sliderId).data("sb-active-slide", slideTo);
$(sliderId + " .sb-slide").removeClass("active");
$(sliderId + " .sb-slide:nth-of-type(" + slideTo + ")").addClass("active");
$(sliderId + " .sb-controller").removeClass("active"), $(sliderId + " .sb-controller:nth-of-type(" + slideTo + ")").addClass("active");
}
})
}
let sbCanDrag = true;
$.fn.slideToNext = function () {
this.each(function () {
if (this.className.match('slibox')) {
$(this).slideTo($(this).data('sb-active-slide') + 1, $(this).data('sb-timed-carousel'));
}
})
return $(this).data('sb-active-slide');
}
$.fn.slideToPrev = function () {
this.each(function () {
if (this.className.match('slibox')) {
$(this).slideTo($(this).data('sb-active-slide') - 1, $(this).data('sb-timed-carousel'));
}
})
return $(this).data('sb-active-slide');
}
$.fn.setTimeTo = function (time) {
this.each(function () {
$(this).data('sb-timer-time', time);
this.time = Math.ceil(this.time / time);
$('#' + this.id + ' .sb-timer').css('animation-duration', time + 'ms');
$('#' + this.id + ' .sb-timer').css('animation-play-state', 'running');
});
return $(this);
}
$.fn.slibox = function slibox(options) {
/**
* Не стоит сокращать названия переменных до несемантичных значений типа o, это ухудшает читаемость
* Также не стоит использовать хелперы ($.assign) в качестве замены стандарных возможностей языка
*
*/
const extendedOptions = Object.assign({
/**
* Не стоит использовать !0 или !1, код должен быть явным и с понятными типами,
* не заставляя разработчика лишний раз интерпретировать его.
* Также это упор на особенности языка, желательно избегать подобных конструкций
*
*/
height: false,
width: false,
activeSlide: 1,
renderArrows: true,
renderControllers: true,
imagesLinks: [],
loadErrorMessage: "Image is not loaded",
noImagesMessage: "There are no images links you added<br><small>Slibox</small>",
imageSize: "contain",
loaderLink: false,
imagePosition: "center",
animateCSS: false,
carousel: false,
timer: false,
timerTime: 5000,
timerCarousel: true,
}, options);
/**
* Перебор this.each излишен и не несет никакой смысловой нагрузки.
* Для chaining можно просто сделать return this
*
*/
/**
* Название el не подходит для строки, к тому же не стоит сокращать.
* Селектор также можно взять из уже обернутого this вместо "#" + this.id
*
* Также неизменяемые примитивы нужно объявлять через const
*
*/
const wrapperId = this.selector;
/**
* ВАЖНО: оборачивать элементы каждый раз - значит обращаться к DOM, это может быть нужно
* только тогда, когда элементы динамические. Статичные необходимо кешировать в переменных.
* Враппер можно взять из this, а не искать через $(wrapperId).
*
* Названия jQuery-переменных рекомендую начинать с $, чтобы избежать путаницы с другими типами
* сущностей
*
*/
const $wrapper = this;
$wrapper.addClass("slibox");
/**
* Двойное равенство не рекомендую использовать никогда, исключение - if (variable != null),
* так как это более лаконичная форма для if (variable !== null && variable !== undefined).
*
* imagesLinks - массив, если нужно на него проверить, то "object" != typeof extendedOptions.imagesLinks
* не подойдет.
*
*/
if (!Array.isArray(extendedOptions.imagesLinks) || extendedOptions.imagesLinks.length === 0) {
/**
* Не стоит писать конструкции вида return 1, 2, false. Инструкции должны быть описаны отдельно,
* а возвращаемое значение - соответствовать глобально возвращаемому, то есть this в данном случае
*
*/
extendedOptions.imagesLinks = [];
$wrapper.html('<h1 class="sb-error">' + extendedOptions.noImagesMessage + "</h1>")
return this;
}
if (!$wrapper.children('.sb-slides')) {
$('<div/>', {
class: 'sb-slides'
}).appendTo(wrapperId);
}
/**
* Хранить данные в объекте элемента конечно можно, но смысла в этом нет. Лучше переделать на
* отдельный объект. Это позволит IDE выдавать подсказки, что не получится в паттерне $(this).data("sb-slide")
*
*/
/**
* ВАЖНО: все строки нужно перевести в именованные константы для избежания опечаток и дубляжей
*
*/
$wrapper.data("sb-slides-count", extendedOptions.imagesLinks.length);
$wrapper.data("sb-carousel", extendedOptions.carousel);
$wrapper.data("sb-timer-time", extendedOptions.timerTime);
$wrapper.data("sb-timer-carousel", extendedOptions.timerCarousel);
$wrapper.data("sb-timer", extendedOptions.timer);
/*
* Setting a link of the loader
*/
if ("string" === typeof extendedOptions.loaderLink) {
$wrapper.css({
background: "url(" + extendedOptions.loaderLink + ") no-repeat center"
})
}
/*
* Create Arrows
*/
if (extendedOptions.renderArrows) {
$('<div/>', {
class: 'sb-arrow sb-arrow-left',
html: '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve"><g><path fill="#da5858" stroke="#da5858" d="M45.539,63.41c0.394,0.394,0.908,0.59,1.424,0.59s1.031-0.197,1.424-0.59c0.787-0.787,0.787-2.061,0-2.848 L20.059,32.233L48.407,3.886c0.786-0.787,0.786-2.062,0-2.848c-0.787-0.787-2.062-0.787-2.849,0L15.822,30.773 c-0.205,0.206-0.384,0.506-0.484,0.778c-0.273,0.738-0.092,1.567,0.465,2.124L45.539,63.41z" /></g></svg>',
data: {
'sb-slider': wrapperId,
'sb-arrow-direction': 'left'
}
}).appendTo(wrapperId);
$('<div/>', {
class: 'sb-arrow sb-arrow-right',
html: '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve"><g><path fill="#da5858" stroke="#da5858" d="M44.152,32.024L15.824,60.353c-0.787,0.787-0.787,2.062,0,2.849c0.394,0.394,0.909,0.59,1.424,0.59 c0.515,0,1.031-0.196,1.424-0.59l29.736-29.736c0.557-0.557,0.718-1.439,0.445-2.177c-0.101-0.272-0.26-0.519-0.464-0.725 L18.652,0.828c-0.787-0.787-2.062-0.787-2.848,0c-0.787,0.787-0.787,2.061,0,2.848L44.152,32.024z" /></g></svg>',
data: {
'sb-slider': wrapperId,
'sb-arrow-direction': 'right'
}
}).appendTo(wrapperId);
}
/**
* Проще переборы делать через extendedOptions.imagesLinks.forEach
*
*/
for (let i = 1; i <= extendedOptions.imagesLinks.length; i++) {
let slide = $(wrapperId + ' .sb-slides .sb-slide:nth-of-type(' + i + ')');
if (slide.length != 0) {
slide = slide
.data('sb-slide', i)
.attr('draggable', true)
.css({
'background-image': 'url("' + extendedOptions.imagesLinks[i - 1] + '")',
'background-repeat': 'no-repeat'
})
.addClass('sb-slide-' + i)
.html('<div class="sb-slider-content">' + slide.html() + '</div>');
} else {
slide = $('<div/>', {
class: 'sb-slide sb-slide-' + i,
data: {
'sb-slide': i
},
draggable: true,
css: {
'background-image': 'url("' + extendedOptions.imagesLinks[i - 1] + '")',
'background-repeat': 'no-repeat'
}
}).appendTo(wrapperId + ' .sb-slides').html('<div class="sb-slider-content"></div>');
}
/*
* Create Controllers
*/
if (extendedOptions.renderControllers) {
if (0 == i - 1) {
$('<div/>', {
class: 'sb-controllers',
data: {'sb-slider': wrapperId}
}).appendTo(wrapperId);
}
$('<div/>', {
class: 'sb-controller',
data: {
'sb-slider': wrapperId,
'sb-controller': i
}
}).appendTo(wrapperId + ' .sb-controllers');
}
/*
* Animate.css functionality
*/
if (extendedOptions.animateCSS) {
if ("object" == typeof extendedOptions.animateCSS) {
if (extendedOptions.animateCSS.length < i) {
$(slide).addClass(extendedOptions.animateCSS[extendedOptions.animateCSS.length - 1])
} else {
$(slide).addClass(extendedOptions.animateCSS[i - 1]);
}
} else if ("string" == typeof extendedOptions.animateCSS) {
$(slide).addClass(extendedOptions.animateCSS);
}
$(slide).addClass("animated");
}
/*
* Image Size
*/
if (extendedOptions.imageSize) {
if ("object" == typeof extendedOptions.imageSize) {
if (extendedOptions.imageSize.length < i) {
$(slide).css({
"background-size": extendedOptions.imageSize[extendedOptions.imageSize.length - 1]
})
} else {
$(slide).css({
"background-size": extendedOptions.imageSize[i - 1]
})
}
} else if ("string" == typeof extendedOptions.imageSize) {
$(slide).css({
"background-size": extendedOptions.imageSize
})
}
}
/*
* Image Position
*/
if (extendedOptions.imagePosition) {
if ("object" == typeof extendedOptions.imagePosition) {
if (extendedOptions.imagePosition.length < i) {
$(slide).css({
"background-position": extendedOptions.imagePosition[extendedOptions.imagePosition.length - 1]
})
} else {
$(slide).css({
"background-position": extendedOptions.imagePosition[i - 1]
})
}
} else {
$(slide).css({
"background-position": extendedOptions.imagePosition
})
}
}
} // End for
$wrapper.width(extendedOptions.width);
if (!extendedOptions.height) {
$wrapper.height(0.5625 * $wrapper.width())
} else {
$wrapper.height(extendedOptions.height)
}
if ("number" == typeof extendedOptions.activeSlide) {
if (extendedOptions.activeSlide > extendedOptions.imagesLinks.length) {
$wrapper.data("sb-active-slide", extendedOptions.imagesLinks.length)
} else if (extendedOptions.activeSlide < 1) {
$wrapper.data("sb-active-slide", 1)
}
$wrapper.data("sb-active-slide", extendedOptions.activeSlide)
} else {
$wrapper.data("sb-active-slide", 1)
}
if (extendedOptions.timer) {
$wrapper.data('sb-timed-carousel', extendedOptions.timerCarousel);
$('<div/>', {
class: 'sb-timer-container',
}).append($('<div/>', {
class: 'sb-timer sb-timer-animate',
style: 'animation-duration: ' + extendedOptions.timerTime + 'ms'
})).appendTo(wrapperId);
this.time = 2;
}
$(".sb-controller").unbind('click');
$(".sb-controller").click(function () {
$($(this).data("sb-slider")).slideTo($(this).data("sb-controller"))
})
$(".sb-arrow").unbind('click');
$(".sb-arrow").click(function () {
let sliderId = $(this).data("sb-slider"),
activeSlide = $wrapper.data("sb-active-slide");
if ("left" == $(this).data("sb-arrow-direction")) {
$(sliderId).slideTo(activeSlide - 1)
} else if ("right" == $(this).data("sb-arrow-direction")) {
$(sliderId).slideTo(activeSlide + 1)
}
})
$wrapper.hover(function () {
let sliderId = '#' + this.id;
$(sliderId + ' .sb-timer').css('animation-play-state', 'paused');
$(sliderId)[0].paused = true;
}, function () {
let sliderId = '#' + this.id;
$(sliderId + ' .sb-timer').css('animation-play-state', 'running');
$(sliderId)[0].paused = false;
});
$wrapper.slideTo($wrapper.data("sb-active-slide"), true);
$(wrapperId + ' .sb-slide').each(function () {
let box = $(this),
container = $wrapper[0];
this.boxOffset, this.myDragFlag = false
box.on('selectstart', function () {
sbCanDrag = false;
});
box[0].ondragstart = function (e) {
if (sbCanDrag) {
this.startX = e.pageX - box[0].offsetLeft - container.offsetLeft
this.myDragFlag = true
}
}
box[0].ondragend = function (e) {
if (sbCanDrag) {
this.boxOffset = e.pageX - this.startX;
if (this.boxOffset - container.offsetLeft <= -20) {
/**
* Вычисляемые переменные нужно оформлять в отдельных константах для лучшей читаемости
*
*/
$wrapper.slideTo($(this).data("sb-slide") + 1)
}
if (this.boxOffset - container.offsetLeft >= 20) {
$wrapper.slideTo($(this).data("sb-slide") - 1)
this.myDragFlag = false
}
} else {
sbCanDrag = true;
}
}
})
return this;
}
}(jQuery);
Все еще вижу подобные конструкции:
this.each(function() {
const $wrapper = $(this)
$wrapper.slibox('slideTo', $wrapper.data('activeSlide') - 1)
})
return this
this.each не нужен, а само слово this в большинстве случаев лучше не использовать, т.к. непонятно, что в нем находится — контекст штука динамическая. В данном случае можно так
const $wrapper = this;
const prevSlideIndex = $wrapper.data('activeSlide') - 1;
$wrapper.slibox('slideTo', prevSlideIndex);
return $wrapper;
Также вижу анонимные функции, конструкции !1, перебор for вместо forEach, двойное равенство, проверку 'object' != typeof extOptions.imagesLinks вместо Array.isArray, хранение данных в $wrapper.data вместо обычного объекта. В общем, меньше половины рекомендаций реализовано, так что дальше анализировать пока не буду
Три года назад JQuery был мёртв уже несколько лет. Вы три года потратили на изучение библиотеки, от которой уже на тот момент успели отказаться даже самые неповоротливые любители легаси, всё это время упорно игнорируя всё, что происходило в мире фронтенда? Зачем?
Через пару лет ждём статью про backbone, а ещё лет через пять — про стрелочные функции :-)
Однако, это должно оправдываться. Если вы делаете слайдер для себя — отлично. Если вы работаете над чужим проектом, то удорожаете разработку личным велосипедом. Так как на его создание уходит время, а еще его нужно будет поддерживать.
И все-таки не совсем понятно, зачем для этой цели выбирать jQuery. Если речь не стоит о поддержке старых браузеров, то чем плох современный JS? Если ради опыта и интереса, тогда понятно. А если нет, то нет.
А автору статьи я бы рекомендовал начать изучить что-нибудь из этих фреймворков: React, vue или Angular. Коммерческой пользы от потраченного времени больше будет.
Сейчас я изучаю Vue, отдельно и в связке с Laravel.
Как я в 15 лет написал свой первый jQuery плагин и как их создавать