All streams
Search
Write a publication
Pull to refresh
47
0
Дмитрий Казаков @DmitryKazakov8

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

Send message

Для 2016-17 бы статья была еще норм, но в 2021… Redux? CRA? export default? Иммутабельность? Тестирование компонентов вместо интеграционных или e2e? Хотя чему удивляться, еще появляются статьи про модалки на jQuery и обработку форм на php. Но выглядит так, будто динозавры повылазили из пещер.


По коду в целом норм, но в package.json надо еще записывать types, чтобы типы подтягивались из главного файла


{
  "main": "./LicenseAgreement.tsx",
  "types": "./LicenseAgreement.tsx"
}

И import React from 'react' неактуален с 17 версии. Также ручной маппинг по строкам 'agree' некорректен — как минимум нужны текстовые константы, оптимально — подключен стор с локализацией и простым доступом к текстам компонента, так как падающие юнит-тесты из-за изменений в тексте — боль.


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

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

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

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


При этом общее состояние мира является сбалансированным и стабильным, изменения в одной области в любом случае повлияют на все остальное, из чего я делаю вывод о "глобальности состояния", но несовершенстве его синхронизации. В программировании я преодолеваю эти несовершенства драфтово называемой "портальной архитектурой", когда есть единый целостный стейт, к которому может "портально" (react context + observer) подключиться любой видимый (view) или невидимый (side effect) потребитель, при этом изменения в хранилище моментально (синхронный event emitter) распространяются до тех потребителей, которым нужны конкретно эти данные (reaction subscribe). Так достигается целостность всей системы, максимальная скорость, независимость состояния и при этом его синхронизированность с отображением.


Почему это проще? Потому что не нужно получать стейты от неограниченного количества источников и синхронизировать их, как-то из props+local state+global state, у которых разные механизмы передачи информации и разные жизненные циклы. Почему проще именно в разработке? Потому что не нужно создавать цепочку передачи информации, которая приводит к ререндеру всех передатчиков и нестабильности финальных данных (проходя через props нескольких компонентов велик соблазн их модифицировать, что встречается повсеместно; из реальности — свет фильтруется, искажается его направление, рассеивается).


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


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


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

У меня как раз обратный опыт, и большинство проектов с тьмой локальных сторов приводят к мешанине, высокой стоимости изменений и техдолгу, и приходилось все это переписывать заново. Единый расширяемый корень, одна точка доступа к данным — это полное отсутствие бойлерплейт-кода, изолированность и цельность state, независимость от view, стабильность, переиспользуемость, удобство создания computed селекторов, доступность из любой точки — и это не недостаток, а благо. Все точки использования легко находятся через findUsages.


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


Явные прослеживаемые связи — эта мантра идеально ложится на единый глобальный стор, так как 1 быстрый переход — и ты в модели, одним поиском находишь все места, в которых этот параметр использовался и мог изменяться, а не бегаешь по пропсам, чтобы найти родителя, который содержит в себе локальный стейт, из которого выстраивается локальный стейт данного компонента.


При этом вот этих "как сделать? Никак" становится крайне мало. Возможно, вы не работали с годными реализациями глобального стейта. То, о чем вы говорите — там есть, но добавляется возможность свободного доступа и независимого от view изменения. Слой данных по моей убежденности должен быть независим, как и слой сайд-эффектов, как и слой апи, как и слой стилей, как и слой модификаторов состояния. А все эти useState, useEffect с изменением стейта и сайд-эффектами, бесконечный props drilling, который приводит к ререндеру цепочки родителей, хотя только конечному компоненту нужны данные — это тот самый техдолг, а не его преодоление. Ну и SSR, видимо, у вас не используется.

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


По поводу вашего примера — как раз так делают и это эффективней:


@makeObservable
class CountStore {
  x = 1;
  y = 2;
  get z() { return this.x + this.y }
}

// спроектировать экшен можно как угодно, я обычно делаю bind на
// глобальный стор и не передаю в параметрах вручную
function actionIncreaseX({ store }) {
  store.x = store.x + 1;
}

const FComp = observer(() => {
  const store = useStore(CountStore);

  return (<div onClick={() => actionIncreaseX({ store })}>{store.z}</div>)
})

Любой потребитель может подключиться к CountStore и использовать его данные — будь это autorun с сайд-эффектом записи изменившегося z в тайтл страницы или другой реактовый компонент, например — с инпутами, позволяющими явно указать x и y. С локальными состояниями все становится сложнее — сайд-эффекты придется описывать в самом компоненте, который хранит стейт, а передача другим компонентам x и y с функциями их установки — большой геморрой. Если это чайлды, то можно и через props, засорив этот механизм передачи данных, а если соседние элементы — то придется использовать некую шину в родительском компоненте, который уже будет передавать чайлдам эти данные и методы их изменения. Вряд ли кто-то выберет этот вариант, если в аргументации есть что-то кроме "очень, очень плохо совать данные в глобальный стейт".


Вообще диалог из разряда "сам дурак" получается, отвечу в той же манере. Данные форм должны быть глобальными. Формы должны быть на конфигах. Там. Затем. Экшены, про которые я говорил — обычные функции модификации стора aka методы, как в примере выше, а не то, что redux под ними подразумевает, мы явно не сходимся в терминологии, и явно вы говорите исходя из опыта на redux а не mobx — это разные миры, про недостатки редаксовых сторов и экшенов я сам могу много всего написать, но вроде на Хабре это уже не актуально.

"лишние церемонии", "глобальная портянка", "сложнее" — это явно от болей работы с редаксом, для mobx это все неактуально. "Точечный доступ" как раз увеличивает связность компонентов и добавляет лишние церемонии, это горизонтальное и parent-child общение между компонентами приводит к большой запутанности. Я описывал неудобства, повторять смысла не вижу.


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


Экшены — не события, а модификаторы стора с возможностью асинхронных вызовов. Слой событий обычно держится рядом с view:


handleClick = (...) => this.context.actions.openTooltip(...)
render = () => <button onClick={this.handleClick} />

Если бы я писал yield findReactElement(Tooltip)[0].handleClick(...) — тогда да, получается лез бы во внутреннее состояние и хендлеры событий — и это возможно только в случае, если этот компонент уже отрендерен, но скрыт стилями (эти хаки нужны только при подходе с локальным состоянием). В описанном мной подходе вызываются лишь экшены — модификаторы стора, которые никак не зависят от view и не знают о нем, а просто меняют данные, после чего store рассылает во view уведомления с изменениями и реакт ререндерит представление.


Если у вас есть более удобные способы работы функции handleOnboardingStep с локальными состояниями — буду рад почитать.

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

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


Про хуки мы уже разговаривали, там нужны очень конкретные и подходящие условия, чтобы снизить количество болей. Но чем глобальный стор насолил кроме отсутствия возможности сделать 2 разных под капотом, но одинаковых с точки зрения использования в компоненте подстора? Эта ситуация, опять же, решается вложенным контекстом, и если один-два места таких будут на большое приложение (а больше вряд ли понадобится) — то велика беда от такого подхода не будет. А вот необходимость иметь доступ ко всему состоянию приложения возникает часто, и локальные сторы во всем проигрывают глобальным. Также при разбиении кода на независимые модули с общим рутом в любом случае будут глобальные компоненты — модалки, нотификации, роутинг, та же локализация. Также придется внутри самих компонентов разруливать вычисление состояния исходя из локального и глобального — почему сразу не делать глобальным? Если на странице 5 форм — никто не заставляет их пихать в единый глобальный store.formData, разумеется тут будут store.forms.firstStep, store.forms.subscribeToNews и т.п.


Если же компонентов на странице много и они однотипные — то это будут store.tooltips[{ label, isOpen, ref }] с простой возможностью все подсчитать, закрыть при необходимости и удалить любой из любой части интерфейса. С локальными состояниями это просто невозможно без системы доступа к ним — а она обычно корява либо как this.componentRef.querySelector('[data-tooltip]')[0] либо как доступ к innerState через разнообразные хаки. Интерфейс на странице в любом случае имеет глобальную раскладку, глобальные стили, так почему его состояние должно дробиться на множество независимых и сложносинхронизируемых?


С глобальным многое становится более красивым, тот же онбординг будет состоять из одноуровневого и независимого вызова функций (редкий кейс, когда пригодятся генераторы, и можно по клику на кнопку вызывать следующий шаг)


function handleOnboardingStep* (store) { // лучше, конечно, не store а actions
  yield store.openTooltip(...)
  yield store.highlightArea(store.tooltips[0])
  yield store.closeTooltip(...)
  yield store.openModal(...)
  ...
}

Сериализация состояния и SSR тоже намного проще. Логирование действий и просмотр пользовательского пути для дебага. Computed селекторы, выдающие значение исходя из состояния приложения. Изолированность от библиотеки рендеринга. Вот никогда не упирался в невозможность или неудобство использования глобальных состояний, они и очищаются прекрасно, и ресетятся, а вот с локальными — сплошные неудобства.

Кому-то может быть полезным, но я вот вообще не понимаю, зачем нужен классовый DI в SPA и чем плохо глобальное хранилище. Парой строк оно расширяется и чистится


componentWillMount() { this.context.extendGlobalStore([NewStore]) }
componentWillUnmount() { this.context.clearGlobalStore([NewStore]) }
render() { return <>{this.context.store.newStore.param}</>}

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


<InjectStoreHoc newStores={[NewStore]} apiMocks={IS_LOCAL ? apiMocks : undefined} />

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


store.juicer.makeJuice(store.apples)
store.juicer.makeJuice(store.oranges)

// либо универсально, если через HOC передан FruitStore
store.juicer.makeJuice(store.fruit)

Сама функция makeJuice таким образом будет выглядеть как


@computedFn
makeJuice = (fruit: IFruit) => fruit.giveJuice()

И никаких дополнительных либ, презентация не на 98 страниц а на 2, обучение коллег занимает 10 минут, весь дополнительный код подгружается отдельным чанком, глобальный стор чистится и освобождается память при переходе на другую страницу, легко тестировать, передавая замоканный стор, имплементирующий IFruit. Не надо всей этой возни с бойлерплейтом, работать с подобным очень неудобно.


Единственная заморочка — в расширении типов глобального стора, так как при таком варианте TS не будет знать, что такое store.fruit, переданный через HOC, но решается либо обычным const fruit = store.fruit as IFruit, либо declare в классе, либо ручным расширением типов глобального стора, либо заведением в глобальном сторе public fruit: IFruit с пустым значением, либо типизированным пропсом — это все нужно только для статической проверки, поэтому выбрать можно по вкусу.


Код из репо комментить не буду, там слишком export default)

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

С этой задачей справляются CSS Modules с именованием классов [folderName]_[className]. В веб-инспекторе видишь class="FormCreateUser_submitButton", за 1 поиск по проекту находишь файл FormCreateUser.scss либо FormCreateUser.tsx, в зависимости от того, что нужно поправить, и 1 поиск по файлу для телепортации в нужное место. Все остальные способы (девтулзы, поиск по тексту), кроме поиска по уникальному id (если есть система автотестов, опирающихся на них, то они будут), намного длиннее и сложнее.

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


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

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


В демке показываешь то, что хочешь показать — кому-то действительно достаточно работоспособного решения, но если разработка там командная, то лучше показывать красивый код

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


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


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

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


А по второй части, где про то, что много осталось кодовых баз на php и хочется какой-то стабильности, чтобы все на одном языке и в одном подходе писали — это нереалистично на данном этапе, когда технологии активно развиваются. Может лет через 20 и устаканится, хотя что делать со всеми старыми кодовыми базами — непонятно. Тут либо развиваемся и что-то оставляем в прошлом, либо страдаем от ограничений устаревших решений.

Как-то слишком базово. Если интересно более масштабное применение ts в конфигах вебпака — выкладывал тут. Можно просто через babel-node запускать сборку, бабелем вырезая типы — практика показывает, что каждый раз проверять их бесполезно, достаточно ориентироваться на подсказки в IDE, т.к. вебпак при запуске сам проверит, правильно ли он сконфигурирован.

Можно при возникновении ошибки валидации (andrew: null) заменять его на дефолтное значение (andrew: ["+99999999"]), это норм для практики некритичной отказоустойчивости. А ошибки эти ловить генератором, который будет работать по while(!checkPhoneBook(obj)), это уже просто в качестве упражнения

Эх, мне было бы приятней видеть еще большее упрощение:


export const counterStore = defineStore({
  count: 0,

  get double() {
    return this.count * 2
  }

  increment() {
    this.count++
  }
})

counterStore.increment()

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

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

Information

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