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

в 9:09, , рубрики: node.js, react.js, ReactJS, redux, Блог компании Яндекс.Деньги, БЭМ, веб-дизайн, верстальщики, никто не читает теги, Разработка веб-сайтов, рефакторинг, фронтенд, яндекс.деньги

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

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

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

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

Часть 2. Про технологии

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

Бэкенд на Node.js, мой первый раз

Для знакомства с проектом новый разработчик получает тестовое задание. Мне повезло: у меня это была задача из бэклога. И в первый же день я столкнулась с Node.js.

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

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

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

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

И целого стека мало

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

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

Что такое БЭМ

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

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

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

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

Технологии Яндекса

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

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

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

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

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

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

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

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

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

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

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

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

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

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

BEM-XJST

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

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

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

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

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

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

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

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

I-BEM

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

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

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

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

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

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

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

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

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

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

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

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

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

Новый кленовый стек на React

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

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

Библиотеки

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

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

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

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

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

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

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

Технологии

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. Сегодня мы решаем, какой будет новая серверная архитектура.

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

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

Что получилось и стало ли лучше? Давайте разбираться

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

Было

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

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

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

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

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

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

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

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

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

Стало

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

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

Было

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

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

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

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

Стало

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

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

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

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

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

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

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

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

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

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

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

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

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

Неожиданные плюшки

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

CSS in JS

Было

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

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

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

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

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

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

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

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

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

Стало

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

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

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

Типизация

Было

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

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

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

Стало

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

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

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

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

Значит, больше никакого БЭМ?

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

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

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

Я читал первую часть. Что там про верстальщиков?

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

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

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

Всем React!

Автор: ayocommon

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js