В целом да, задачи, где требуется математика, современному фронтендеру встречаются очень редко. В основном это либо рисование анимаций на канвасе (траектории), либо высчитываемые по формулам линии на графиках. А делать браузерные игры — это уже совсем другая профессия. Так что в целом достаточно и математики 7 класса + знания особенностей работы с числами в JS (округление, bigInt, погрешности при расчетах, конвертация относительных и абсолютных величин, операции с датами и временем в миллисекундах, дельты для определения направления движения мыши / прокрутки, разбиение области на взаимно влияющие зоны для увеличения / уменьшения размеров элемента).
Это несложно с математической точки зрения, поэтому принято считать, что фронтендерам не нужна сложная математика, но требования к логике и внимательности очень высокие, так как приходится учитывать множество краеугольных кейсов и возможность работы кода в различных окружениях. А эти качества отлично нарабатываются математическими и геометрическими задачами, поэтому, опять же, принято считать, что разработчик с плотным точнонаучным бэкграундом быстрее напишет стабильное решение, но это полумиф, на мой взгляд.
Иначе непременно запутаетесь в порядке, либо это приведет к антипаттерну, как во многих опенсорс-творениях, типа createOAuthString(null, null, 'repo,user'), плюс каждому разработчику придется изучать непосредственно реализацию функции (и не раз в зависимости от задач и ввиду забывчивости).
Получение текстовых строк 200 { error: "User already exists" } действительно с системой локализации не матчится и подходит только для неожиданных сбоев, поэтому во всех проектах делаем константы 200 { error: "USER_EXISTS" }, фронт уже выполнит необходимую логику и заберет из локалей перевод. С остальным согласен, HTTP коды только для транспортных ошибок удобны, а те, кто на них опирается в разработке сложных приложений, сталкиваются с их многозначностью, которую приходится специфицировать либо в body, либо в заголовках ответа (типа 402 resp.headers { validation_error: "FIELD_NOT_VALID" }).
да, можно было бы писать на MobX и не думать о заведении констант с type, объектов с type и пэйлоадом, мутациях стора в иммутабельном формате, когда вместо изменения одних параметров приходится дублировать весь стор, кучи дополнительных ts-типов, иерархии проброса контекстов, лишних ререндерах. Так туду-листы уже не пишут
Если только для фронтенда, то действительно, разделение на прод-зависимости и дев-зависимости не несет значимого смысла, т.к. в ci после сборки не нужны node_modules, и в финальную папку можно копировать только папку build.
В изоморфной же схеме либо при сборке для ноды, когда зависимости не включаются в билд, а подтягиваются в рантайме (например, с помощью опции externals в Webpack), разделение на разного рода зависимости становится актуальным. После сборки в финальном образе с файлами ci в идеале должен выполнить npm i --production, чтобы очистить node_modules от лишнего и уменьшить размер. Эту оговорочку надо бы включить в статью
this.each не нужен, а само слово this в большинстве случаев лучше не использовать, т.к. непонятно, что в нем находится — контекст штука динамическая. В данном случае можно так
Также вижу анонимные функции, конструкции !1, перебор for вместо forEach, двойное равенство, проверку 'object' != typeof extOptions.imagesLinks вместо Array.isArray, хранение данных в $wrapper.data вместо обычного объекта. В общем, меньше половины рекомендаций реализовано, так что дальше анализировать пока не буду
С одной стороны честно, а с другой технический материал низкого качества. Думаю, вам стоит сначала поучиться несколько лет, а затем — продолжить писать статьи, чтобы приносить пользу сообществу, а не делиться своими первыми открытиями.
Написал несколько комментариев по общим темам, ведь данное сообщество — не только чтобы хейтить и пиариться, но и помогать друг другу) Небольшую часть отрефакторил, но полноценная переработка — уже ваша зона ответственности. Кроме этого можно написать еще про множество моментов, но по более чистой версии:
Скрытый текст
!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);
Да, годное решение для разработки на ноде, если не отвлекают тексты ошибок в консоли процесса. Так как использую тайпчекинг только на финальной стадии с включенным noEmitOnError, то не подумал о таком варианте.
Проверка типов, подсветка, автодополнение для TS — есть в используемом мной WebStorm без дополнительных плагинов, насколько знаю — и в других IDE. Медленнее идет разработка, потому что при пересборке при любой ошибке компиляция падает и программа не работает, а то, что типы не указаны говорит и IDE, никакого смысла параллельно проверять компилятором не вижу — когда будет написана рабочая версия кода с несколькими итерациями рефакторинга я покрываю код типами и при коммите все типы в программе проверяются.
Это я и назвал «неблокирующим режимом», который помогает, а не мешает. Использую как для разработки для ноды, так и для фронта
А где, собственно, разбор кода как написать слайдер, на который дана ссылка? Зачем читателям простейшая информация из документации?
«Slibox — it's a very fast, powerful, tiny slider. Mobile-friendly, easy to use», код — github.com/GitCodeDestroyer/Slibox/blob/master/src/slibox.js. Красивое описание и трешовая реализация — бич опенсорс разработки, которая по моему опыту на 98% состоит из подобного. А статья — бесполезный самопиар, минус.
TS, на мой взгляд, слишком медленный и неудобный для разработки. При каждом изменении проверять все типы — значит блокировать полет мысли при разработке и замедлять тестирование работы в реальном окружении. Поэтому я все же предпочитаю при кодинге применять babel с плагином, вырезающим типы, а в качестве хелпера использовать встроенные в IDE подсказки тайпскрипта. Работает намного быстрее и не блокирует.
А проверку типов во всем коде можно выполнять на precommit и в ci
Спасибо за описание некоторых проблем при скриншотном тестировании и возможных путей их решения — но для чего они, собственно? Кроссбраузерность на Cypress не проверить, и кроссплатформенность верстки тоже, хотя из-за особенностей браузеров и систем могут быть значительные различия в отображении. Визуальное сравнение может помочь только в кейсах, когда возникло неожиданное влияние одних стилей на другие (неправильно указаны слои, совпадение имен классов на глобальном уровне, изменившийся при prod-оптимизации порядок следования стилей), но по большей части подобные баги выявятся на ручном feature-тестировании, а при грамотных подходах их вообще не должно возникнуть. Бывает, конечно, что в одном месте поменяли — и в очень далеком аукнулось, что можно выявить только полноценным регрессом, но создать систему скриншотного тестирования, способную отловить подобное — настолько сложное и муторное занятие, что вряд ли оправданно.
Спасибо, годно!
Для крупных проектов с интеграциями действительно пригодится знать смещения контента и влияние продолжительности загрузки ассетов на блокировку интерактивности. Для обычных корпоративных проектов, где больших ресурсов на исследование производительности нет, я обычно опираюсь на стандартные браузерные средства без обсерверов:
добавляя туда userAgent и информацию из роутера. При разработке на Реакте классовым методом — еще навешиваю декоратором измерение на componentDidMount и количество ререндеров. При необходимости можно из window.performance также вытянуть информацию по времени загрузки ресурсов, но эти метрики лучше вести отдельно по регионам, так как они относятся к качеству CDN. В целом этого достаточно для локализации всех проблем с производительностью. Но если есть ресурсы на отдельную команду по перфомансу — то можно и заморочиться с детальной разбивкой метрик, как в статье
Хранить конфиг в jsx — это как хранить его в верстке, доступ к конфигурации каждого поля сложен и медленен, и вместо единообразных конфиг-объектов приходится дублировать код от формы к форме. Если у вас будет 50 таких форм, и нужно изменить какую-то логику, придется заниматься этим в каждом конкретном компоненте, который для всего остального приложения является «черным ящиком». Это в целом очень неудобный паттерн, и то, что он используется в нескольких опенсорс-решениях для упрощения интеграции в свой проект, ничуть не свидетельствует о том, что это решение лучше. Наоборот, проблем при интеграции этих библиотек по опыту больше, чем от использования движка на конфигах.
Для меня Реакт — библиотека, синхронизирующая состояние между js-стейтом и DOM с бонусом в виде жизненного цикла компонентов. JSX в качестве хранилища стейта крайне неудобен ввиду сложности сериализации-десериализации, он является лишь описанием, как js-структуры корректно перевести в DOM.
Насчет кучи проверок и ифов в конструкторе форм из конфига — вы явно перегибаете, их будет совсем немного при грамотном проектировании, а в большинстве случаев — ни одной: {formConfig.map(({ FieldComponent, fieldProps }) => <FieldComponent {...fieldProps}/>)}, но при динамической раскладке сюда добавится дополнительный слой в виде группировки полей из конфига по определенному признаку и их выведению в соответствующих размеру экрана рядах, но это несколько дополнительных строк, и уж точно «дополнительных фреймворков» не нужно.
If-конструкции уходят практически полностью благодаря переносу в конфиг в виде семантических параметров — isDisabled, isShown, isOptional, и их можно менять динамически в удобной манере: formConfig.phone.isOptional = formConfig.email.isValid() || formConfig.name.isDisabled || true, и зашивать это статично в валидатор — плохая идея. При сабмите формы легко можно пробежаться по isDisabled и исключить эти поля и из отправки на бэк, и из валидаций.
Через пропы влиять на jsx — путь к раздуванию компонентов и рассинхрону апи у однородных сущностей. А вот добавить параметр к унифицированному компоненту и сделать изменение в одной точке — что может быть проще? Если компонент рендеринга форм начинает выглядеть сложно — это композиционная проблема, а не проблема подхода.
В целом для простейших форм и если их до десятка на проекте, можно и по принципу «черного ящика» создавать компоненты и с локальным стейтом, но в перспективе все равно придется создавать унифицированное решение с более открытым интерфейсом.
Я бы не бухтел при выдаче подобных задач, только если бы очень хотел попасть именно в эту контору и условия были бы отличными. Но если меня собеседует совсем «зеленый» человек и говорит что надо использовать двойное равенство вместо тройного, потому что так короче, и дает задачу написать сортировку «пузырьком», то чешу репу и говорю «я вам перезвоню»)) Не шучу, бывало и такое, причем на собеседовании на сеньорские позиции.
Еще на алгоритмы некоторые ребята дают… Тоже за то, чтобы узнавать у собеседующихся об их опыте путем проектирования решения для конкретных рабочих задач. Механика воплощения этих проектов будет зависеть от фреймворка и архитектуры, от подходов, использующихся в проекте, и, как правило, с вдохновением из StackOverflow, а не кодингом «с нуля». Но от юниоров, закончивших какие-нибудь курсы, хотелось бы ожидать как раз умения работать с чистым языком и логикой, так как опыта в решении реальных задач у них, как правило, нет, так что подобные задачки подойдут.
По поводу хранения состояния форм (валидаторы, значения, функции для проверки каждого поля, характеристики полей) я пришел к тому, что глобальный стейт для этого выгоднее локального, но только при реактивной архитектуре. Взаимодействие с бэком вне слоя апи, а напрямую из компонентов приводит к слишком разбросанной и сложно контролируемой логике. «Просто отображать ошибку» — интересно, каким образом вы планируете это сделать, и как планируете не позволять юзеру отправлять поле с некорректными значениями. Но интересно не очень, так как проектировал системы сложных форм десятки раз, и с помощью внешнего обращения к внутреннему стейту компонентов, и параллельной глобальной системой контроля состояния полей непосредственно через DOM, и двухсторонней связкой по системе событий — у этих способов довольно много недостатков, которые можно устранить именно глобальным реактивным состоянием. Попробуйте, должно понравиться.
В нереактивном глобальном стейте (обычном реактовом контексте), конечно, можно хранить функции и выполнять запросы, но будут проблемы с лишними ререндерами.
По сути просто не нужно обращаться к соседним сторам в constructor, то есть сначала создается весь стор целиком, а потом вызываются методы. Так не будет никаких undefined, и setTimeout не пригодится) Так что тоже не считаю это проблемой
Есть еще https://github.com/gristlabs/ts-interface-builder, если я правильно понял задачу как генерацию валидационных функций из ts-типов
В целом да, задачи, где требуется математика, современному фронтендеру встречаются очень редко. В основном это либо рисование анимаций на канвасе (траектории), либо высчитываемые по формулам линии на графиках. А делать браузерные игры — это уже совсем другая профессия. Так что в целом достаточно и математики 7 класса + знания особенностей работы с числами в JS (округление, bigInt, погрешности при расчетах, конвертация относительных и абсолютных величин, операции с датами и временем в миллисекундах, дельты для определения направления движения мыши / прокрутки, разбиение области на взаимно влияющие зоны для увеличения / уменьшения размеров элемента).
Это несложно с математической точки зрения, поэтому принято считать, что фронтендерам не нужна сложная математика, но требования к логике и внимательности очень высокие, так как приходится учитывать множество краеугольных кейсов и возможность работы кода в различных окружениях. А эти качества отлично нарабатываются математическими и геометрическими задачами, поэтому, опять же, принято считать, что разработчик с плотным точнонаучным бэкграундом быстрее напишет стабильное решение, но это полумиф, на мой взгляд.
Так делать, конечно, не надо, если вдруг кто-то из новичков решит использовать материалы статьи в своих проектах. Пишите
Иначе непременно запутаетесь в порядке, либо это приведет к антипаттерну, как во многих опенсорс-творениях, типа
createOAuthString(null, null, 'repo,user')
, плюс каждому разработчику придется изучать непосредственно реализацию функции (и не раз в зависимости от задач и ввиду забывчивости).Получение текстовых строк
200 { error: "User already exists" }
действительно с системой локализации не матчится и подходит только для неожиданных сбоев, поэтому во всех проектах делаем константы200 { error: "USER_EXISTS" }
, фронт уже выполнит необходимую логику и заберет из локалей перевод. С остальным согласен, HTTP коды только для транспортных ошибок удобны, а те, кто на них опирается в разработке сложных приложений, сталкиваются с их многозначностью, которую приходится специфицировать либо в body, либо в заголовках ответа (типа402 resp.headers { validation_error: "FIELD_NOT_VALID" }
).да, можно было бы писать на MobX и не думать о заведении констант с type, объектов с type и пэйлоадом, мутациях стора в иммутабельном формате, когда вместо изменения одних параметров приходится дублировать весь стор, кучи дополнительных ts-типов, иерархии проброса контекстов, лишних ререндерах. Так туду-листы уже не пишут
Если только для фронтенда, то действительно, разделение на прод-зависимости и дев-зависимости не несет значимого смысла, т.к. в ci после сборки не нужны node_modules, и в финальную папку можно копировать только папку build.
В изоморфной же схеме либо при сборке для ноды, когда зависимости не включаются в билд, а подтягиваются в рантайме (например, с помощью опции externals в Webpack), разделение на разного рода зависимости становится актуальным. После сборки в финальном образе с файлами ci в идеале должен выполнить npm i --production, чтобы очистить node_modules от лишнего и уменьшить размер. Эту оговорочку надо бы включить в статью
Все еще вижу подобные конструкции:
this.each не нужен, а само слово this в большинстве случаев лучше не использовать, т.к. непонятно, что в нем находится — контекст штука динамическая. В данном случае можно так
Также вижу анонимные функции, конструкции !1, перебор for вместо forEach, двойное равенство, проверку 'object' != typeof extOptions.imagesLinks вместо Array.isArray, хранение данных в $wrapper.data вместо обычного объекта. В общем, меньше половины рекомендаций реализовано, так что дальше анализировать пока не буду
С одной стороны честно, а с другой технический материал низкого качества. Думаю, вам стоит сначала поучиться несколько лет, а затем — продолжить писать статьи, чтобы приносить пользу сообществу, а не делиться своими первыми открытиями.
Написал несколько комментариев по общим темам, ведь данное сообщество — не только чтобы хейтить и пиариться, но и помогать друг другу) Небольшую часть отрефакторил, но полноценная переработка — уже ваша зона ответственности. Кроме этого можно написать еще про множество моментов, но по более чистой версии:
Это я и назвал «неблокирующим режимом», который помогает, а не мешает. Использую как для разработки для ноды, так и для фронта
«Slibox — it's a very fast, powerful, tiny slider. Mobile-friendly, easy to use», код — github.com/GitCodeDestroyer/Slibox/blob/master/src/slibox.js. Красивое описание и трешовая реализация — бич опенсорс разработки, которая по моему опыту на 98% состоит из подобного. А статья — бесполезный самопиар, минус.
А проверку типов во всем коде можно выполнять на precommit и в ci
Для крупных проектов с интеграциями действительно пригодится знать смещения контента и влияние продолжительности загрузки ассетов на блокировку интерактивности. Для обычных корпоративных проектов, где больших ресурсов на исследование производительности нет, я обычно опираюсь на стандартные браузерные средства без обсерверов:
const navigationTiming = performance.getEntriesByType('navigation')[0]
const loggedNavigationKeys = [
'domComplete',
'responseStart',
'domInteractive',
'domainLookupEnd',
'domContentLoadedEventEnd',
];
добавляя туда userAgent и информацию из роутера. При разработке на Реакте классовым методом — еще навешиваю декоратором измерение на componentDidMount и количество ререндеров. При необходимости можно из window.performance также вытянуть информацию по времени загрузки ресурсов, но эти метрики лучше вести отдельно по регионам, так как они относятся к качеству CDN. В целом этого достаточно для локализации всех проблем с производительностью. Но если есть ресурсы на отдельную команду по перфомансу — то можно и заморочиться с детальной разбивкой метрик, как в статье
Для меня Реакт — библиотека, синхронизирующая состояние между js-стейтом и DOM с бонусом в виде жизненного цикла компонентов. JSX в качестве хранилища стейта крайне неудобен ввиду сложности сериализации-десериализации, он является лишь описанием, как js-структуры корректно перевести в DOM.
Насчет кучи проверок и ифов в конструкторе форм из конфига — вы явно перегибаете, их будет совсем немного при грамотном проектировании, а в большинстве случаев — ни одной:
{formConfig.map(({ FieldComponent, fieldProps }) => <FieldComponent {...fieldProps}/>)}
, но при динамической раскладке сюда добавится дополнительный слой в виде группировки полей из конфига по определенному признаку и их выведению в соответствующих размеру экрана рядах, но это несколько дополнительных строк, и уж точно «дополнительных фреймворков» не нужно.If-конструкции уходят практически полностью благодаря переносу в конфиг в виде семантических параметров — isDisabled, isShown, isOptional, и их можно менять динамически в удобной манере:
formConfig.phone.isOptional = formConfig.email.isValid() || formConfig.name.isDisabled || true
, и зашивать это статично в валидатор — плохая идея. При сабмите формы легко можно пробежаться по isDisabled и исключить эти поля и из отправки на бэк, и из валидаций.Через пропы влиять на jsx — путь к раздуванию компонентов и рассинхрону апи у однородных сущностей. А вот добавить параметр к унифицированному компоненту и сделать изменение в одной точке — что может быть проще? Если компонент рендеринга форм начинает выглядеть сложно — это композиционная проблема, а не проблема подхода.
В целом для простейших форм и если их до десятка на проекте, можно и по принципу «черного ящика» создавать компоненты и с локальным стейтом, но в перспективе все равно придется создавать унифицированное решение с более открытым интерфейсом.
В нереактивном глобальном стейте (обычном реактовом контексте), конечно, можно хранить функции и выполнять запросы, но будут проблемы с лишними ререндерами.