Pull to refresh
44
-3
Дмитрий Казаков @DmitryKazakov8

Front-end архитектор

Send message

В отрыве от фреймворка это называется "двухсторонний биндинг" (https://seemple.js.org/), только с глобальным состоянием, а не локальным.

Разговор о том, что первично, тем более если вы сравниваете с концепцией "начало начал не имеет начала, безначальность бесконечна" (фрактал), был бы не особо уместен. В моем понимании это лишь одна из составляющих — движение есть следствие взаимодействия других сил, и для меня оно не первично.


Не знаю, как вы сделали вывод, что я отрицаю структуру в интерфейсах — но это другой слой, который не относится к данным, и представлен в виде иерархичного DOM-дерева, создаваемого опять же иерархичной композицией реакт-компонентов. Прокидывать же в инпут данные через всех родителей, а сигналы от него собирать по всей цепочке — это и есть ограничение, заставляющее "учитывать везде предельную скорость взаимодействия", портальность снимает эту проблему. Но она присутствует в цепочках react props, когда данные, нужные одному лишь чайлду, заставляют всех родителей и соседние компоненты перерендериваться, а в предлагаемой "фрактальной" архитектуре, похоже, именно так.

А вот неплохо, радует, что опыт познания мира начинает проникать в программирование. Тоже потихоньку пишу статью со своим симбиозом, но в основе не "всплытие" данных по дереву (как если смотреть на эту фрактальную картину сбоку), а единое глобальное состояние с доступом в любую точку. В материальном мире мы работаем с неэффективными передатчиками — свет, волны — которые обладают ограниченной скоростью и не позволяют получать доступ к краю Вселенной мгновенно, но в программировании можно обойти это ограничение и каждый элемент связывать "порталом" с глобальным стейтом:


@portal
class Input {
  render() {
    const { store } = this.portal;

    return <input value={store.any.data.from.the.universe} />;
  }
}

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


То есть относительно ваших идей я предлагаю не пробираться сквозь фракталы для доступа к компонентам через длинную цепочку (под капотом) вызовов, а обращаться к любому объекту напрямую, будто до него рукой подать. Надо до Сириуса — пожалуйста, как будто он за окном, и за всеми двойными звездами, которые есть за ним. Если вы работали с observable-паттерном, то это должно быть знакомо.


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


Да, не очень, конечно, говорить про свой подход, когда вы ждете комментариев про свою идею, но не вижу, чем бы была полезна такая вот фрактальность, которая добавляет лишний код и цепочки parent-child. Навскидку код будет сильно запутываться и при малейшей небрежности уходить в глубины call stack is too small for you imagination

Я давно остановился на observable (в данном случае mobx), для SPA ключевой момент — синхронизация js-состояния (стор) и DOM (компоненты с жизненным циклом). Если у компонента есть доступ к любым свойствам и методам хранилища, а ререндер происходит только при изменении использованных при прошлом рендере свойств, то такая система становится идеальной по удобству и перфомансу. При добавлении любых дополнительных прослоек — ручного проброса свойств и методов (коннект редакса), вручную написанных SCU, вызова методов через диспетчер, маппинга изменений в хранилище исходя из констант (типов экшенов), отсутствие прямого доступа к нескольким подсторам, иммутабельность и т.п., и удобство разработки, и скорость приложения ухудшаются. Так что тут минимализм вполне оправдан)


У реакт-роутера много недостатков, причем легко какой-либо функционал не внедрить. Надо блокировать переход на роут по каким-то условиям? Ставь версию, в которой есть Prompt. Нужно задавать роуты конфигом, а не свичом в компонентах? Ставь соответствующую версию. Выполнять действия перед загрузкой страницы и после ухода? Тоже подбирай из многообразия версий. Куда проще этой возни написать свой роутер, весь этот функционал займет не так уж много строк кода, включая синхронизацию с URL, безопасный парсинг, анимации переходов между страницами, промисные цепочки переходов.


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

Спасибо за перевод!


Подобный путь проходит большинство компаний с крупными реакт-приложениями. В статье, на мой взгляд, примерно середина этого пути "от первого прототипа до грамотно спроектированной системы". Чтобы комментарий не был пустословным, попробую предсказать дальнейший путь разработчиков в том проекте.


Как исходный уровень:


  • есть достаточно структурированная кодовая база
  • единообразие стиля написания кода и некоторых принципов (наименование, запрет использования определенных элементов, реэкспорт), наверняка внедрен и Prettier
  • пишутся юнит- и интеграционные тесты
  • есть страница с визуализацией компонентов и примерами использования (Storybook) без выделения в отдельный репозиторий (что намного более удобно, если не нужно ее использовать в совсем других проектах)
  • есть типы (с линтингом ESLint, что более современно)
  • организована двойная проверка кода — на гит хуки и в CI/CD
  • код группируется по модулям

Пространства для улучшения:


  • к тестовым прогонам можно прикрутить систему наподобие Allure
  • можно разделить тесты на части, чтобы не прогонять полные регрессы на пуш в каждую ветку, оставив полные только для релизных веток или ручного запуска
  • стек redux+saga чрезмерно многословен и запутан, в будущем явно разработчики поймут, что редюсеры, константы, селекторы, контейнеры, пробросы многочисленных импортов через пропсы, вручную написанные SCU (если не все на селекторах и каждый компонент контеризируется, что, наверное, еще хуже) намного более эффективно было бы заменить на мутабельные реактивные структуры
  • react-router, вполне возможно, будет заменен на другое решение. Эта библиотека меняет подходы от версии к версии и делает код несовместимым, а в последних версиях работа с конфигами практически не поддерживается, что приводит к вынужденному разбрасыванию роутов по различным файлам. Недостатки очевидны, разбиение на асинхронные чанки, поддержка и документирование осложнены (из конфига можно ее генерировать автоматически)
  • для локализации понадобится система автоматического создания id для сообщений + сбор всех констант в отдельный файл при сборке для отправки в систему локализации и независимого обновления текстов на сайте, без трат времени разработчика и изменения кода в гите
  • все, про что рассказывается в статье, нужно будет поместить в техническую базу знаний, так как количество соглашений и условностей уже стало большим, и обучить новых разработчиков будет не так просто лишь путем менторинга
  • вполне возможно, что от такого количества юнит-тестов ребята откажутся, оставив их только для утилит, так как интеграционные намного лучше справляются с обеспечением итогового качества, а shallow render реактовых компонентов пригождается в очень немногих случаях (например, когда один компонент может работать в десятках режимов)
  • в CI/CD внедрят проверку производительности (скорость рендеринга и т.п.) для истории перфоманса и анализа возможных узких мест
  • про CRA в начале было упомянуто, в дальнейшем про него не вижу рассуждений (как и ответов на вопросы из вступления), но если эта штука используется, то, безусловно, первым делом нужно будет переходить на кастомную сборку. Промышленная разработка и слишком высокоуровневые пакеты (особенно zero config) несовместимы по причине отсутствия гибкости, так что если ребята там присматриваются к Next.js, то лучше отказаться сразу от этой идеи.

Итогом этих нескольких шагов будет качественная смена состояния на более стабильное (примат интеграционных тестов над юнитами, анализ скорости), быстрее разрабатываемое (автоматика в локализации, отказ от неэффективных архитектур), быстрее выкладываемое (разделение тестов), проще изучаемое (техническая база знаний, автогенерация документации из конфигов, например — роутинга или апи). Потом, отталкиваясь от нового состояния этих организационных моментов и, зная специфику конкретного проекта, можно затронуть темы работы с гитом, связки различных систем (база знаний + дизайн-система + таск-трекер + гит-хранилище), работы с CDN, системы метрик… Много всего.


По выводу темы я бы не согласился с автором, сказавшим "А самое важное — стремитесь к минимализму". По моему опыту главное — это удобство, которое подразумевает довольно большую автоматизацию.

Одна из базовых характеристик отказоустойчивых систем — явно определенные потоки ошибок, которые попадают в соответствующие перехватчики. То есть, к примеру, при работе с апи бэка можно использовать такой флоу:


function authUser({ store, api, actions }, { login, password }) {
  return Promise.resolve()
    .then(() => api.postUserData({ login, password }))
    .then(({ userInfo }) => (store.user = userInfo))
    .catch(error => {
      if (error.name === "SOLVED") return;

      if (error.name === "INVALID_LOGIN") {
        return actions.pushFormValidators({ 
          password: { value => value === login, message: 'Пользователя не существует' } 
        })
      }

      return actions.showNotification('Неизвестная ошибка')
    })
}

При этом обработка HTTP-ошибок будет производиться в api-слое, то есть имеются следующие сценарии:


  • при 404/403 метод api.postUserData сам выдаст нотификацию и вернет Promise.reject(Err.SOLVED), таким образом обработчик выше не будет предпринимать действий.
  • при 402 (ошибка валидации или формата данных, присланная бэком) отдаст Promise.reject(Err[response.errorName]), обработчик подсветит эту ошибку в форме и не даст пользователю второй раз ввести некорректные данные
  • при неизвестной ошибке в самом экшене authUser (например, опечатка stAre.user = userInfo) либо при багах кода в процессе запроса, ошибка попадет в последнюю строчку кэтчера. Этот же сценарий в случае, если бэк вернет неизвестную константу на фронт вместо "INVALID_LOGIN", то есть при ошибке в контракте общения.

Таким образом явно определяются потоки всех возможных ошибок и стратегии их обработки (можно и дополнительные слои выносить, я привел базовый пример), четко ограничивается скоуп их распространения и исключается влияние на остальной функционал приложения. В целом, хотя при throw вместо Promise.reject будет работать так же, для типизации+семантичности все же лучше использовать Promise.reject, тут я с автором согласен, для предсказуемых "ошибок", которые по сути являются перенаправлением в другой блок обработки промиса (catch вместо then). Но учитывать возможность возникновения неожиданных исключений всегда необходимо, то есть, если заменить reject на throw, должно работать так же, иначе может возникнуть ситуация, что при выполнении действия ломается вся страница или большая часть функционала (очень часто встречаю это при работе).

Хорошие ответы, только не хватает этих оговорок в самом тексте статьи) Нужен контекст, что примененные подходы работают именно в вашем большом проекте, где:


  • в BFF и API Gateway проникла бизнес-логика и запутанные взаимосвязи (да, в статье об этом говорится, но как о возможности, а не об опорных исходных данных)
  • каждый байт на счету при передаче данных с бэка по апи (очень редкий кейс, характерный только для хайлоада, так как в остальных корп продуктах заботятся о перфомансе выполнения операций, стабильности и унифицированности, но не о наличии некоторых дополнительных данных)
  • архитектурные практики недостаточно контролируются, поэтому востребована система с жестким паттерном

Нетипизированные req & res в Express действительно проблема при интенсивном использовании, но никто не заставляет использовать их как хранилища. Так, сессии и кэш лучше держать в Redis, как и дополнительные данные типа traceId запросов, а работа с базой отлично типизируется и стандартизируется, что исключит "неконтролируемые мутации". В Nest же, как вижу, предлагается использовать неявный слой IoC, а про дополнительные данные, привязанные к сессии пользователя, не говорится, так что сравнить не с чем. В целом, не думаю, что это значительный недостаток.


В создании системы распределенного рендеринга участвовал, по сути она состоит из "определить микрофронтенды, которые ответственны за рендеринг страницы" + "ответы от их bff склеить в единый html-документ", эти задачи можно решить массой способов. Наверняка решение "подключить декоратором к контроллеру" продиктовано архитектурой Nest, то есть и в этом случае задействована специфика вашего проекта, в котором жесткие подходы выгоднее свободного архитектурирования. Для максимальной прозрачности удобнее было бы иметь сервис-детектор, который достаточно просто покрывается тестами, а сбор данных произвести через const requiredBffs = detector(currentRoute); const htmlParts = Promise.all(requiredBffs.map(fetch)) через слой кэширования, а затем провести склейку по маркерам. В идеале система должна работать автоматически, а информация о частях, из которых состоит конкретная страница, содержаться в конфиге роутов, что не потребует от разработчиков вообще задумываться об этом механизме и проставлять декораторы. То есть я к тому, что, возможно, жесткие паттерны в данном случае ухудшили экспириенс, а не улучшили)


И "как же писать бизнес-логику" в BFF — тоже контекстуальная тема, которая не должна затрагивать большинство корп проектов. Когда фронтовый бэк из рендерера для seo и ускорения первой отрисовки превращается в толстый бэк с кучей наворотов, пора бы задуматься, туда ли все повернуло, и не стоит ли остальную логику вынести на "хардкорный" бэк. Так что логичнее было бы вынести это в отдельную статью, сравнив несколько паттернов организации сущностей и взаимодействия между ними, ибо в итоге раздел "архитектурные слои" получился очень немногословным и смятым

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


  1. То, что можно делать SSR на ноде, создавая сервер-прослойку (BFF) — тема избитая и в корпоративных приложениях используется уже несколько лет.
  2. Проблема необходимости разного набора данных разным приложениям решается версионированием АПИ, тут ничем BFF не поможет
  3. API Gateway как маппер, в какой микросервис пойти в зависимости от урла запроса (https://api.domain.com/ms1/users | https://api.domain.com/ms2/auth) и по какому протоколу, действительно можно вынести в BFF, но фронтендерам придется следить за изменением инфраструктуры бэковых сервисов и синхронизироваться. Удобнее все же отдать реализацию этого маппера бэкендерам, в BFF же просто проксируя запросы в единую точку.
  4. То, что бизнес-логика в виде нормализации данных, стейт-машин, синхронизации между различными источниками (офлайн режим, сторонние сервисы), проверка прав есть и на фронтенде — это логично. Делать запрос на бэк "юзер нажал кнопку Восстановить пароль, есть ли у него права для отображения этой страницы?" или "можно ли ему показать кнопку редактирования?" действительно нет смысла, если можно проверить эти права во фронтовом стейте с пермишенами, полученными заранее. Хотя это и выглядит "размазыванием ответственности", в основном этот код обслуживает нужды интерфейса.
  5. Пример кода на Express, в котором "все слои перемешаны" — дело рук не этого простого, гибкого и удобного инструмента, а разработчика. Конечно, можно написать любую дичь и сказать "поддерживать такой код не хочется", но вот нападки на Express этим не обоснованы. Валидации, обработка ошибок, логирование, метод отправки на фронт (стримами в данном случае), миддлвары обработки запроса, схемы — все это выносится в удобные слои, которые хоть и склеиваются внутри миддлвар в итоге, но позволяют абстрагироваться.
  6. "протащить созданный экземпляр логгера через всё приложение" можно и через global (ничего страшного в таких микроприложениях не будет) или более удобным декоратором @useLogger class UserEntity extends EntityClass {}, для чего дополнительные невидимые контейнеры — не понятно. Это же BFF, простой сервер-рендерер-прокси, а не сложная система с кучей сервисов.
  7. Миддлвары — отличный паттерн, явно определяющий порядок обработки запроса и позволяющий удобно логировать этапы и в любой момент прервать этот поток, выбросив исключение, либо отправив response. При этом структурировать обработку можно как угодно — через мапперы, классы, функциональные контроллеры, схемы обработки, описанные в json, — все зависит от видения разработчика и применяемых подходов. Nest переусложнен и жестковато структурирован для BFF, в котором основной функционал — по схеме роутинга отдать рендер фронтендового фреймворка + проксировать запросы на бэк с соответствующими валидациями и нормализацией + логирование + работа с сессиями. Express идеален для подобных задач благодаря своей простоте и развитой экосистеме.

Ну и как-то я не уловил момент перехода с BFF с простым примером на всяческие UserEntity, домены, сервисы, пайпы, гарды… Это, видимо, когда фронтендеру после создания сервера-прокси пришла мысль "а почему бы весь бэк не переделать на ноду? Я могу!") Ну максимум там будут сервисы хранения сессии пользователей и кэширования. Или BFF был просто вступлением, а суть — в рекламе ролика с гексагональным паттерном для высчитывания остатка денег на счету пользователя? Критикую не ролик, а несоответствие теме статьи и в целом не совсем логичное повествование.

Ну только не {isCondition && <SomeComponent1 />} конечно, а {Boolean(isCondition) && <SomeComponent1 />}, так как этот оператор приводит к boolean лишь при сравнении, но выводит в итоге оригинальное значение. Была в одном известном проекте система прав и пермишенов, основанная на 0 и 1 вместо булеанов, то есть isCondition был при отсутствии прав === 0, соответственно на странице оказывалось немало нод-нулей, отчего ехала верстка. А так как разработчиков было много, пришлось очень тщательно следить за этим на код-ревью.

Такое предложение в трекере уже есть, дописал коммент с предложением скопировать функционал из раздела Prettier версии 2020.2. Должно быть действительно полезно всем фронтендерам, так как обычное форматирование "Reformat Code" слабо стандартизируется. Реализация такой фичи в моих глазах бы значительно увеличила привлекательность данного IDE, если бы уже не был прочно завербован в армию использующих)

Спасибо, в основном пользуюсь Eslint --fix on save, тоже недавно появилась в вашем редакторе — так как в моих проектах prettier интегрирован в eslint, то фиксы обоих инструментов применяются одновременно.
Такой вопрос — планируется ли для Stylelint сделать похожий функционал? Пока приходится настраивать отдельный вотчер, который после сохранения файла переформатирует его. Это занимает по времени в районе 1 секунды, по сравнению с моментальным реформаттингом js-файлов кажется, что уже не круто)

Есть еще https://github.com/gristlabs/ts-interface-builder, если я правильно понял задачу как генерацию валидационных функций из ts-типов

В целом да, задачи, где требуется математика, современному фронтендеру встречаются очень редко. В основном это либо рисование анимаций на канвасе (траектории), либо высчитываемые по формулам линии на графиках. А делать браузерные игры — это уже совсем другая профессия. Так что в целом достаточно и математики 7 класса + знания особенностей работы с числами в JS (округление, bigInt, погрешности при расчетах, конвертация относительных и абсолютных величин, операции с датами и временем в миллисекундах, дельты для определения направления движения мыши / прокрутки, разбиение области на взаимно влияющие зоны для увеличения / уменьшения размеров элемента).


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

createOAuthString('https://github.com', 'abc123', 'repo,user')

Так делать, конечно, не надо, если вдруг кто-то из новичков решит использовать материалы статьи в своих проектах. Пишите


createOAuthString({
  host: 'https://github.com', 
  clientId: 'abc123', 
  scope: 'repo,user'
})

Иначе непременно запутаетесь в порядке, либо это приведет к антипаттерну, как во многих опенсорс-творениях, типа 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(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 вместо обычного объекта. В общем, меньше половины рекомендаций реализовано, так что дальше анализировать пока не буду

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


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


Скрытый текст
!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, то не подумал о таком варианте.

Information

Rating
Does not participate
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity