Похоже, вы не знакомы с концептами SPA и SSR — почитайте. На сервере делается выполнение js-кода и конвертация в html-строку со всем размещением данных, этот html отдается клиенту, и он "гидрирует" то, что получил от сервера и то, что ожидают скрипты, выполняющиеся на клиенте. Это достаточно новый концепт, но он действительно значительно ускоряет загрузку клиентского интерфейса — я описывал выше.
И как же другие серверные языки могут выполнить js-код, на котором написан клиент, чтобы сформировать html? Разве что с помощью виртуальной node-среды, так это то же самое. Я сейчас говорю про SPA, а не про древнюю схему, когда на сервере хранятся html-темплейты и реплейсом заменяются какие-то части, а соответственно при переходе на новую страницу делается полный рефреш страницы либо по аяксу загружаются куски готового html и заменяются — эта схема давно устарела, у нее громадное количество недостатков.
Если вопрос про SSR то да, нода позволяет изоморфно выполнять js-код и ряд фреймворков может компилироваться в готовый html. Асинхронные запросы за данными тоже могут выполняться на сервере, предоставляя в json-формате готовые структуры. Если бэк написан на другом языке в соседнем докере — то делать запросы к нему можно эффективней, чем с фронтенда, напрямую в его докер. Если он написан на ноде — то еще быстрей, фактически только в базу сходить. Сюда добавляется эффективное кеширование, и в итоге если раньше SPA делало 5 запросов на клиенте после обработки JS, то есть это время плюсовалось и стадия отрисовки и тем более интерактивности наступала с большой задержкой, то с подобной схемой при первом открытии страницы нужно 0 запросов и готовый html получается при первом запросе, соответственно значительное ускорение. В энтерпрайзе такая схема уже четвертый год используется, по крайней мере в тех проектах, в которых я участвовал — все метрики перфоманса действительно значительно улучшаются.
Минус — больше серверных ресурсов уходит на рендеринг, но это вполне горизонтально масштабируется. Количество запросов в секунду у ноды тоже стало приемлемым, при необходимости, опять же, балансируется. Но для хайлоада все же лучше оставить в ноде только веб-часть, а требовательные к перфомансу сервисы писать на более низкоуровневых и многопоточных платформах.
Плюсанул за последний абзац — ровно 5к и 1-в-1 описание, пару лет так работаю на зарубежные компании. И бывшие коллеги точно по такой же схеме, так что максимально жизненно. Выше ставка только по проектной работе бывает — на 2-3 месяца, а с учетом простоя то на то и выходит.
Здорово получилось, действительно что-то новое, шустрое и почти юзабельное. По старой привычке переписал примеры для себя на mobx — намного меньше кода получилось и он полностью лишен бойлерплейта, кроме вызова makeAutoObservable в конструкторе, так что все-таки в проекты не потащу — там скорость восприятия кода и явность взаимосвязей намного важнее сэкономленных килобайтов, но для пет-проджекта для развития нового восприятия работы с данными — вполне можно.
Об одном моменте задумался — генераторы же не полифиллятся, но судя по caniuse поддерживаются 95.41% клиентов, значит вполне уже можно в прод, если не для госконтор и азиатского рынка. В общем — годно.
Повторюсь, бутстрап и другие готовые системы стилей — для приложений, в которых не важен дизайн, а важен функционал. По ссылке же своя дизайн-система (отступы, шрифты, возможно точки адаптивности, цвета, иконки и другие элементы). Если набутстрапить и переписывать импортантами все — это не то что ускорение и увеличение удобства разработки и стабильности интерфейса, а полная деградация. Да, приходится тащить эту недотелегу в гору вместо того, чтобы выполнять задачу эффективно и чисто.
Второй момент — в подобных приложениях верстка занимает от силы 5% времени фронтендера, остальное — архитектурные вопросы и js, так как подобное пишется на фреймворках для более высокой скорости работы, разбиения по компонентам, переиспользуемости, стабильности UX. Если туда добавить готовую дизайн-систему с хаками, можно однозначно сказать, что хорошо не получится.
Сейчас макеты выгружают в Figma / Zeplin, они там в векторном формате. При клике на элемент эти инструменты сами генерируют css стили — размеры, цвета, шрифты, остается просто скопировать к себе и настроить раскладку блоков и адаптивность. Иконки или растровая графика парой кликов выгружаются в svg или jpg, в общем, верстать стало намного проще.
Раньше да, приходилось из Photoshop / Illustrator / Corel Draw самостоятельно вычислять размеры, черри-пикать цвета, сложнее определять отступы от элементов и сложнее экспортить иконки / растровые изображения.
Конструкции #{выражение} действительно страшновато выглядят, но в реальных приложениях годы назад только такое видел, а сам писал последний раз в 2014 типа таких систем раскладок (LESS)
Здесь всего лишь задается всем элементам одинаковая высота и вертикальный отступ — пригождается в компонентах нотификаций и галереях, чтобы друг под другом находились элементы и не сразу "прыгали" вверх при удалении, а с анимацией. Но в стилях это можно только статично описать, то есть если высота элементов разная — уже работать не будет, поэтому подобные вычисления производятся в js. В общем, не пригодится.
А те миксины, про которые я говорил — простейшие наборы статичных параметров типа .inline-block() { vertical-align: top; display: inline-block; }, такие вот небольшие кирпичики. Динамические параметры в стили сейчас передаются с помощью Custom CSS Variables, то есть в стилях пишешь .myClass { color: var(--main-color) } и затем при изменении этого свойства у родительского элемента этот чайлд перекрасится. Монструозные либы с кучей логики как-то ушли в прошлое и только на простых сайтах используются, ввиду сложностей кастомизации.
Так с любыми библиотеками — при необходимости кастомизации код обрастает хаками, затем библиотека форкается и собирается модифицированный билд, затем при изменении требований времени на возню в большой чужой кодовой базе уходит все больше, а стабильность становится все хуже, так как приходится поддерживать обратную совместимость, что применительно к интерфейсам довольно сложно.
Со временем за небольшой срок разработчик учится делать свою библиотеку миксинов для библиотеки компонентов, что в поддержке проще и намного гибче. Поэтому лет 10 я уже не использую стилевые фреймворки, а готовые библиотеки компонентов — только для админок, где по бизнес-требованиям важен не дизайн, а функционал.
То есть берется инструмент запросов с неявным внутренним хранилищем, неявно там что-то нормализуется и преобразовывается с помощью мидлвары… Для остальных интеракций, видимо, будет заведен другой стейт-менеджер, и каким-то образом нужно будет синхронизировать с этим react-query… Что-то вообще не понимаю смысла всего этого.
Я мыслю в категориях api.getUser(user => store.createOrUpdate(store.user, user)) и render() { return <>{this.context.store.users}</> } с разделением слоев апи, модификаторов стора, хранилищ и реакт-компонентов, отвечающих за View. Запросы из компонентов и параллельные сторы, неявные трансформации и технические параметры в данных — это для меня другой мир...
"Отсутствие строки лучше чем ее наличие" — так же относится и к двум дополнительным параметрам, дополнительному слою, дополнительной библиотеке и документации. Этих строк с partialAssign будет ну пара десятков на среднее приложение, а размер библиотеки, включенной в бандл и код обвязки займет явно намного больший объем, увеличив и количество поддерживаемых компонентов системы. Поэтому аргумент звучит неубедительно.
Я очень сомневаюсь, что это стоит 0, вообще-то это все довольно дорого.
Как я понял, в целом эта система нужна, если одинаковые запросы шлются из разных компонентов и данные складываются в разные хранилища, и именно для этого нужны "глобальные id для всех сущностей". В реальности за несколько лет мне не попадалось таких "архитектур" — если данные используются в нескольких компонентах, то они выносятся в глобальное хранилище (redux, mobx) и их обновление происходит явно, id нужен только для partialUpdate. Поэтому я так и негативно отнесся к предложениям в статье. Но если действительно приложение так построено, что данные и методы их получения многократно дублируются и раскиданы по локальным сторам, то подобная нормализация, безусловно, нужна. Как нужно и переписывание архитектуры ядра, на мой взгляд.
"для разработчика ничего не меняется в работе, ни с получением данных" — вот это все равно звучит очень странно. Нормализованные или ненормализованные данные все равно же надо положить в хранилище, не будет же iresine этим заниматься? Или подразумевается, что все полученные данные вместо redux или mobx будут храниться в некоем iresineStore и напрямую получаться оттуда вместо других хранилищ без системы синхронизации с основным стором?
По мне так строчка this.users.set(user.id, user) выигрывает по сравнению с созданием слоя нормализации с эвент-эмиттером, изменением большого количества сущностей с добавлением лишнего type, большого оверхеда по обучению команды и поддержанию единообразия. И в итоге идеально все равно не получится — будут легаси-ручки бэка или сторонних интеграций, данные в которых не поменять, и "нормализация" кашеобразно распараллелится на фронт и бэк. Да и в самом фронте соблюдать получится только при создании нового проекта, так как внедрять подобный слой в существующую кодовую базу дело недель работы без какого-либо бизнес-выхлопа, кроме негативного — увеличения сложности поддержки.
Статью внимательно прочитал, но так и не понял, зачем это нужно. Хранилища в памяти с ключом по id в моей практике пригождались только 2 раза — при получении частичных данных (например, user) в десятке разных ручек (но выбранный подход с нормализацией оказался в итоге не самым эффективным, добавив значительный оверхед) и при необходимости скорости доступа О1 в требовательных к перфомансу интерфейсах при наличии 10к+ элементов в массиве. В остальных случаях store.update(store.users, user), где update соответствующей стратегией находит целевой элемент или создает новый, идеальный вариант.
Спасибо за статью. В предлагаемой на MobX архитектуре, хотя код в этой первой части и не приведен, прослеживаются некоторые спорные с моей точки зрения моменты:
Объединение store, actions (лучше их называть store modificators, чтобы не путать с редаксовыми) и selectors в единый слой. Те данные и методы, с которыми работают фронтендеры — не микросервисы и не игровые сущности, для которых группировка подходит четко, и Dog может run() и можно get condition() исходя из внутренних параметров. АПИ своего бэка или стороннее может быть так спроектировано, что содержит смешанные данные, которые придется разложить по десятку семантических сторов, поэтому фича редакса с возможностью по одной константе отлавливать payload в разных подсторах очень востребованна.
Касательно селекторов — то же самое, часто необходима комбинация данных из нескольких семантических сторов, и get canShowSomeBlock не будет принадлежать ни стору User, ни UI, ни Forms, а данные, необходимые для вычисления данного параметра, могут браться из них всех. И тут на сцену выходит либо Dependency Injection со всей сопутствующей кашей и полным уничтожением семантичности и консистентности хранилищ, либо все же создается отдельный слой.
Если же модификаторы и селекторы признать отдельным слоем и передавать в них весь store со свободой модификации и чтения данных из любого, то получаются простые для понимания и использования слои с собственной структурой, продиктованной не архитектурой хранилищ, а реальными вариантами использования. За счет передачи всего стора получается удобная группировка без необходимости в модификаторах делать ручные DI и вызывать многочисленные методы.
Из недостатков описанного мной подхода — возможность раздувания этих функций-модификаторов хранилища, что решается более узкой их специализацией. Также многим, привыкшим к структурам типа Dog.run() не понравится отсутствие "явного разделения ответственности", когда функция может получать доступ к любому параметру без жестких ограничений, как и реактовые компоненты (через контекст) к любому экшену-модификатору или опять же параметру стора, что якобы дает разработчику "свободу действий", что обязательно выльется в лапшеобразный код с неявными взаимосвязями. На деле такое встречается намного чаще как раз при наличии ограничений, которые разнообразными хаками (DI, горизонтальные связи, параллельные системы доступа к данным, дубляж) преодолеваются и код становится не только лапшеобразным, но и неподдерживаемым — при желании его отрефакторить сломаться может что угодно. В случае же, если некий экшен, написанный юниором, использует данные из 4 подсторов сразу, хотя его можно было бы разбить на 2 более семантичных экшена, это рефакторится за минуты фактически без рисков поломки, и не является легаси, в отличие от хаков, обходящих основной архитектурный паттерн.
По поводу того, что в модификаторах не стоит вызывать асинхронную логику получения данных из сторонних источников. Тут, думаю, все же нужно подходить контекстуально — если данные юзера могут приходить только в одной ручке, то ничего плохого в том, чтобы написать
я не вижу. Если же данные юзера могут приходить и в других ручках, то можно создать более универсальный модификатор setUser = user => partialAssign(store.user, user), но не считаю это обязательным изначально, так как выглядит преждевременной оптимизацией и в общем случае приведет к созданию десятков лишних функций.
А сам сбор данных из сторонних источников в модификаторах считаю оправданным, так как иначе придется выделять еще один слой "подготовки данных перед вызовом модификатора", который всегда будет вызываться перед самим модификатором и в 99% случаев это будет дубляж. Оставшийся процент — кейсы, когда источником данных может служить не одна сторонняя система, а несколько, но это проще решить if условием в самом модификаторе, чем менеджерингом отдельного слоя предварительной нормализации данных. Либо можно таким вот promise-middleware паттерном
Конечно, export default — зло, подход выше — только для сторонних библиотек с большим набором потенциально неуникальных экспортов. Вот как раз для тех трех и подходит, ну и для нодовых built-ins. Вынесение этих библиотек в отдельный файл и реэкспорт переменных в контролируемом формате тоже нормальное решение, хотя и удлиняет цепочку импортов.
Мне ближе вариант с неймспейсами (но нужен хороший tree-shaking по использованным функциям в этом случае)
import _ from 'lodash';
import React from 'react';
import effector from 'effector';
_.clone();
React.useState();
effector.createEvent();
А поддержка старых браузеров обычно обеспечивается либо автоматическим полифиллингом, либо загрузкой уже скомпилированной библиотеки по наличию фичей в браузере.
Но подход интересный
Высказывать предпочтения не запрещено, наоборот, плюрализм мнений — это прекрасно, как и то, что занимаетесь обучением других людей. На собеседования действительно трачу много времени, сотня в год как минимум набирается, и пара десятков в качестве интервьюера — каждый сам выбирает, по каким критериям подбирать работу, у меня они достаточно специфичные, поэтому редко устраиваюсь, а подбираю месяцами. Забивать свободное время программированием мне уже не интересно, если есть желание — занимаюсь пет-проджектами. У вас другая история, много говорите про деньги в статье — для меня же куда важнее идейная и архитектурная составляющие, так что цикл поиска работы разный.
Про GraphQL верно — все нюансы его работы не знаю, как и сопутствующие библиотеки, ссылаюсь на мнение коллег, анализ их юзкейсов и статистику использования. Исходя из этого и сказал, что не рекомендовал бы начинающим изучать эту технологию, что подтвердили ваши слова про "мало кто это в рунете сейчас умеет". Из TS моделей для реста формировать валидаторы несложно, достаточно типов запроса-ответа, обработка ошибок и вариантов ответа бэка тоже не сложна, так что я бы начал обучение с этого — явно в подавляющем большинстве проектов используется он и будет актуален еще долго. Сколько времени займет изучение реста? Точно не больше, чем графовой системы запросов, старт прост как fetch(url), дальше — типизация, валидация, обработка. Когда человек, изучающий рест, дорастет до миддла? Может завтра, а может никогда.
Как я понял, цель этой статьи скорее самореклама, так как статей типа "как войти в айти" огромное количество, и многие более обоснованны. И то, что вы с чего-то начали рассказывать в комментариях, что хотите обучать и у вас уже много учеников, подтверждает. В целом не имею претензий, сказал только что у GraphQL малое распространение и много нюансов, а SC хайп из прошлого, который неудобен и неэффективен.
Про рефакторинг уже сказали, про xpath пока нет… Глобальный поиск по дереву типа #root div.Agreement div.checkboxWrapper input как раз усложняет "генерацию и адаптацию тестов к изменяемому коду", делает тесты нестабильными и привязанными к конкретной реализации. Поиск по уникальному идентификатору — единственный способ создать абстрагированные тесты, которые не потребуют исправления при рефакторинге, так как иерархичное DOM-дерево не является статичной точной опоры.
Про отсечение "лишних выдач и бэктрекинг" не понял, возможно имелись в виду статичные моки данных под первым, но бэктрекинг… Сложно догадаться, что под этим имелось в виду, и при чем тут React.
Вообще опасно заходить в JS топик на Хабре с рекомендациями, какие технологии изучать. Сообщество программистов достаточно четко разбилось по используемым фреймворкам и подходам, и явно многим не понравится то, что фокус ставится строго на определенные.
Я лично не советовал бы GraphQL, так как в реальном проекте не видел ни разу, и по собеседованиям в сотнях компаний только в паре он использовался — он создает большую нагрузку на бэк-разработчиков, в большинстве случаев ухудшает перфоманс и стабильность (права доступа, оптимизацию и кэш, стейт-машины по отдельным ручкам куда проще менеджерить), там сложно с валидацией, нестандартными данными. Также и от SC везде отказываются ввиду колоссального количества недостатков. Лучше бы посоветовать новичкам изучить CSS (+Modules) с PostCSS-плагинами, которые могут расширять его функциональность подо все нужды и классический REST + RPC с Socket, так как это намного более распространено и под те самые малые и средние проекты, о которых вы говорите, GraphQL интегрировать почти точно не будут. Если цель как раз зарабатывать деньги программированием, лучше учиться популярной классике, а не тому, что когда-то было на хайпе.
Интересно, почему ваш коммент минусуют… Возможно, из-за async/await? С ними сложно работать, так как уходишь в try-catch hell для имплементации того функционала, который был бы однострочным с Promise. Также сам концепт псевдосинхронного кода, когда не понимаешь (либо легко пропустить при чтении), что последующий код будет выполнен не сейчас, а в неопределенном будущем. В концепте MobX видел, как в not strict mode люди забывали писать runInAction (да, конечно я знаю о schedule, который может это автоматизировать — но не видел в проектах), что приводило к лишним ререндерам. В целом я придерживаюсь Promise — это простой инструмент с явным потоком в рамках многим привычного middleware паттерна, но без глобалок. Не отрицаю, что можно и с асинками писать грамотный код и путем составления best practice минимизировать ошибки, но сама формализация best practice — редкость, так что лучше уж Promise.
Может, поэтому минусовали… А так-то здравые рассуждения, пусть и немного эмоционально)
Статья называется "Кому с Redux жить хорошо", как я понял из содержимого — основной упор на обзор юзкейсов этого инструмента, а TDD используется как пример. Поэтому считаю свою критику вполне обоснованной, так как к комбинации диспетчер-экшены-константы-редюсеры-селекторы-контейнеры-ручной маппинг-иммутабельность-проброс через props с обновлением родителей — довольно много контраргументов. Это обсуждалось много раз на Хабре, и подавляющее большинство проектов (из тех, в которых я участвовал — все) перешли либо на MobX, либо на локальные стейты+контекст+hooks&функциональщину. Поэтому в статье стоило бы сделать вывод "Кому все-таки хорошо с Redux? Никому", а не "потом навертим Redux Tookilt и будет чуть менее больно".
Если вы бы хотели, как красиво пишете в комментарии, сделать пример работы через TDD в отрыве от конкретных подходов ("без всяческих усложнений", "инженерные практики", "качественный код", "не привязываясь к конкретному стеку", "нужно просто научиться писать тесты"), то не ставили бы статье такой броский заголовок, и не рисовали бы картинки "архитектуры" редакса и вот этот весь бойлерплейт. Поэтому говорить красивые слова про инженерию, предсказуемый результат и качество кода не стоит — статья в целом не об этом.
Про перевернутую пирамиду тестирования — юнит-тесты (основа пирамиды) — это не про интерфейсные компоненты, которые преобразуются в другую структуру (DOM) и работают в десятках разных окружений в комбинации со слоями позиционирования и стилей. Это про чистые функции (нормализация данных, модификация хранилищ) и редко — про сайд-эффекты. Отрендеренный в виртуальной Jest-среде компонент и то, что ему переданы корректные пропы говорит только об этом, а не дает никакой гарантии работоспособности. Если это защита функционала от случайной модификации другим разработчиком — то да, в этом кейсе работает, но и в равной степени мешает, так как часто приходится править тесты "для галочки". Для корректных типов данных есть TS, который справляется с задачей не хуже, а для всего остального — интеграционные тесты.
Допустим, есть бизнес-задача:
при активном состоянии чекбокса Agree кнопка Sumbit доступна, при неактивном — задизейблена.
Под эту задачу по TDD пишется интеграционный тест:
Этот код завязан на id, соответственно ему не важна реализация — React, Redux, Svelte, простой html. Также он проверяет, что эти элементы отрендерены, не скрыты стилями, интерактивны, не перекрыты другими слоями. Это как раз концептуальный TDD для автоматизированного контроля соответствия приложения бизнес-требованиям, и если бы вы говорили в статье про это, а не как навернуть в Redux-приложении бесполезные тесты для view-слоя, то код тестов был бы аналогичен.
То, что Jest "стабильнее и быстрее" работает — тут не поспоришь, хотя на хорошей машине headless-браузеры работают очень шустро. Только вот даром такие юниты не нужны, если компонент прост — то его и так легко спроектировать, если сложен — то почти всегда можно раздробить на части, так что эти "react-redux-юниты" и тут не помогут.
Похоже, вы не знакомы с концептами SPA и SSR — почитайте. На сервере делается выполнение js-кода и конвертация в html-строку со всем размещением данных, этот html отдается клиенту, и он "гидрирует" то, что получил от сервера и то, что ожидают скрипты, выполняющиеся на клиенте. Это достаточно новый концепт, но он действительно значительно ускоряет загрузку клиентского интерфейса — я описывал выше.
И как же другие серверные языки могут выполнить js-код, на котором написан клиент, чтобы сформировать html? Разве что с помощью виртуальной node-среды, так это то же самое. Я сейчас говорю про SPA, а не про древнюю схему, когда на сервере хранятся html-темплейты и реплейсом заменяются какие-то части, а соответственно при переходе на новую страницу делается полный рефреш страницы либо по аяксу загружаются куски готового html и заменяются — эта схема давно устарела, у нее громадное количество недостатков.
Если вопрос про SSR то да, нода позволяет изоморфно выполнять js-код и ряд фреймворков может компилироваться в готовый html. Асинхронные запросы за данными тоже могут выполняться на сервере, предоставляя в json-формате готовые структуры. Если бэк написан на другом языке в соседнем докере — то делать запросы к нему можно эффективней, чем с фронтенда, напрямую в его докер. Если он написан на ноде — то еще быстрей, фактически только в базу сходить. Сюда добавляется эффективное кеширование, и в итоге если раньше SPA делало 5 запросов на клиенте после обработки JS, то есть это время плюсовалось и стадия отрисовки и тем более интерактивности наступала с большой задержкой, то с подобной схемой при первом открытии страницы нужно 0 запросов и готовый html получается при первом запросе, соответственно значительное ускорение. В энтерпрайзе такая схема уже четвертый год используется, по крайней мере в тех проектах, в которых я участвовал — все метрики перфоманса действительно значительно улучшаются.
Минус — больше серверных ресурсов уходит на рендеринг, но это вполне горизонтально масштабируется. Количество запросов в секунду у ноды тоже стало приемлемым, при необходимости, опять же, балансируется. Но для хайлоада все же лучше оставить в ноде только веб-часть, а требовательные к перфомансу сервисы писать на более низкоуровневых и многопоточных платформах.
Плюсанул за последний абзац — ровно 5к и 1-в-1 описание, пару лет так работаю на зарубежные компании. И бывшие коллеги точно по такой же схеме, так что максимально жизненно. Выше ставка только по проектной работе бывает — на 2-3 месяца, а с учетом простоя то на то и выходит.
Здорово получилось, действительно что-то новое, шустрое и почти юзабельное. По старой привычке переписал примеры для себя на mobx — намного меньше кода получилось и он полностью лишен бойлерплейта, кроме вызова makeAutoObservable в конструкторе, так что все-таки в проекты не потащу — там скорость восприятия кода и явность взаимосвязей намного важнее сэкономленных килобайтов, но для пет-проджекта для развития нового восприятия работы с данными — вполне можно.
Об одном моменте задумался — генераторы же не полифиллятся, но судя по caniuse поддерживаются 95.41% клиентов, значит вполне уже можно в прод, если не для госконтор и азиатского рынка. В общем — годно.
Повторюсь, бутстрап и другие готовые системы стилей — для приложений, в которых не важен дизайн, а важен функционал. По ссылке же своя дизайн-система (отступы, шрифты, возможно точки адаптивности, цвета, иконки и другие элементы). Если набутстрапить и переписывать импортантами все — это не то что ускорение и увеличение удобства разработки и стабильности интерфейса, а полная деградация. Да, приходится тащить эту недотелегу в гору вместо того, чтобы выполнять задачу эффективно и чисто.
Второй момент — в подобных приложениях верстка занимает от силы 5% времени фронтендера, остальное — архитектурные вопросы и js, так как подобное пишется на фреймворках для более высокой скорости работы, разбиения по компонентам, переиспользуемости, стабильности UX. Если туда добавить готовую дизайн-систему с хаками, можно однозначно сказать, что хорошо не получится.
Сейчас макеты выгружают в Figma / Zeplin, они там в векторном формате. При клике на элемент эти инструменты сами генерируют css стили — размеры, цвета, шрифты, остается просто скопировать к себе и настроить раскладку блоков и адаптивность. Иконки или растровая графика парой кликов выгружаются в svg или jpg, в общем, верстать стало намного проще.
Раньше да, приходилось из Photoshop / Illustrator / Corel Draw самостоятельно вычислять размеры, черри-пикать цвета, сложнее определять отступы от элементов и сложнее экспортить иконки / растровые изображения.
Конструкции #{выражение} действительно страшновато выглядят, но в реальных приложениях годы назад только такое видел, а сам писал последний раз в 2014 типа таких систем раскладок (LESS)
Но это контрпродуктивно и подходит только для унифицированных раскладок интерфейса
Здесь всего лишь задается всем элементам одинаковая высота и вертикальный отступ — пригождается в компонентах нотификаций и галереях, чтобы друг под другом находились элементы и не сразу "прыгали" вверх при удалении, а с анимацией. Но в стилях это можно только статично описать, то есть если высота элементов разная — уже работать не будет, поэтому подобные вычисления производятся в js. В общем, не пригодится.
А те миксины, про которые я говорил — простейшие наборы статичных параметров типа
.inline-block() { vertical-align: top; display: inline-block; }
, такие вот небольшие кирпичики. Динамические параметры в стили сейчас передаются с помощью Custom CSS Variables, то есть в стилях пишешь.myClass { color: var(--main-color) }
и затем при изменении этого свойства у родительского элемента этот чайлд перекрасится. Монструозные либы с кучей логики как-то ушли в прошлое и только на простых сайтах используются, ввиду сложностей кастомизации.Так с любыми библиотеками — при необходимости кастомизации код обрастает хаками, затем библиотека форкается и собирается модифицированный билд, затем при изменении требований времени на возню в большой чужой кодовой базе уходит все больше, а стабильность становится все хуже, так как приходится поддерживать обратную совместимость, что применительно к интерфейсам довольно сложно.
Со временем за небольшой срок разработчик учится делать свою библиотеку миксинов для библиотеки компонентов, что в поддержке проще и намного гибче. Поэтому лет 10 я уже не использую стилевые фреймворки, а готовые библиотеки компонентов — только для админок, где по бизнес-требованиям важен не дизайн, а функционал.
То есть берется инструмент запросов с неявным внутренним хранилищем, неявно там что-то нормализуется и преобразовывается с помощью мидлвары… Для остальных интеракций, видимо, будет заведен другой стейт-менеджер, и каким-то образом нужно будет синхронизировать с этим react-query… Что-то вообще не понимаю смысла всего этого.
Я мыслю в категориях
api.getUser(user => store.createOrUpdate(store.user, user))
иrender() { return <>{this.context.store.users}</> }
с разделением слоев апи, модификаторов стора, хранилищ и реакт-компонентов, отвечающих за View. Запросы из компонентов и параллельные сторы, неявные трансформации и технические параметры в данных — это для меня другой мир...Ну, может, кому-то пригодится.
"Отсутствие строки лучше чем ее наличие" — так же относится и к двум дополнительным параметрам, дополнительному слою, дополнительной библиотеке и документации. Этих строк с partialAssign будет ну пара десятков на среднее приложение, а размер библиотеки, включенной в бандл и код обвязки займет явно намного больший объем, увеличив и количество поддерживаемых компонентов системы. Поэтому аргумент звучит неубедительно.
Я очень сомневаюсь, что это стоит 0, вообще-то это все довольно дорого.
Как я понял, в целом эта система нужна, если одинаковые запросы шлются из разных компонентов и данные складываются в разные хранилища, и именно для этого нужны "глобальные id для всех сущностей". В реальности за несколько лет мне не попадалось таких "архитектур" — если данные используются в нескольких компонентах, то они выносятся в глобальное хранилище (redux, mobx) и их обновление происходит явно, id нужен только для partialUpdate. Поэтому я так и негативно отнесся к предложениям в статье. Но если действительно приложение так построено, что данные и методы их получения многократно дублируются и раскиданы по локальным сторам, то подобная нормализация, безусловно, нужна. Как нужно и переписывание архитектуры ядра, на мой взгляд.
"для разработчика ничего не меняется в работе, ни с получением данных" — вот это все равно звучит очень странно. Нормализованные или ненормализованные данные все равно же надо положить в хранилище, не будет же iresine этим заниматься? Или подразумевается, что все полученные данные вместо redux или mobx будут храниться в некоем iresineStore и напрямую получаться оттуда вместо других хранилищ без системы синхронизации с основным стором?
По мне так строчка
this.users.set(user.id, user)
выигрывает по сравнению с созданием слоя нормализации с эвент-эмиттером, изменением большого количества сущностей с добавлением лишнего type, большого оверхеда по обучению команды и поддержанию единообразия. И в итоге идеально все равно не получится — будут легаси-ручки бэка или сторонних интеграций, данные в которых не поменять, и "нормализация" кашеобразно распараллелится на фронт и бэк. Да и в самом фронте соблюдать получится только при создании нового проекта, так как внедрять подобный слой в существующую кодовую базу дело недель работы без какого-либо бизнес-выхлопа, кроме негативного — увеличения сложности поддержки.Статью внимательно прочитал, но так и не понял, зачем это нужно. Хранилища в памяти с ключом по id в моей практике пригождались только 2 раза — при получении частичных данных (например, user) в десятке разных ручек (но выбранный подход с нормализацией оказался в итоге не самым эффективным, добавив значительный оверхед) и при необходимости скорости доступа О1 в требовательных к перфомансу интерфейсах при наличии 10к+ элементов в массиве. В остальных случаях
store.update(store.users, user)
, где update соответствующей стратегией находит целевой элемент или создает новый, идеальный вариант.Спасибо за статью. В предлагаемой на MobX архитектуре, хотя код в этой первой части и не приведен, прослеживаются некоторые спорные с моей точки зрения моменты:
Объединение store, actions (лучше их называть store modificators, чтобы не путать с редаксовыми) и selectors в единый слой. Те данные и методы, с которыми работают фронтендеры — не микросервисы и не игровые сущности, для которых группировка подходит четко, и Dog может run() и можно get condition() исходя из внутренних параметров. АПИ своего бэка или стороннее может быть так спроектировано, что содержит смешанные данные, которые придется разложить по десятку семантических сторов, поэтому фича редакса с возможностью по одной константе отлавливать payload в разных подсторах очень востребованна.
Касательно селекторов — то же самое, часто необходима комбинация данных из нескольких семантических сторов, и get canShowSomeBlock не будет принадлежать ни стору User, ни UI, ни Forms, а данные, необходимые для вычисления данного параметра, могут браться из них всех. И тут на сцену выходит либо Dependency Injection со всей сопутствующей кашей и полным уничтожением семантичности и консистентности хранилищ, либо все же создается отдельный слой.
Если же модификаторы и селекторы признать отдельным слоем и передавать в них весь store со свободой модификации и чтения данных из любого, то получаются простые для понимания и использования слои с собственной структурой, продиктованной не архитектурой хранилищ, а реальными вариантами использования. За счет передачи всего стора получается удобная группировка без необходимости в модификаторах делать ручные DI и вызывать многочисленные методы.
Из недостатков описанного мной подхода — возможность раздувания этих функций-модификаторов хранилища, что решается более узкой их специализацией. Также многим, привыкшим к структурам типа Dog.run() не понравится отсутствие "явного разделения ответственности", когда функция может получать доступ к любому параметру без жестких ограничений, как и реактовые компоненты (через контекст) к любому экшену-модификатору или опять же параметру стора, что якобы дает разработчику "свободу действий", что обязательно выльется в лапшеобразный код с неявными взаимосвязями. На деле такое встречается намного чаще как раз при наличии ограничений, которые разнообразными хаками (DI, горизонтальные связи, параллельные системы доступа к данным, дубляж) преодолеваются и код становится не только лапшеобразным, но и неподдерживаемым — при желании его отрефакторить сломаться может что угодно. В случае же, если некий экшен, написанный юниором, использует данные из 4 подсторов сразу, хотя его можно было бы разбить на 2 более семантичных экшена, это рефакторится за минуты фактически без рисков поломки, и не является легаси, в отличие от хаков, обходящих основной архитектурный паттерн.
По поводу того, что в модификаторах не стоит вызывать асинхронную логику получения данных из сторонних источников. Тут, думаю, все же нужно подходить контекстуально — если данные юзера могут приходить только в одной ручке, то ничего плохого в том, чтобы написать
я не вижу. Если же данные юзера могут приходить и в других ручках, то можно создать более универсальный модификатор
setUser = user => partialAssign(store.user, user)
, но не считаю это обязательным изначально, так как выглядит преждевременной оптимизацией и в общем случае приведет к созданию десятков лишних функций.А сам сбор данных из сторонних источников в модификаторах считаю оправданным, так как иначе придется выделять еще один слой "подготовки данных перед вызовом модификатора", который всегда будет вызываться перед самим модификатором и в 99% случаев это будет дубляж. Оставшийся процент — кейсы, когда источником данных может служить не одна сторонняя система, а несколько, но это проще решить if условием в самом модификаторе, чем менеджерингом отдельного слоя предварительной нормализации данных. Либо можно таким вот promise-middleware паттерном
Конечно, export default — зло, подход выше — только для сторонних библиотек с большим набором потенциально неуникальных экспортов. Вот как раз для тех трех и подходит, ну и для нодовых built-ins. Вынесение этих библиотек в отдельный файл и реэкспорт переменных в контролируемом формате тоже нормальное решение, хотя и удлиняет цепочку импортов.
Мне ближе вариант с неймспейсами (но нужен хороший tree-shaking по использованным функциям в этом случае)
А поддержка старых браузеров обычно обеспечивается либо автоматическим полифиллингом, либо загрузкой уже скомпилированной библиотеки по наличию фичей в браузере.
Но подход интересный
Высказывать предпочтения не запрещено, наоборот, плюрализм мнений — это прекрасно, как и то, что занимаетесь обучением других людей. На собеседования действительно трачу много времени, сотня в год как минимум набирается, и пара десятков в качестве интервьюера — каждый сам выбирает, по каким критериям подбирать работу, у меня они достаточно специфичные, поэтому редко устраиваюсь, а подбираю месяцами. Забивать свободное время программированием мне уже не интересно, если есть желание — занимаюсь пет-проджектами. У вас другая история, много говорите про деньги в статье — для меня же куда важнее идейная и архитектурная составляющие, так что цикл поиска работы разный.
Про GraphQL верно — все нюансы его работы не знаю, как и сопутствующие библиотеки, ссылаюсь на мнение коллег, анализ их юзкейсов и статистику использования. Исходя из этого и сказал, что не рекомендовал бы начинающим изучать эту технологию, что подтвердили ваши слова про "мало кто это в рунете сейчас умеет". Из TS моделей для реста формировать валидаторы несложно, достаточно типов запроса-ответа, обработка ошибок и вариантов ответа бэка тоже не сложна, так что я бы начал обучение с этого — явно в подавляющем большинстве проектов используется он и будет актуален еще долго. Сколько времени займет изучение реста? Точно не больше, чем графовой системы запросов, старт прост как
fetch(url)
, дальше — типизация, валидация, обработка. Когда человек, изучающий рест, дорастет до миддла? Может завтра, а может никогда.Как я понял, цель этой статьи скорее самореклама, так как статей типа "как войти в айти" огромное количество, и многие более обоснованны. И то, что вы с чего-то начали рассказывать в комментариях, что хотите обучать и у вас уже много учеников, подтверждает. В целом не имею претензий, сказал только что у GraphQL малое распространение и много нюансов, а SC хайп из прошлого, который неудобен и неэффективен.
Про рефакторинг уже сказали, про xpath пока нет… Глобальный поиск по дереву типа
#root div.Agreement div.checkboxWrapper input
как раз усложняет "генерацию и адаптацию тестов к изменяемому коду", делает тесты нестабильными и привязанными к конкретной реализации. Поиск по уникальному идентификатору — единственный способ создать абстрагированные тесты, которые не потребуют исправления при рефакторинге, так как иерархичное DOM-дерево не является статичной точной опоры.Про отсечение "лишних выдач и бэктрекинг" не понял, возможно имелись в виду статичные моки данных под первым, но бэктрекинг… Сложно догадаться, что под этим имелось в виду, и при чем тут React.
Вообще опасно заходить в JS топик на Хабре с рекомендациями, какие технологии изучать. Сообщество программистов достаточно четко разбилось по используемым фреймворкам и подходам, и явно многим не понравится то, что фокус ставится строго на определенные.
Я лично не советовал бы GraphQL, так как в реальном проекте не видел ни разу, и по собеседованиям в сотнях компаний только в паре он использовался — он создает большую нагрузку на бэк-разработчиков, в большинстве случаев ухудшает перфоманс и стабильность (права доступа, оптимизацию и кэш, стейт-машины по отдельным ручкам куда проще менеджерить), там сложно с валидацией, нестандартными данными. Также и от SC везде отказываются ввиду колоссального количества недостатков. Лучше бы посоветовать новичкам изучить CSS (+Modules) с PostCSS-плагинами, которые могут расширять его функциональность подо все нужды и классический REST + RPC с Socket, так как это намного более распространено и под те самые малые и средние проекты, о которых вы говорите, GraphQL интегрировать почти точно не будут. Если цель как раз зарабатывать деньги программированием, лучше учиться популярной классике, а не тому, что когда-то было на хайпе.
Интересно, почему ваш коммент минусуют… Возможно, из-за async/await? С ними сложно работать, так как уходишь в try-catch hell для имплементации того функционала, который был бы однострочным с Promise. Также сам концепт псевдосинхронного кода, когда не понимаешь (либо легко пропустить при чтении), что последующий код будет выполнен не сейчас, а в неопределенном будущем. В концепте MobX видел, как в not strict mode люди забывали писать runInAction (да, конечно я знаю о schedule, который может это автоматизировать — но не видел в проектах), что приводило к лишним ререндерам. В целом я придерживаюсь Promise — это простой инструмент с явным потоком в рамках многим привычного middleware паттерна, но без глобалок. Не отрицаю, что можно и с асинками писать грамотный код и путем составления best practice минимизировать ошибки, но сама формализация best practice — редкость, так что лучше уж Promise.
Может, поэтому минусовали… А так-то здравые рассуждения, пусть и немного эмоционально)
Статья называется "Кому с Redux жить хорошо", как я понял из содержимого — основной упор на обзор юзкейсов этого инструмента, а TDD используется как пример. Поэтому считаю свою критику вполне обоснованной, так как к комбинации диспетчер-экшены-константы-редюсеры-селекторы-контейнеры-ручной маппинг-иммутабельность-проброс через props с обновлением родителей — довольно много контраргументов. Это обсуждалось много раз на Хабре, и подавляющее большинство проектов (из тех, в которых я участвовал — все) перешли либо на MobX, либо на локальные стейты+контекст+hooks&функциональщину. Поэтому в статье стоило бы сделать вывод "Кому все-таки хорошо с Redux? Никому", а не "потом навертим Redux Tookilt и будет чуть менее больно".
Если вы бы хотели, как красиво пишете в комментарии, сделать пример работы через TDD в отрыве от конкретных подходов ("без всяческих усложнений", "инженерные практики", "качественный код", "не привязываясь к конкретному стеку", "нужно просто научиться писать тесты"), то не ставили бы статье такой броский заголовок, и не рисовали бы картинки "архитектуры" редакса и вот этот весь бойлерплейт. Поэтому говорить красивые слова про инженерию, предсказуемый результат и качество кода не стоит — статья в целом не об этом.
Про перевернутую пирамиду тестирования — юнит-тесты (основа пирамиды) — это не про интерфейсные компоненты, которые преобразуются в другую структуру (DOM) и работают в десятках разных окружений в комбинации со слоями позиционирования и стилей. Это про чистые функции (нормализация данных, модификация хранилищ) и редко — про сайд-эффекты. Отрендеренный в виртуальной Jest-среде компонент и то, что ему переданы корректные пропы говорит только об этом, а не дает никакой гарантии работоспособности. Если это защита функционала от случайной модификации другим разработчиком — то да, в этом кейсе работает, но и в равной степени мешает, так как часто приходится править тесты "для галочки". Для корректных типов данных есть TS, который справляется с задачей не хуже, а для всего остального — интеграционные тесты.
Допустим, есть бизнес-задача:
Под эту задачу по TDD пишется интеграционный тест:
Этот код завязан на id, соответственно ему не важна реализация — React, Redux, Svelte, простой html. Также он проверяет, что эти элементы отрендерены, не скрыты стилями, интерактивны, не перекрыты другими слоями. Это как раз концептуальный TDD для автоматизированного контроля соответствия приложения бизнес-требованиям, и если бы вы говорили в статье про это, а не как навернуть в Redux-приложении бесполезные тесты для view-слоя, то код тестов был бы аналогичен.
То, что Jest "стабильнее и быстрее" работает — тут не поспоришь, хотя на хорошей машине headless-браузеры работают очень шустро. Только вот даром такие юниты не нужны, если компонент прост — то его и так легко спроектировать, если сложен — то почти всегда можно раздробить на части, так что эти "react-redux-юниты" и тут не помогут.