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

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

Send message

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

Сейчас да, сделать на них точечное обновление, ssr и нормальный проброс не только строковых значений - та еще задача. С массивами работать сложно, shadow dom концепт непростой... Проблем масса. Но как прототип для нативных кастомных элементов - пойдет. Мы же о будущем говорим, и я надеюсь что удобство jsx может придти и в стандарты

Спасибо за статью, интересно!

Я думаю, что в будущем подходы будут различные в зависимости от задач. Так,

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

  • малый бизнес - продолжит пользоваться no-code решениями и готовыми CMS

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

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

Кстати по поводу установки зависимостей в CI - они как правило меняются не часто, и можно сделать кеш слоев именно в CI

#13 [ 5/10] COPY pnpm-lock.yaml /app/
#13 CACHED
#14 [ 7/10] RUN pnpm install --prod
#14 CACHED

Так будет упираться только в скорость копирования в большинстве случаев, а это хорошо оптимизируется. Лишние 100мб дадут лишь несколько секунд сверху. Я не силен в девопсировании, но у нас в рецепте билда на восстановление из кеша и все сопутствующее уходит 1 минута + 30с сборка, так что увеличение размера node_modules влияет незначительно.

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

Когда я перевел сборку проекта (не тестов) с Babel на SWC получилось вот что. Верхние результаты с минификацией, которая тоже переехала с Terser на Terser.SWC. Также здесь включено море плагинов, которые остались без изменений, поэтому результат довольно чистый - изменился только транспайлер

(CI) Babel без прогретого кеша + Terser

#16 22.96 [WEBPACK] finished building node within 20.21 seconds
#16 40.69 [WEBPACK] finished building web within 36.324 seconds

(CI) SWC + Terser.SWC

#16 13.14 [WEBPACK] finished building node within 10.57 seconds
#16 29.60 [WEBPACK] finished building web within 24.831 seconds

(Локальная сборка без минификации) Babel без прогретого кеша

[WEBPACK] finished building node within 5.443 seconds
[WEBPACK] finished building web within 6.745 seconds

(Локальная сборка без минификации) Babel с прогретым кешем

[WEBPACK] finished building node within 2.348 seconds
[WEBPACK] finished building web within 4.044 seconds

(Локальная сборка без минификации) SWC

[WEBPACK] finished building node within 1.609 seconds
[WEBPACK] finished building web within 3.742 seconds

Все на одной машине, запуски делал раз по 5, среднее значение скорости менялось не сильно. Выводы получились следующие:

  • SWC не требует прогревания кеша, что максимально отражается в CI

  • Terser.SWC - очень быстрый минификатор, без него результаты не такие поразительные

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

  • Размер node_modules действительно вырос, но цифры у меня не сохранились.

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

Про "никто не меняет вью" в принципе согласен, но я скорее для спортивного интереса поддерживаю архитектурные части изолированными и абстрагированными. Не только Реакт могу поменять на Preact/Inferno/SolidJS и экспериментирую с заменой через адаптеры на другие рендереры (даже веб-компоненты), но и MobX на другие реактивные библиотеки, тот же Solid mutableStore. Это позволяет мне лично расти как специалисту и создавать долгоподдерживаемые проекты, заодно сравнивая перфоманс и набор фичей и оттачивая дизайн системы. И в целом стараюсь не завязываться на конкретные особенности (ex. версия определенной либы, которая рендерит асинхронно). И другие архитектурные части тоже делаю самодостаточными и независимыми. Поэтому хаки чисто для MobX в виде асинхронного автобатчинга не рассматриваю.

Конечно, можно и с ним жить, и утилиты подбить так, что будет и SSR, и нормальный стек вызовов, и соблюдена очередность реакций. Можно и на альтернативные библиотеки наворачивать такой функционал, но все равно мне ближе явный подход - где изменяется стор, оборачивать в batch (runInAction в mobx). Так добавляется бойлерплейт, но упрощается понимание за счет синхронности операций в и так непростом асинхронном мире JS, а в ряде случаев - улучшается перфоманс (незначительно, но в перегруженных компонентами страницах асинхронщина может привести к глитчам).

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

И еще - в асинхронной реакции может уже не быть той сущности, с которой она должна работать (компонент размаунтился, this = undefined, состояние модульного стора очистилось).

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

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

  2. Цепочки авторанов (в первом меняется параметр, который слушает второй авторан, в третьем прослушивается результат второго) - хоть Реакт 18 и добавил асинхронности в рендер, я на это не полагаюсь, и на каждый рендер подготавливаю финальное состояние, чтобы отрендерилось 1 раз. Т.к. ориентируюсь на более-менее свободную замену частей приложения (в том числе рендерера типа Реакта), то важно синхронно подготовить данные для избежания ререндеров - это просто хороший тон и предотвращение проблем.

  3. Ну и минорное замечание к решению выше - если подменять одну функцию другой и передавать в setTimeout(fn) то контекст теряется и в стеке вызовов будет batch вместо реального имени функции. Фиксится парой строк, но все равно об этом думать надо

Оказывается, это Mazaa шифруется под новым ником, интересно)

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

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

  configure({
    reactionScheduler: f => setTimeout(f, 1)
  });

Тоже пользуюсь Find Usages, дебаг очень простой. А чтобы не было ворнингов все эшены автоматом оборачиваются в action, а для батчинга runInAction, если после асинхронных вызовов. Я пока не считаю, что оправданно вырубать enforceActions - многое в коде зависит от батчинга (мне нужно чтобы несколько переменных менялись сразу, а не постепенно - не для рендера в Реакте, а для autorun, которые например отслеживают форму - надо чтобы и value поменялись, и isFocused, и валидаторы, и авторан вызвался 1 раз с финальными данными). И таких ситуаций много. Плюс я стараюсь делать библиотеко- и фреймворко-независимую архитектуру, а в альтернативах MobX тоже используются колбэки системной функции для батчинга, иначе его просто не выстроить синхронно.

К теме дебага - все экшены у меня еще логируются в визуальный интерфейс (история вызова + время на исполнение) в виде stack bars chart, что максимально сужает поиск по Find Usages.

Немного вклинюсь по поводу юзстора - решается оберткой (я пишу на классовых компонентах, поэтому пример для них)

import { observer } from 'mobx-react';
import { Component } from 'react';

import { TypeGlobals } from 'models';

import { StoreContext } from './StoreContext';

export class ConnectedComponent<TProps = any, TRoute = never> extends Component<TProps, any> {
  // SSR not able to recalculate components on observable updates,
  // so on server side it has to be rendered twice
  static observer = IS_CLIENT ? observer : (someComponent: any) => someComponent;

  // Describe context, so no boilerplate in components needed
  static context: TypeGlobals;
  static contextType = StoreContext;
  declare context: TypeGlobals;
}

// Any component

class AnyComponent extends ConnectedComponent<TypeProps> {
  render() {
    this.context // Perfectly typed and reactive, no boilerplate at all
    this.props // Perfectly typed and reactive, no boilerplate at all
  }
}

В Webpack делаю транформер, который все компоненты, которые экстендят ConnectedComponent превращаются в

const AnyComponent = ConnectedComponent.observer(class AnyComponent...)

Так получается реактивность без сахарных декораторов (чтобы использовать не Babel, а более шустрый SWC транспайлер), с отдельной логикой для SSR и без бойлерплейта.

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

Не могли бы привести пример предупреждений в консоли? У меня нету ни в одном проекте, как вызвать?

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

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

"В эффекторе же у вас есть только события, которые способны изменять стор" - ну и в Mobx есть функции-экшены, которые изменяют стор, только без необходимости иммутабельно писать как в редаксе (store, data) = > ({...store, items: store.items.map(item => ({...item, isViewed: data.id === item.id}))}) , разводя дубляж и значительно усложняя код, и заводя прослойку в виде событий, которые являются чистым бесполезным бойлерплейтом. Все, что нужно - вызвать функцию, она меняет нужный параметр в сторе, автоматически происходит эффективный перерендеринг.

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

У меня обычно в проектах разные интерфейсы для renderWait и renderEmpty, поэтому просто пишу get renderWait() { return <SomeMarkup /> } и в самом render() { if (isLoading) return this.renderWait; } . В целом тут только для очень специфических кейсов пригодится унификация и наследование, и если унифицировано - то лучше вынести на уровень выше, чтобы верхний компонент показывал соответствующую разметку, не раздувая нижние компоненты.

А разные типы одного компонента с общим функционалом сделал бы через class BranchWrapper{ render() { return Children.clone(this.props.children, { getChildNode: () => logic; }) } } . Так что варианты есть. Хотя это как раз React way, по которому не очень хочется идти, ООП довольно сильно усложняет восприятие, на мой взгляд. А еще лучше - через контекст, чтобы совсем отвязаться от пути Реакта - class SomeBranch { handleButtonClick = () => this.context.getNodes(this.props.branchId) }. Думаю, так проще, чем наследовать, но у каждого свой путь)

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

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

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

  • Оформление компонентов в классы

  • 3 метода жизненного цикла - componentWillMount (для SSR+client), componentDidMount (только client) и componentWillUnmount. Я бы предпочел, чтобы они более семантично и короче назывались, но это не большая проблема

  • Доступ к context (некоему глобальному объекту) из любого компонента без дополнительных импортов

  • Грамотно сделанный HTML-in-JS шаблонизатор со скоупом функции и JS-in-HTML

  • (иногда) Рефы, чтобы не генерировать уникальные идентификаторы для определенных частей компонента

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

SWC кстати стал отличным компилятором, перевел уже несколько проектов на него в качестве лоадера для Webpack. Умеет многое из того, что умеет babel, включая полифиллы по usage исходя из browserslist, при этом работает быстрее на 20-25% в моих кейсах. Но плохо умеет в "сахарные" декораторы, приходится переписывать на функциональные, и нет плагина для оптимизации размера lodash (в случае если делается `import lodash fom 'lodash'; lodash.get()`). Минификация еще намного быстрее, чем в Terser. В целом - вполне production-ready.

Недавно было найдено решение https://github.com/DmitryKoterov/conditional-aggregate-webpack-plugin , но есть определенный issue, который я там описал. С доработкой все работает достаточно стабильно, и пересборка блочится пока не будут выполнены необходимые условия, измененные файлы агрегируются.

Asus mb16ac с матовой пленкой, годы таскаю по поездкам к мини пк. Стойки удобные можно из Китая заказать.

Начал вебмастерить сайты в 2004, после того как надоело переустанавливать винду и крякать игры (для школьника норм заработок был), первый крупный продовый проект был на Mambo/Joomla тогда же (школьный сайт с гостевой книгой, блогом, кастомным шаблоном, небезызвестными снежинками и форумом на phpbb). Чуть позже запилил стриминговый радио-сайт (плеер через flash) с трансляцией через Winamp, с друзьями до сих пор иногда запускаем.

После школы уже пошли заказы на сайты на Wordpress, Typo3, OpenCart, ModX, много сайтов-визиток. Параллельно с универом и после него настругал десятки сайтов на CouchCMS (однозначно лучший движок на мой взгляд). Интересные времена были. Как в статье и описано приходилось и в базу, и в хостинг, и в администрирование, и контент, и дизайн, и верстку, и скрипты на php, а тогдашнее сео заставляет вздрагивать (те же публикации на тысячах досок объявлений, чтобы войти в топ по перекрестным ссылкам, или невидимый текст белым на белом). Хотя веб-мастеров было немало, толково сайт, который не разъезжается, кроссбраузерный, и с минимальной резиновостью сделать могли очень немногие.

Иногда жаль, что по навыкам практически ничего с того времени не пригодилось.

Information

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