Хабрахабр

Реактивный фронтенд. История о том, как мы снова всё переписали

Привет, это снова Катя из Яндекс.Денег. Продолжаю свою историю о том, как я перестала верстать и начала жить. В первой части я рассказала, как меня сюда занесло и чем занимаются наши фронтендеры. Сегодня — про фронтовый стек, откуда там React и куда делся БЭМ.

Погнали! Спойлер: БЭМ пока никуда не делся ¯\_(ツ)_/¯.

Много текста, картинок и кода, как обещала.
Внимание: высокая концентрация фронтенда.

Далекий 2016. Пытаюсь писать на React, выходит довольно сносно. Еще не подозреваю, что через год буду переводить на React целые сервисы. 2017 начинается в Яндекс.Деньгах, у меня начинается БЭМ головного мозга, и я все еще не подозреваю.
Для знакомства с проектом новый разработчик получает тестовое задание. Мне повезло: у меня это была задача из бэклога. И в первый же день я столкнулась с Node.js.

Задача приложения – оркестрация данных от Java-бэкенда для подготовки во view-ориентированном виде, а также серверный рендеринг и роутинг. Фронтендер в Яндекс.Деньгах отвечает не только за клиентскую часть, но и за серверную прослойку в виде Node.js-приложения. В качестве серверного фреймворка мы используем Express, а для разработки внутренних приложений без завязки на legacy решили использовать Koa2. Сказали бы мне такое пару лет назад, я бы ничего не поняла, а все довольно просто: когда на сервер приходит запрос из браузера, Node.js формирует HTTP-запросы на бэкенд, получает необходимые данные и шаблонизирует веб-странички. Но мы не раскатываем на внешних пользователей код на Koa2: у фреймворка нет достаточной поддержки, зато есть открытые уязвимости. Разработчики полюбили дизайн фреймворка, и мы решили не даунгрейдиться до Express, так Koa2 остался в стеке.

Node.js 8 стала LTS и уже работает на наших production-серверах. Мы уже писали про место Node.js в нашем фронтенде, но с тех пор кое-что изменилось. Также мы хотим отказаться от Nginx-серверов, которые поднимаем на каждом хосте для раздачи статики — их заменят отдельные сервера с Nginx, а когда-нибудь и CDN.

Еще мы используем локальный реестр пакетов и благодаря этому не ходим во внешнюю сеть — это ускоряет сборку и повышает безопасность всей системы. Чтобы шарить код между проектами, но не выкладывать его в открытый доступ, используем целый набор инструментов: храним модули в Bitbucket и собираем их в Jenkins. Любите своих бэкендеров 😉 Такой подход нам подсказали джависты, они классные.

Он помог с кластеризацией, а еще избавил нас от одного старого bash-скрипта, который запускал приложения. А еще мы провели эксперимент — внедрили в одно из приложений диспетчер процессов, который упростил администрирование сервисов на Node.js.

У нас во фронтенде везде javascript. И на сервере, и на клиенте, и под капотом внутренних инструментов. Мы знаем и другие языки, но javascript отлично со всем справляется.

А вот БЭМ в рамках наших задач справляется не со всем.

Что такое БЭМ

БЭМ – подход к веб-разработке, придуманный Яндексом во времена статических HTML-страниц и CSS-каскадов длиною в жизнь. Никакого компонентного подхода еще не было, а поддерживать единообразие множества сервисов было нужно. Яндекс не растерялся и разработал свой собственный компонентный подход, который сегодня позволяет создавать изолированные компоненты и писать гибкий декларативный код.

Часть из них заточена под специфику БЭМ, а некоторые вполне могут использоваться в отрыве от БЭМ-архитектуры. БЭМ не только методология, но и большой набор технологий и библиотек. Если вам в проекте понадобится мощный шаблонизатор или достойный пример компонентной абстракции над DOM, вы знаете, где их можно найти 😉

Поэтому мы начали переводить сервисы на React. Некоторые из них уже живут в двух приложениях, построенных на разных стеках:

– характерная для Яндекса БЭМ платформа;
– молодая и модная экосистема React.

Пришло время рассказать, за что я полюбила БЭМ.

Уровни переопределения

Уровни, уровни, уровни… БЭМ! Профит!
Уровни переопределения – одна из основных фишек БЭМ методологии. Чтобы понять, как они работают, посмотрим на картинку:

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

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

Какие бывают уровни

На картинке выше несколько уровней переопределения:

  • Базовый уровень – библиотека – поставляет исходный модуль кода;
  • Следующий уровень – проект – модифицирует этот модуль под нужды проекта;
  • Уровень выше – платформа – делает тот же модуль специфичным для разных устройств;
  • Вишенка на торте – уровень экспериментов – изменяет модуль для проведения A/B тестирования.

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

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

