Как стать автором
Обновить
44
-3
Дмитрий Казаков @DmitryKazakov8

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

Отправить сообщение

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

Если проект экспериментальный - то делать полностью свой конфиг.

Вот здесь https://habr.com/ru/post/506636/ описывал намного более продвинутый вариант конфигурирования - конфиг вебпака протипизирован TS, разбит по папкам, файлы в которых имеют семантичные имена. Также там описан отличный вариант проброса env-параметров, рецепт параллельной сборки для фронта и для сервера, запуск сервера после билда.

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

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

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

export const messages = wrapMessages({
  hello: 'Hello {user}',
})

а айди формируется автоматически из __dirname. Для экспорта в любую систему локализации - либо пробежаться по всем этим файлам простой регуляркой, либо выполнить во всех этих файлах wrapMessages, подменив ее функционал на JSON.stringify. Так получится JSON с [{id: 'pages/user/accordion.hello', value: 'Hello {user}'}] , что можно загрузить в любую систему локализации. В коде в соответствии с веткой и локалью запрашивается готовый JSON и кладется в стор, который уже является источником правды для текстов в коде. Если там значения нет - то выводится дефолтный текст из объекта выше.

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

JSON бандл как правило приходит через систему локализации, обычно удобнее получать данные через запрос к ней, а не загрузкой JSON, так как тексты - ответственность не разработчиков, а контент-менеджеров, поэтому в коде лучше держать только константы с базовым примером текста.

Только английский как основной не всем подойдет, это очередное ограничение.

$my_sign_in_reset_label - это что? Доллар-моя-подпись-в-сбросе-надписи? Доллар зачем, название страницы зачем? Если "для уникальности" - то пусть об уникальности заботятся инструменты.

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

Переопределение стилей делается добавлением класса к компоненту, в Реакте например через проп <MyComponent headerClassName={styles.headerGreen} />, в самом компоненте в его class будет два класса - дефолтный и переданный, так не сломаются псевдоклассы.

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

То, что не приходится писать {styles.table} - значит фреймворк решает за тебя, как будут называться классы, что является очередным ограничением. Кто-то может это назвать удобством - но нет, работа с именованиями классов это отдельное искусство, цель которого - дать ясное представление о структуре элементов на сайте и их взаимосвязях. Для этого нужна вложенность и семантика. Безиерархичные стили и автогенерируемые классы ухудшают читаемость и понятность.

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

Современные браузеры грузят ресурсы параллельно, и размер CSS как правило мал, особенно оптимизированного и сжатого, поэтому стили грузятся раньше скриптов, и нагрузка на браузер как раз снижается.

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

Ясный и понятный код у каждого свой. У одного это $.$$ { this.$ }, у другого yield* pipe(partial(sum, 1), multiply(3)).stream(), у третьего await StreamPipeUserControllerManager.emit('SuperLongNameFnFromThatModule'), у четвертого - императивный. И у каждого свое форматирование, табуляция, "лучшие практики", нейминг, длина строк, вложенность и т.п. Правила нужны для того, чтобы код оставался консистентным, согласованным и каждый человек не переписывал код другого человека под свое понимание понятности.

VSCode лучшая только для тех, кто к ней привык, и делать только для нее - как раз "заставлять ходить в брюках и рубашке", как собственно весь $mol и заставляет. "Только так, это самое лучшее, все остальное дно". Нет, не лучшее, а видение одного человека, опыт которого отличается от опыта всех остальных разработчиков.

Запрет на console.log в линтере - отличная практика, при необходимости сверху просто ставится // eslint-disable-next-line no-console. Увеличивает внимательность сотрудников, охраняет чистоту консоли. Не доводилось работать в проектах, которые при открытии консоли выводят несколько страниц кому-то когда-то нужного текста для разработки? Мне доводилось больше, чем несколько раз.

Кастомизируемость библиотек - это важно, рад, что в $mol есть хорошо кастомизируемые. Проблемы с библиотеками для Реакта бывают, и иногда весьма сложные. Но справиться можно со всем, изредка дописывая кастомные компоненты, это часть разработки.

Порядок и консистентность в файловой структуре - это отлично, только он должен создаваться соглашениями в команде, а не навязываться кодом. Код - продукт который делает команда, а не клетка, в которой они сидят. Подходы к форматированию файлов и паттерны написания кода - тоже корнями уходят в опыт и предпочтения команды. Хотя жесткие фреймворки освобождают от рутины синхронизации мнений, это подходит не всем. Я обычно высказываю в комментариях и статьях свое мнение о том, как должно быть все устроено, но в команде гибок и открыт для предложений, часто устраиваю глобальный рефакторинг проектов для того, чтобы он более соответствовал требованиям бизнеса и запросам коллектива. С $mol либо так, как видит его создатель, либо никак. С Реактом как правило всех устраивает html-подобный синтаксис и жизненный цикл, а все остальное - гибкое.

Отсутствие сокрытия состояния и глобальный доступ - это и есть глобальность. Когда можно вызвать тултип или нотификацию из любого места, провалидировать форму до перехода на другую страницу, сделать серию модалок, каждая из которых будет иметь доступ к данным предыдущей. При необходимости, разумеется. Если такое есть в $mol то может быть в этом пункте мы сходимся, только для меня состояние - это observable объект, а "канал" - функция изменения этого объекта из любого места. Каждое изменение рассылается тем компонентам, сайд-эффектам и геттерам, которым оно нужно. Этих концептов хватает для построения приложения любой сложности.

По поводу подходов к хранению состояния, подпискам на него и методов работы с жизненным циклом Flux, HOC, Redux, Hooks, MobX - лестница развития фронтенд-разработки. Где-то ступеньки приводят не туда, где-то возвращают на путь к идеальной архитектуре, и конца этого пути пока не видно - мы свидетельствуем начало кроссплатформенного программирования интерфейсов, и недостатков много и в CSS+HTML+JS, и в подходах с их использованием. Вполне возможно, что и вы со временем кардинально пересмотрите подход к фронтенду, по мере развития технологий всеобщими усилиями.

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

А как вы генерируете из разметки json-файл, если объявляете тексты по месту использования? Он попадает туда в шаблонизированном виде, который удобен для добавления склонений, множественного числа, переводов строк, форматов дат? Суть объявления в отдельных файлах как раз удобство генерации id, преобразования в json и контролируемых шаблонах, которые можно отредактировать в системе локализации.

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

Кеш можно и не перегружать, но корректный hot-reload без дополнительной возни обязателен.

Тексты в разметке - всегда боль. Когда они хранятся в отдельных ts-файлах, как я сказал, можно их прогонять через любой шаблонизатор с нужными функциями, а при удалении ключей ругнется TS, при добавлении же неиспользуемых - ESLint. Так ли приятно видеть скажем проект, в разметке которого масса арабских или китайских слов? Но всегда приятно, когда видишь семантическое `{getLn(messages.buttonReset)}`. И в сторах, и в разметке, и в экшенах, и в сайд-эффектах это основа проекта, нацеленного на распространение в другие страны.

Семантичные имена классов с гарантией отсутствия пересечений - CSS Modules, где класс `.table` будет преобразован например по маске `[folder]_[name]-[hash:4]` и в итоговом коде будет `.ModalProduct_table-1234`. При этом в коде это будет просто `{styles.table}` с любым семантичным ключом, не ограниченным фреймворком. Тайпчек для css - однозначно лишнее, все проекты, в которых мы пробовали типизировать стили, в итоге приходили к мысли о бесполезности этого.

Точка отказа в файле стилей - это что-то новенькое, не сталкивался ни разу. CSS-файл обычно загружается параллельно с js и выполняется раньше него, улучшая метрики сайта. Также благодаря постпроцессорам типа CSSO стили эффективно преобразуются и размер их кардинально уменьшается, а за счет параллельной обработки браузером увеличивается скорость при открытии других страниц и при добавлении элементов на страницу, когда браузер уже знает как отображать новый элемент, а не получает инлайновый `style`.

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

VSCode - один из многих IDE, намного эффективней выносить правила в ESLint, который поддерживается широким спектром редакторов и может выполняться на CI для проверки консистентности форматирования и стиля кода.

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

Мощь готовых библиотек в соответствии функционала бизнес-требованиям. Широкий выбор библиотек и компонентов очень важен.

Свобода в организации файлов мне нужна для того, что жесткий подход в именовании файлов и их хранении - ограничение, которое может стать проблемой, как в том же Next.js, не позволяя совершенствовать архитектуру.

До каналов и инверсии я не добрался, но безусловно называть глобальные сторы и экшены непотребством - непрофессионально. Я разрабатываю в трехуровневом стиле: глобальные-модульные-локальные, из которых локальные - практически не используются, так как есть масса задач, когда контролировать компоненты требуется из-вне и часто на самом верхнем уровне (тот же онбординг).

Я понимаю, что все мы стремимся к идеальной архитектуре кода, но на мой взгляд вы давно свернули не в ту сторону, даже в самых базовых вещах, которые на мой взгляд и новичку должны быть понятны, мы расходимся. Как бы хорошо $mol ни работал в маленьких примерах и бенчмарках - в целом концепт очень навязчивый и ограничивающий, в стиле "а мы пойдем своим путем" вместо того, чтобы развивать и развиваться вместе с общим потоком фронтенд-разработки.

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

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

Стили в TS малопривлекательная идея, т.к. реимплементируют функциональность, которая проработана в многочисленных препроцессорах и плагинах. Styled-components как вариация - это ад. Стили должны быть отдельными для эффективного кеширования и не включаться в скрипты, раздувая их размер и ухудшая перфоманс.

Генерируются js-файлы из стилевых и шаблонных. Сами стилевые и шаблонные имеют синтаксис, вряд ли поддерживаемый ESLint, Prettier, Stylelint. Опора на .editorconfig в проектах, в которых участвует не один разработчик не приводит к консистентности и не налагает достаточного количества ограничений. Переформатирование же сгенерированных файлов на precommit приведет к большим расхождениям между тем, что локально у человека, и тем, что в гите.

Про http имел в виду что таков дефолтный протокол для localhost - возможно, во фреймворке есть возможность включить https, но я, как говорил, не добрался до более глубокого изучения. Нужно это для одинаковой среды между локальным сервером и стендом.

Стандартная библиотека компонентов подойдет для маленькой CRM какой-нибудь студии, в крупных проектах требуется мощь Material UI или Antd, которые экономят громадное количество времени на разработку и легко интегрируются в Figma/Zeplin дизайнерами.

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

Попробовал запустить локально, но наткнулся на море багов. Все по туториалу сделал, перезапускал сервер и перезагружал страницу несколько раз - пустая. Потом удалил папку my, внезапно страница my/hello заработала. Пересоздал папку с другим неймспейсом - пишет что найти неймспейс не удается. Любые изменения в файлах не подтягиваются. Создал чистую папку с инсталляцией чистого hyoo-ru/mam.git, почему-то локальный сервер стал показывать что папка my/hello существует и видимо из какого-то кеша подгружает файлы из соседнего проекта. Дальше возиться смысла не вижу, пока версия библиотеки не станет достаточно протестированной (у меня свежий linux и море проектов на других фреймворках, которые запускаются без багов и танцев).

Тем не менее, посмотрел базовое описание и примеры - в них мне явно не подходит:

  • локализация в json, нужна ts-типизированная

  • процессинг стилей через postcss-cssnext вместо CSS Modules с кастомными плагинами транформации

  • дефолтное форматирование без ESLint и Prettier с кастомными энтерпрайзными правилами, прикрутить их будет непросто, т.к. много кода автогенерируется

  • http протокол локальной разработки

  • много автоматизированной "магии". Как правило мне нужен только рендерер в DOM с лайфциклом и совместимость с большим количеством opensource библиотек (компоненты, графики, таблицы, визуальные редакторы и т.п.). Архитектурные моменты с нужным функционалом как правило просто прикручиваю свои с дописыванием коннектора к рендереру.

  • негибкая структура файлов

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

Вот в этих комментариях описывал:

https://habr.com/ru/post/534632/comments/#comment_22459664

https://habr.com/ru/company/ruvds/blog/507518/comments/#comment_21763020

https://habr.com/ru/company/skillfactory/blog/539068/comments/#comment_22619868

https://habr.com/ru/post/565904/comments/#comment_23221032

https://habr.com/ru/post/586526/comments/#comment_23656254

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

До него так и не добрались руки, как вижу this.$ \>= и т.п. , сразу в конец списка приоритетов по просмотру идет. Не то, чтобы было сложно разобраться - но семантичность это крайне важно для проектов, в которых участвую. Как и распространенность инструмента, экосистема, опыт разработчиков в этой системе. Также $mol - это комплексный фреймворк, я же ориентируюсь на кастомные решения в каждой из отдельных областей (рендеринг, роутинг, ssr, локализация, генерация, организация файлов и т.п.), потому что часто требуется кастомный функционал.

Но это все пальцем по воде, в бою ваш фреймворк не использовал, поэтому нет какого-либо сформированного мнения. Но попробую, когда будет свободное время.

Пожалуйста - https://habr.com/ru/post/450360/ . Уже 3 года назад Redux и Styled-components были глубоким легаси, которые только затрудняют развитие приложения.

Спустя 3 года и десятка энтерпрайз-проектов на подобной архитектуре я стал из ярого противника убежденным сторонником TS, а из сторонника реакт-хуков их ярым противником. Но React, MobX, CSS Components, Webpack и Babel прошли интенсивную проверку боем и продолжают на данный момент оставаться лидерами для старта энтерпрайз-проекта.

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

Поэтому мои личные рекомендации - использовать для старта нового проекта именно этот стек + TS. Но подходы в той статье довольно устарели на мой взгляд, сейчас топлю за модульную архитектуру (lazy-load скриптов для каждой страницы, каждая из которых содержит модульные сторы и экшены), так как это лучше масштабируется при большом количестве страниц и разработчиков. Поэтому подумываю о сиквеле той статьи, но для этого сначала нужно завершить разработку вспомогательных библиотек.

Идея-то неплохая, если SPA без SSR, но есть ряд сайд-эффектов.

Во-первых, невозможно отловить из другого компонента, что модалка открыта и закрыть ее - нельзя вызвать context.actions.hideModal. Соответственно, не выполнить какую-то логику при открытой модалке - например, я в форме внутри модалки могу вызвать context.actions.shakeModal при ошибке валидации, а тут непонятно - открыта она или нет, и не открыт ли второй инстанс.

Во-вторых, как вы это сериализуете если есть SSR? При хранении в глобальном сторе `modals: [{ message, type }]` это легко сериализуется, а если хранилище открытости локальное - все намного сложнее.

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

В-четвертых, afterHideCallback удобнее определять при открытии, а не при закрытии - если например используется в целях онбординга (симуляция пользовательских действий). Да и нужных данных при вызове закрытия может не быть.

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

Почему не позволит? В HOC можно сделать такие же propTypes, как в обернутом компоненте, просто если они не переданы использовать дефолтные значения return <OriginalComponent prop1={props.prop1 || 'default value'} />

Да, с половиной я работал, и с Firebase для no-server сайтов. Речь о том, насколько разное взаимодействие с одной и той же базой на разных серверных языках. Вряд ли переход на другой язык потребует внезапно изменить базу одного типа на другую — скорее всего останется та же самая, а переход на другую будет по причинам, не зависимым от языка.


Во фронтенде используется хранилище в оперативной памяти, взаимодействие с которым реализуется через state manager, который у разных фреймворков, как правило, кардинально различается, и в этом плане все сложнее.

SQL синтаксис запросов и работа с базой разве сильно отличаются? Если используются ORM, только в синтаксисе различия, поэтому это не в счет сложности миграции. Кафки и рэббиты тоже те же самые, адаптеры как правило пишутся в сопоставимом формате — разве нет? Та же история про целостность и непротиворечивость — паттерны реимплементируются. Конечно, будут некоторые локальные особенности и ограничения подходов в фреймворках на других языках — но концептуально все, в моем понимании, то же самое.


А вот многопоточность да, может очень сильно различаться. Если не прав, буду рад более подробному объяснению — сам только на php и node.js фулстачил, да на python по мелочи дорабатывал — каких-то концептуальных отличий не видел, кроме ноды — там можно писать изоморфный код бэк-фронт, делать единую схему моделей и валидаций апи бэк-фронт. В этом смысле удобство и переиспользуемость шикарные, но производительность по сравнению с более низкоуровневыми серверными языками оставляет желать лучшего (судя по обзорным статьям), хотя и на ноде под танком 1000 запросов / сек для ряда кейсов сервер за 10мс секунд стабильно отдавал ответ, если задача не требует подключения к базе. Существенные различия по скорости только в сложных сценариях, включающих походы в разные базы и микросервисы. Но у нас речь не о скорости, а о сложности перехода в бэке с одного стека на другой — и тут мой опыт говорит, что это намного проще, чем на фронте.

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


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


Даже если знаешь, как идеально все организовать с точки зрения логики — подобрать подходящие библиотеки и написать аналогичную систему на другом стеке — дело не месяцев, но лет, путем нескольких масштабных итераций рефакторинга. Для чего это нужно, если на своем стеке решишь бизнес-задачи за N времени, а на новом за N*X? Если люди приходят "получить новый опыт, расширить свои компетенции и прокачаться" — это хорошо, но компаниям нужно совершенно не это, а качественное и быстрое решение задач. Поэтому на вакансии на других стеках я даже не смотрю — самокритик меня живьем съест, если буду делать задачи за значительно более долгое время, не смогу планировать сроки даже приблизительно, и качество значительно упадет. Считаю, что это более ответственный подход, чем "пойду не знаю куда и будь что будет, там разберусь".

Попробуйте, должно понравиться — бойлерплейта в разы меньше (по факту только observer и проброс контекста, в остальном работа со стором — как с обычным объектом, только реактивным).


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


Это не значит, что не умею "готовить" функциональные компоненты и грамотно выделять кастомные хуки — просто с классами работать намного удобнее и организовывать код в них проще.


БЭМ — это один из вариантов решения проблемы глобальной области видимости, когда создаются околоуникальные именования (хотя нередко они все же пересекаются и возникают баги). Минусы — длинные названия, нет автодополнения, нет быстрого перехода в стилевые файлы на конкретный класс, сложнее отслеживать наличие неиспользованных или отсутствующих классов, сложнее придумывать названия. Так как в реакт-проектах практически везде используется сборщик, то намного эффективнее использовать CSS Modules и получать все преимущества — автоматические суффиксы и префиксы по названию файла в наименованиях (исключают возможность пересечения), быстрые переходы сразу на нужный класс в стилях, автодополнение, возможность проверки неиспользуемых или отсутствующих классов. Это намного удобнее.

Да выбросьте вы из головы Redux, крайне неэффективный стейт-менеджер. Понимаю, что взяли его скорее для опыта и понимания работы легаси-проектов, но на том же MobX сделать намного проще. Вот пример реализации на нем + TS.

Импорты и типы

import _ from 'lodash';
import axios from 'axios';
import { observer } from 'mobx-react';
import { autorun, observable, runInAction, toJS } from 'mobx';
import { ChangeEvent, Component, ContextType, createContext } from 'react';
import Selectable from 'selectable.js';

import styles from './Table.scss';

type TypeStarship = {
  url: string;
  name: string;
  crew: string;
  model: string;
  length: string;
  edited: string;
  created: string;
  passengers: string;
  consumables: string;
  manufacturer: string;
  vehicle_class: string;
  cargo_capacity: string;
  cost_in_credits: string;
  max_atmosphering_speed: string;
};

Контекст для прямого доступа из дочерних компонентов

// eslint-disable-next-line @typescript-eslint/naming-convention
const StarshipsStore = createContext<{ starships: Array<TypeStarship> }>({ starships: [] });

type TypeStarshipsStoreContext = ContextType<typeof StarshipsStore>;

class ConnectedComponentStarship<TProps = any> extends Component<TProps> {
  static context: TypeStarshipsStoreContext;
  static contextType = StarshipsStore;
  declare context: TypeStarshipsStoreContext;
}

Верхняя обертка с пробросом контекста

export class App extends Component {
  render() {
    return (
      <StarshipsStore.Provider value={observable({ starships: [] })}>
        <Table />
      </StarshipsStore.Provider>
    );
  }
}

Верхний компонент таблицы. В нем загружается контент, набрасывается Selectable и трекаются измененные значения в ячейках

@observer
class Table extends ConnectedComponentStarship {
  localState: { prevStarships: Array<TypeStarship> } = observable({
    prevStarships: [],
  });

  select: any;
  trackDisposer: IReactionDisposer | null = null;

  componentDidMount() {
    void axios({
      method: `get`,
      url: `http://swapi.dev/api/vehicles`,
    }).then((result: any) => {
      runInAction(() => {
        this.context.starships = result.data.results;
        this.localState.prevStarships = result.data.results;
      });

      this.select = new Selectable({
        appendTo: `.${styles.table}`,
        filter: `.${styles.tableCell}`,
        autoRefresh: false,
        lasso: {
          border: '1px solid blue',
          backgroundColor: 'rgba(52, 152, 219, 0.1)',
        },
        ignore: [`input`],
      });

      document.addEventListener(`keydown`, this.escKeyDownHandler);

      this.trackChangedStarships();
    });
  }

  escKeyDownHandler = (evt: any) => {
    if (evt.key === `Escape` || evt.key === `Esc`) {
      evt.preventDefault();
      this.select.clear();
    }
  };

  trackChangedStarships = () => {
    this.trackDisposer = autorun(() => {
      if (_.isEqual(this.localState.prevStarships, this.context.starships)) return;

      const changedStarships = _.differenceWith(
        this.localState.prevStarships,
        this.context.starships,
        _.isEqual
      );

      changedStarships.forEach((starship) => {
        console.log('starship data changed', toJS(starship));
      });

      runInAction(() => {
        this.localState.prevStarships = toJS(this.context.starships);
      });
    });
  };

  componentWillUnmount() {
    document.removeEventListener(`keydown`, this.escKeyDownHandler);
    this.trackDisposer?.();
  }

  render() {
    const { starships } = this.context;

    const renderedKeys: Array<keyof TypeStarship> = [
      'cargo_capacity',
      'cost_in_credits',
      'max_atmosphering_speed',
      'name',
    ];

    return (
      <div className={styles.table}>
        <TableHeader renderedKeys={renderedKeys} />
        {starships.map((starship, index) => (
          <TableRow key={index} starship={starship} renderedKeys={renderedKeys} />
        ))}
      </div>
    );
  }
}

Заголовки и строки таблицы. Добавил динамический вывод столбцов, по сравнению с оригинальным кодом

@observer
class TableHeader extends ConnectedComponentStarship<{ renderedKeys: Array<keyof TypeStarship> }> {
  render() {
    const { renderedKeys } = this.props;

    return (
      <div className={styles.tableRowHeader}>
        {renderedKeys.map((param) => (
          <div key={param} className={styles.tableCell}>
            {param}
          </div>
        ))}
      </div>
    );
  }
}

@observer
class TableRow extends ConnectedComponentStarship<{
  starship: TypeStarship;
  renderedKeys: Array<keyof TypeStarship>;
}> {
  render() {
    const { starship, renderedKeys } = this.props;

    return (
      <div className={styles.tableRow}>
        {renderedKeys.map((param) => (
          <TableCell key={param} starship={starship} param={param} />
        ))}
      </div>
    );
  }
}

Ячейка. Если выбрано несколько ячеек, то в хранилище целевой корабль ищется по url, значения мутируются одним батчем благодаря runInAction, в отличие от оригинального решения, где много последовательных dispatch. Если выбрана одна ячейка, значение просто мутируется.

@observer
class TableCell extends ConnectedComponentStarship<{
  starship: TypeStarship;
  param: keyof TypeStarship;
}> {
  handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { starships } = this.context;
    const { starship, param } = this.props;

    const selectedFields = document.querySelectorAll(`.ui-selected`);

    if (!selectedFields.length) {
      runInAction(() => (starship[param] = event.target.value));

      return;
    }

    runInAction(() => {
      selectedFields.forEach(({ dataset }) => {
        const targetStarship = starships.find((s) => s.url === dataset.url);

        if (targetStarship) targetStarship[dataset.param] = event.target.value;
      });
    });
  };

  render() {
    const { starship, param } = this.props;

    return (
      <div data-url={starship.url} data-param={param} className={styles.tableCell}>
        <input onChange={this.handleChange} value={starship[param]} type={'text'} />
      </div>
    );
  }
}

Также исправил ряд ошибок оригинального кода, но все равно это все - чисто для примера, в реальном проекте нужен будет ряд доработок (обработка ошибок, валидации ответа запроса, слой запросов, вместо нереактового Selectable я бы предпочел кастомное решение, без необходимости делать querySelectorAll) и т.п.

В итоге обслуживающий код - минимальный, ответственность разделена (ячейки изменяют данные в хранилище, а высокоуровневый компонент Table независимо отслеживает измененные данные и может вызывать сохранение в базу). Благодаря этому можно изменять значения из-вне (по клику на кнопки вне Table, из модалок и т.п.).

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

Также присмотритесь к CSS Components вместо строкового указания глобальных классов. К теме статьи не относится, но видеть БЭМ в 2021 как-то очень удивительно.

Чтобы сделать изменение только одной колонки, достаточно добавить фильтр

selectedFields.filter(({ dataset }) => dataset.param === param)

"Я про случай когда единственный повод не использовать CDN для разработчика — лень скачивать и собирать библиотеку"


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


Остальные минусы вида "на случай работы в дороге", "дополнительный поиск DNS" и "удаление древнейших версий библиотек" локальны и их можно обойти.


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

Под "блокирующим" я подразумевал ts-loader без transpileOnly, т.е. билд не будет собираться, если есть ошибки в типах — это на удивление распространенный, но на мой взгляд неэффективный подход. Под неблокирующим имел в виду как раз ваш вариант.


Сам люблю часто рефакторить, но тут все зависит от архитектуры. Если в приложении нет связки по строкам и динамических названий структур, то в 95% случаев достаточно делать findUsages в IDE и при рефакторинге интерфейсов и параметров все места легко открыть и поправить. Поэтому ситуация, когда на pre-commit ругается ts у меня крайне редкая, и в целом намного удобнее использовать IDE чем потом в консоли смотреть, что развалилось, и вручную переходить на каждый файл и искать строчку с ошибкой.


Если архитектура не подходит и findUsages не работает корректно — тогда да, ваш вариант имеет место быть, и поможет найти развалившиеся места.

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Дата рождения
Зарегистрирован
Активность