Уровни позволяют писать сложные модули на основе простых, легко комбинировать поведение и шарить один и тот же код между сервисами. А собирает этот код ENB – Webpack в мире БЭМ.

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

Теперь подробнее рассмотрим инструменты БЭМ платформы, чтобы понять, с чем БЭМ справляется недостаточно хорошо и почему он не подошел для решения наших задач.

BEM-XJST

Начну с любимого – шаблонизатора bem-xjst. До Яндекс.Денег я использовала Jade, и Bem-xjst отлично проиллюстрировал минусы Jade, которых я тогда не видела. Шаблоны bem-xjst декларативные [1], в них нет if hell [2], и они отлично соответствуют требованиям компонентного подхода [3]. Все это хорошо видно на примере:

В песочнице можно посмотреть результат шаблонизации и поиграться с ним.

Как это работает. Внутри – секрет идеальной архитектуры 😉

  • пишем BEMJSON. BEMJSON это JSON, описывающий БЭМ-дерево. БЭМ-дерево это представление DOM-дерева в виде независимых компонент;
  • bem-xjst принимает на вход BEMJSON и применяет шаблоны. Этот процесс можно сравнить с рендерингом в браузере. Браузер обходит DOM-дерево и постепенно применяет CSS правила к его DOM-узлам: размер, цвет текста, отступы. Так же bem-xjst обходит BEMJSON, ищет соответствующие его узлам шаблоны и постепенно применяет их: тег, атрибуты, содержимое. «Применить шаблон» значит сгенерировать из него HTML-строку. Генерацией HTML из BEMJSON занимается один из движков шаблонизатора – BEMHTML.

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

Он использует принципы из XSLT и потому такой декларативный. Чем длиннее ваша борода, тем выше шанс, что вы уже заметили отсылку в названии шаблонизатора: XSLT (eXtensible Stylesheet Language Transformations) => XJST (eXtensible JavaScript Transformations). Если вы не знаете, что такое XSLT, считайте, что вам повезло 🙂

Bem-xjst изоморфный. Мы рендерим HTML страницы на сервере и динамически меняем его на клиенте. Для шаблонизации в рантайме bem-xjst предоставляет API, которым мы пользуемся при написании клиентского javascript-кода.

I-BEM

С помощью bem-xjst мы описываем представление, а логику – с помощью i-bem. I-bem – абстракция над DOM, которая предоставляет высокоуровневый API для работы с компонентами. Проще говоря, позволяет писать это:

вместо этого:

Мы оперируем сущностями, которые описали в шаблоне: все равно, будет это jQuery-селектор или DOM-элемент. Чтобы писать код, не нужно знать про внутреннюю реализацию компонента. Там же описана низкоуровневая логика, а значит мы не нагружаем код с основной логикой лишними проверками. Мы можем создавать кастомные события, подходящие для конкретной объектной модели, а работа с нативными событиями и интерфейсами будет скрыта во внутренней реализации. В итоге, код легко читается и не зависит от конкретной технологии.

Это и есть декларативный javascript. I-bem позволяет описывать логику работы компонента как набор состояний [1]. В i-bem реализован собственный Event Emitter: при изменении состояний компоненты автоматически генерируют события, на которые может подписаться другая компонента [2].

Так выглядит большая часть клиентского javascript-кода на БЭМ:

Как это работает

  • по событию domReady i-bem находит в DOM-дереве компоненты (блоки) и инициализирует их – создает в памяти браузера js-объект, соответствующий блоку;
  • при наступлении нужных событий мы устанавливаем блоку маркеры, отражающие состояние. Роль маркеров выполняют CSS-классы. Например, при клике на input, мы добавляем ему класс «input_focused», который служит маркером;
  • при выставлении таких маркеров i-bem запускает коллбэки, заданные в javascript-реализации блока.

Писать логику просто: нужно описать возможные состояния блока (те самые маркеры) и задать обработчики изменения этих состояний (те самые коллбэки).

С i-bem мы без труда переопределяем поведение компонент, создаем стройный API их взаимодействия и динамически изменяем их в рантайме. Так чего же не хватает?
Мы любим БЭМ за декларативность, легкую масштабируемость и высокоуровневые абстракции, но не готовы больше мириться с его ограничениями. Ниже мы рассмотрим проблему клиентского рендеринга, хранения данных и другие ограничения БЭМ-платформы. Со временем эти проблемы, возможно, будут решены БЭМ-контрибьюторами, но мы не готовы ждать.

Поэтому мы решили перейти на собственный стек. Современный веб c SPA и адаптивностью под мобильные устройства требует адаптивности и от нас. И выбрали React.

React принес в нашу жизнь virtual DOM, hot reload, CSS in JS и большое комьюнити, частью которого мы стали.

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

Библиотеки

Разбиение интерфейсных сущностей на независимые БЭМ-блоки отлично дружит с компонентным подходом React. Разработчики Яндекса написали bem-react-core и перевели на React базовую UI-библиотеку компонент. Мы написали над ней библиотеку-адаптер, которая учитывает специфику этих компонент и поставляет их в качестве HOC:

Такие библиотеки подключаются не в приложении, а в основной библиотеке компонент:

Приложение зависит только от основной библиотеки и получает все компоненты из нее:

Это сокращает количество зависимостей приложения, и библиотеки не попадают в бандл дважды под разными версиями.

Технологии

React не завязан на конкретные технологии и мы сами выбираем инструменты и библиотеки. В моем вооружении есть axios, redux, redux form, redux thunk, styled-components, TypeScript, flow, jest и другие классные штуки. Чтобы не допускать зоопарка, мы согласовываем использование новых технологий с другими разработчиками – отправляем в специальный репозиторий pull-реквест с анализом того, чем полезна технология и почему выбрали именно ее.

Заходит фронтендер в бар, а бармен ему и говорит

Для приложений на React мы создаем платформу, которая объединит библиотеки и процессы для их создания и поддержки. Сердце этой платформы – консольная утилита Frontend Bar. Bar умеет готовить много вкусных штук.

В меню:

  1. Конфиг со льдом: bar смешает и встряхнет ваши yml переменные и приготовит шаблон конфига для ansible.
  2. Сок с ароматом конфигураторов: bar создаст новое приложение на основе модульной заготовки – Juice.
  3. Сет базовых настроек библиотеки. Ожидается в скором времени.

Создать сочное приложение теперь легко – «frontend-bar make juice». Make juice, not war! Когда Bar разворачивает новое приложение, он выполняет набор конфигураций из Juice: генерируется package.json, .babelrc, код ключевых мидлвар и роутов, код корневой компоненты. Frontend Bar облегчит выделение новых микросервисов и поможет соблюдать единые правила написания кода.

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

Спойлер: выберем луковую.

Динамические интерфейсы

Было

Выше я писала, что bem-xjst предоставляет API для шаблонизации в рантайме. I-bem, в свою очередь, умеет работать с DOM-деревом. Подружим их и сможем динамически генерировать и изменять HTML. Попробуем изменить кнопку по событию:


Она добавляет блоку класс, но не применяет шаблон [1]. В этом примере видно слабую сторону БЭМ: i-bem не хочет дружить с bem-xjst и не хочет ничего знать про шаблоны. Мы перерендериваем компонент вручную [2]:

  • описываем новый кусочек БЭМ-дерева [3];
  • затем применяем новый шаблон [4];
  • и инициализируем еще один компонент на текущей DOM-ноде [5].

Кроме того, i-bem не создает diff БЭМ-деревьев, поэтому происходит перерендер всей компоненты, а не изменившихся частей. Рассмотрим простой пример: перерендерить содержимое модального окошка по требованию. Оно состоит из трех элементов:

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

Но i-bem не станет разбираться, что изменилось, полностью перерендерит весь компонент и тоже расслабится. Хочется сделать [1] и расслабиться. Это ухудшает производительность и вызывает неприятные сайд-эффекты: где-то мелькает инпут, где-то зависает бесхозный тултип. В этом-то примере серьезных последствий не будет, но что, если вот так неаккуратно перерендеривать целые формы? Это усложняет разработку, и мы опять грустим. Мы из-за этого грустим и вручную управляем частями компоненты, чтобы сделать точечный перерендер [2].

Стало

React пришел и все разрулил. Он сам отслеживает состояние компонент, мы больше не управляем рендерингом вручную и не задумываемся о взаимодействии с DOM. React содержит имплементацию virtual DOM. При вызове React.createElement создается js-объект DOM-узла с его свойствами и наследниками – virtual DOM этого компонента, который сохраняется внутри React. При изменении компонента React вычисляет новый virtual DOM, а затем diff сохраненного и нового, и обновляет только ту часть DOM, которая изменилась. Все летает, а нам остается лишь оптимизировать сложную логику, используя shouldComponentUpdate. Это успех!

Хранение данных

Было

В БЭМ мы готовим все данные на сервере и передаем их в компоненты страницы:

Мы не сможем получить их на клиенте, поэтому каждый компонент заранее принимает набор данных, которые нужны для всех возможных сценариев его работы. Компоненты изолированы и не будут делиться данными друг с другом, а значит, в разные компоненты придется прокидывать одни и те же данные [1]. Это значит, что мы нагружаем компонент данными, которые могут ему не понадобиться [2].

Поэтому мы написали bem-redux, который адаптирует Redux для БЭМ. Иногда нас выручает глобальная сущность, в которой хранится часть общих данных, но глобальное хранение переменных плохо вписывается в концепцию БЭМ. Он отлично управляет нашими данными в рамках простых интерфейсов, но при разработке сложной компоненты мы упираемся в проблему рендеринга, которую я описала выше. Redux – стейт-менеджер, управляющий потоком данных. Redux не дружит с i-bem, мы фиксим баги и грустим.

Стало

Redux + React = <3
Redux хранит данные для всего приложения в одном месте [1]:

Компонента сама решает, когда и какие данные ей нужны [2]:

Нам нужно только описать сценарии работы компоненты [3] и указать, откуда брать данные для его выполнения [4]:

А React сделает все остальное [5]:

Это успех! Такой подход позволяет следовать принципу единой ответственности и инкапсулировать логику компоненты в самой компоненте, а не размазывать ее в коде страницы.

За все приходится платить

За успех мы расплатились нехилым количеством legacy на React. Больно видеть, как твой код, написанный всего пару месяцев назад, плавно превращается в deprecated.

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

И пока мы учились его готовить, перепачкали всю свою кухню и сами замарались по локоть. React достаточно молод и не готов к enterprise-разработке, в отличие от БЭМ. Пишем как придется и ездим на конференции, чтобы узнать, как надо. И мы по-прежнему ведем споры о том, нужен нам flow или все-таки нет, и все еще не до конца понимаем, что хранить в сторе, а что в локальном стейте. Бьем шишки, но уверенно движемся вперед.

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

CSS in JS

Было

Рассмотрим простой кейс из жизни: раскрасить и анимировать иконку по событию, примерно так:

Кода всего ничего:

Правда, по правилам БЭМ, придется разнести его аж по трем директориям:

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

Тогда мы перепишем СSS-анимацию на jQuery и немного погрустим. А что, если длительность анимации от чего-то зависит и устанавливается динамически?

Стало

Styled-components, я люблю тебя! СSS in JS – one love! Мой внутренний верстальщик ликует:

Приятный бонус нового стека. Модульность сохранилась, CSS-анимация работает и никакой ручной работы с классами.

Типизация

Было

Раньше мы писали тонны jsDoc. Давайте посмотрим, полезен ли он:

Что содержит state? Этот пример взят из продакшен кода. Да, есть readme, но увы, он немного устарел. Понятия не имею. Придется углубиться в код. Да, нам стыдно, но с документацией и комментариями так бывает часто, они ненадежны. Спешим, не углубляемся, ломаем и грустим. Или не углубляться и нечаянно все сломать.

Стало

На помощь пришла типизация. «Тык» на тип, и вся подноготная метода перед глазами. Лень разбираться? Precommit checker запустит flow, и разобраться все равно придется.

Сроки горят, менеджер пингует, а у тебя и там «cannot get property», и тут «property is missing». Я невзлюбила flow с первого взгляда. Примерно вот так: Но недавно мне рассказали, что типами можно проектировать О_о Как проектировать типами?

Flow перестал быть кошмаром. Мой мир перевернулся. Надежный код – приятный бонус! Описывать API модулей типами до написания кода оказалось удобно и полезно.

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

Мы готовим письма на сервере, и описанные выше ограничения БЭМ-платформы не затрагивают это приложение. На БЭМ реализован наш шаблонизатор e-mail рассылки. Использование БЭМ для его разработки – уместное элегантное решение.

И даже если мы перестанем писать на БЭМ, он все равно найдет нас 🙂 К тому же наши дизайнеры прототипируют с помощью БЭМ и иногда приносят нам сверстанные компоненты вместо макетов.

Я участвовала в переводе одного из приложений с БЭМ на React и уяснила важную вещь.

Я не воспринимала всерьез фронтенд-сообщество и его изменчивый мир. До прихода в Яндекс.Деньги я была простым верстальщиком и потратила не один год, верстая тонны HTML и JSX. Не понимала, зачем менять jQuery. Не понимала, зачем изучать первый Angular, чтобы завтра забыть о нем и изучать второй. Ajax на Fetch, чтобы заменить потом Fetch на Axios.

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

Всем React!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть