Перед вами история интегрирования БЭМ-методологии в React-вселенную. Материал, который вы прочитаете, построен на опыте разработчиков Яндекса, развивающих самый масштабный и нагруженный сервис в России — Яндекс.Поиск. Мы никогда раньше не рассказывали настолько подробно и глубоко о том, почему делали так, а не иначе, что нами двигало и чего мы действительно хотели. Внешнему человеку доставались сухие релизы и обзоры на конференциях. Лишь в кулуарах можно было услышать нечто подобное. Я как соавтор негодовал из-за скудности информации снаружи каждый раз, когда рассказывал о новых версиях библиотек. Но в этот раз мы поделимся всеми подробностями. Все слышали о методологии БЭМ. CSS-селекторы с подчёркиваниями. Компонентный подход, о котором говорят, имея в виду способ написания CSS-селекторов. Но про CSS в статье не будет ни слова. Только JS, только хардкор! Чтобы понимать, почему появилась методология и с какими проблемами тогда столкнулся Яндекс, я рекомендую вам ознакомиться с историей БЭМ. БЭМ и правда зарождался как спасение от сильной связности и вложенности в CSS. Но деление простыней style.css на файлы для каждого блока, элемента или модификатора неизбежно привело к схожему структурированию JavaScript-кода. В 2011 году Open Source обзавёлся первыми коммитами фреймворка i-bem.js, который работал в связке с шаблонизатором bem-xjst. Обе технологии выросли из XSLT и служили популярной тогда идее разделять бизнес-логику и представление компонента. Во внешнем мире это были прекрасные времена Handlebars и Underscore. bem-xjst — шаблонизатор иного типа. Чтобы пополнить знания об архитектуре подходов к шаблонизации, я настоятельно рекомендую доклад Сергея Бережного. А сам шаблонизатор bem-xjst можете попробовать в онлайн-песочнице. В силу специфики поисковых сервисов Яндекса пользовательские интерфейсы строятся по данным. Страница результатов поиска уникальна для каждого запроса. 
Поисковый запрос по ссылке 
Поисковый запрос по ссылке 
Поисковый запрос по ссылке Когда деление на блок, элемент и модификатор распространилось на файловую систему, это позволило максимально эффективно собирать только нужный код, фактически под каждую страницу, под каждый запрос пользователя. Но как? Современная структура директории компонента Как и в некоторых других компаниях, в Яндексе разработчики интерфейсов отвечают за фронтенд, состоящий из клиентской части в браузере и серверной части на Node.js. Серверная часть обрабатывает данные «большого» поиска и накладывает на них шаблоны. Первичная обработка данных преобразует JSON в BEMJSON — структуру данных для шаблонизатора bem-xjst. Шаблонизатор обходит каждый узел дерева и накладывает на него шаблон. Так как первичное преобразование происходит на сервере и благодаря делению на мелкие сущности узлы соответствуют файлам, при шаблонизации мы пушим в браузер код, который будет использован только на текущей странице. Ниже соответствие BEMJSON ноды файлам на файловой системе. За изоляцию компонентов JavaScript-кода в браузере отвечала модульная система YModules. Она позволяет синхронно и асинхронно доставлять модули в браузер. Пример работы компонентов с YModules и i-bem.js можно найти здесь. Сегодня для большинства разработчиков подобное делает модульная система webpack и невышедший стандарт динамических импортов. Набор из БЭМ-методологии, декларативного шаблонизатора и JS-фреймворка с модульной системой позволял решать любую задачу. Но со временем в пользовательские интерфейсы пришла динамика. В 2013 году в Open Source феерично пришёл React. На самом же деле Facebook начал использовать его ещё в 2011 году. Джеймс Лонг (James Long) в своих записях с конференции JS Conf US говорит: React изменил подход к проектированию веб-приложений. Он стал настолько популярен, что сегодня не найти разработчика, который бы не слышал о React. Но важно иное: приложения стали другими, в нашу жизнь пришли SPA. Принято считать, что у разработчиков Яндекса особенное чувство прекрасного в отношении технологий. Порой странное, с чем спорить сложно, но безосновательное — никогда. Когда React набирал звезды на GitHub, многие, кто были знакомы с веб-технологиями Яндекса, настаивали: Facebook победил, бросайте свои поделки и бегите всё переписывать на React, пока не поздно. Тут важно понимать две вещи.  Во-первых, не было войны. Компании не соревнуются в создании лучшего фреймворка на Земле. Если компания начнёт тратить меньше времени (читай — денег) на инфраструктурные задачи при той же продуктивности, все от этого только выиграют. Нет смысла писать фреймворки, чтобы писать фреймворки. Лучшие разработчики создают инструменты, которые решают задачи компании лучшим способом. Компании, сервисы, цели — всё это разное. Отсюда многообразие инструментов. Во-вторых, мы искали способ применять React так, как бы именно нам этого хотелось. Со всеми возможностями, что давали наши технологии, описанные выше. Бытует мнение, что код с использованием React по умолчанию быстрый. Если так считаете и вы, то глубоко заблуждаетесь. Единственное, что делает React, — в большинстве случаев помогает оптимально взаимодействовать с DOM. Вплоть до версии 16 React обладал фатальным недостатком. Он был в 10 раз медленнее bem-xjst на сервере. Мы не могли позволить себе такое расточительство. Время ответа для Яндекса — одна из ключевых метрик. Представьте, на запрос с рецептом глинтвейна вы получите ответ в 10 раз медленнее обычного. Вас не устроят оправдания, даже если вы хоть что-то смыслите в веб-разработке. Что уж говорить об объяснении вроде «зато разработчикам стало удобнее общаться с DOM». Прибавьте сюда соотношение цены внедрения и профита — и вы сами примете единственно верное решение. К счастью ли к горю, разработчики — странные люди. Если что-то не получается, то это совсем не повод всё бросать… Мы были уверены, что можем победить медлительность React. У нас уже есть быстрый шаблонизатор. Всего-то надо на сервере генерировать HTML, используя bem-xjst, а на клиенте «заставить» React принять эту разметку за свою. Идея была настолько проста, что ничто не предвещало провала. В версиях до 15 включительно React валидировал достоверность разметки через хеш-сумму — алгоритм, который любую оптимизацию превращает в тыкву. Чтобы убедить React в валидности разметки, требовалось проставить каждой ноде id и вычислить хеш-сумму всех нод. Также это означало поддержку двойного набора шаблонов: React для клиента и bem-xjst для сервера. Простые тесты на скорость с простановкой id дали понять, что продолжать нет смысла. Шаблонизатор bem-xjst — очень недооценённый инструмент. Посмотрите доклад главного мейнтейнера Славы Олиянчука и сами во всём убедитесь. bem-xjst построен на базе архитектуры, которая позволяет использовать один синтаксис шаблонов для разных преобразований исходного дерева. Очень похоже на React, не так ли? Эта особенность сегодня позволяет существовать таким инструментам, как react-sketchapp. Из коробки bem-xjst содержит два вида преобразований: в HTML и в JSON. Любой достаточно усидчивый разработчик может написать свой движок преобразований шаблонов во что угодно. Мы научили bem-xjst преобразовывать дерево с данными в последовательность вызовов HyperScript-функций. Что означало полную совместимость и с React, и с другими реализациями Virtual DOM алгоритма, например с Preact. Подробный рассказ о подходе с генерацией вызовов HyperScript-функций Поскольку шаблоны React предполагают совместное существование вёрстки и бизнес-логики, нам пришлось принести логику из i-bem.js в свои шаблоны, для этого не предназначенные. Для них это было противоестественно. Они и собирались иначе. Кстати! Ниже пример из глубин склеивания разных миров в одном рантайме. Конечно, у нас была своя сборка. Как известно, самая быстрая операция — это конкатенация строк. На ней построен движок bem-xjst, на ней же строилась сборка. Файлы блоков, элементов и модификаторов лежали по папочкам, и сборке надо было только склеить файлы в правильной последовательности. При таком подходе можно параллельно склеивать JS, CSS и шаблоны, равно как и сами сущности. То есть если у вас в проекте четыре компонента, на ноутбуке четыре ядра, а сборка одной технологии компонента занимает одну секунду, то сборка проекта займёт две секунды. Тут должно становиться понятнее как нам удаётся пушить в браузер только необходимый код. Всё это для нас делал ENB. Итоговое дерево для шаблонизации мы получали только в рантайме, а так как зависимость между компонентами должна была возникнуть немного раньше, чтобы собирать бандлы, то эту функцию на себя взяла мало кому известная технология deps.js. Она позволяла строить граф зависимостей между компонентами, после чего сборщик мог склеить код в нужной последовательности, обходя граф.  Работу в этом направлении прекратил выход React версии 16. Скорости выполнения шаблонов на сервере сравнялись. На мощностях продакшна разница становилась незаметной. Node: v8.4.0
Children: 5K По ссылкам ниже можно восстановить историю подхода: Пробовали ли мы что-то ещё?  В середине рассказа будет нелишним поговорить о том, что нами двигало. Стоило сделать это в начале, но — кто старое помянет, тому глаз в подарок. Зачем нам всё это? Что такого может принести БЭМ, чего не может React? Вопросы, которые задаёт чуть ли не каждый. Функциональность компонентов из года в год усложняется, и количество вариаций увеличивается. Это выражается конструкциями if или switch, в итоге неизбежно растёт кодовая база, как следствие — увеличивается вес компонента и использующего такой компонент проекта. Основная часть логики React-компонента заключена в методе render(). Чтобы изменить функциональность компонента, необходимо переписать большую часть метода, что неизбежно ведёт к экспоненциальному росту количества узкоспециализированных компонентов.  Все знают библиотеки material-ui, fabric-ui и react-bootstrap. В целом у всех известных библиотек с компонентами один и тот же недостаток. Представьте, что у вас несколько проектов и все используют одну библиотеку. Вы берёте одни и те же компоненты, но в разных вариациях: тут селекты с чекбоксами, там без, тут синие кнопки с иконкой, там красные без. Вес CSS и JS, который вам приносит библиотека, во всех проектах будет одинаков. Но почему? Вариации компонентов заэмбежены внутрь самого компонента и поставляются вместе с ним, хотите вы этого или нет. Для нас это неприемлемо. В Яндексе тоже есть своя библиотека с компонентами — Лего. Применяется в ~200 сервисах. Хотим ли мы, чтобы использование Лего в Поиске стоило столько же для Яндекс.Здоровья? Ответ вы знаете. Чтобы поддерживать несколько платформ, чаще всего создают или отдельную версию для каждой платформы, или одну адаптивную. Разработка отдельных версий требует дополнительных ресурсов: чем больше платформ, тем больше усилий. Поддержка синхронного состояния продуктовых свойств в разных версиях вызовет новые сложности. Разработка адаптивной версии усложняет код, увеличивает вес, снижает скорость работы продукта при должной разнице между платформами. Хотим ли мы, чтобы наши родители/друзья/коллеги/дети пользовались десктопными версиями на мобильных с более низкой скоростью интернета и более низкой производительностью? Ответ вы знаете. Если вы разрабатываете проекты для большой аудитории, надо быть уверенными в каждом изменении. A/B-эксперименты — один из способов получить такую уверенность. Способы организации кода для экспериментов: Если в проекте много длительных экспериментов, ветвление кодовой базы вызывает существенные расходы. Надо поддерживать актуальной каждую ветку с экспериментом: портировать исправленные ошибки и продуктовую функциональность. Ветвление кодовой базы кратно усложняет пересекающиеся эксперименты. Точечные условия работают гибче, но усложняют кодовую базу: условия эксперимента могут затрагивать разные части проекта. Большое количество условий ухудшает производительность за счёт увеличения объёма кода для браузера. Надо убирать условия, делать код основным или полностью удалять неудачный эксперимент. В Поиске ~100 экспериментов онлайн в различных комбинациях на различной аудитории. Вы могли это видеть сами. Вспомните, может, вы замечали функциональность, а спустя неделю она магически исчезала. Хотим ли мы проверять продуктовые теории ценой поддержания сотен веток активной кодовой базы в 500 000 строк, которую изменяют ~60 разработчиков ежедневно? Ответ вы знаете. Например, можно создать компонент CustomButton, унаследованный от Button из библиотеки. Но унаследованный CustomButton не применится ко всем компонентам из библиотеки, содержащим Button. В библиотеке может быть компонент Search, построенный из Input и Button. В этом случае внутри компонента Search не появится унаследованный CustomButton. Хотим ли мы вручную обходить всю кодовую базу, где используется Button?  Мы решили изменить стратегию. В предыдущем подходе брали за основу технологии Яндекса и пытались заставить React работать на этой основе. Новая тактика предполагала обратное. Так появился проект bem-react-core. Мы увидели в нём возможность избавиться от явного первоначального рендеринга в HTML и от ручной поддержки состояния JS-компонента уже потом в рантайме — по сути, стало возможным слить в одну технологию BEMHMTL-шаблоны и JS-компоненты. Вначале мы планировали перенести все лучшие практики и свойства bem-xjst в библиотеку поверх React. Первое, что бросается в глаза, — это сигнатура, или, если вам удобнее, синтаксис описания компонентов. Что вы наделали, есть же JSX! Первая версия была построена на базе inherit — библиотеки, которая помогает реализовывать классы и наследование. Как некоторые из вас помнят, в те самые времена апплай прототайпов в JavaScript не было классов, не было super. В общем, их и сейчас нет, точнее, это не те классы, которые в первую очередь приходят на ум. inherit делала всё, что сейчас умеют классы в стандарте ES2015, и то, что принято считать чёрной магией: множественное наследование и слияние прототипов вместо перестроения цепочки, что положительно влияет на производительность. Вы не ошибётесь, если подумаете, что это похоже по смыслу на inherits в Node.js, но работают они по-разному. Ниже пример синтаксиса шаблонов bem-react-core@v1.0.0. App-Header.js App-Header@desktop.js App-Header@touch.js index.js output@desktop.html output@touch.html Устройство шаблонов более сложных компонентов можно посмотреть здесь. Поскольку класс есть объект, а работать с объектами в JavaScript удобнее всего, синтаксис вышел соответствующим. Позже синтаксис перекочевал в своего вдохновителя bem-xjst. Библиотека представляла собой глобальное хранилище из объектныx деклараций — результатов выполнения функции decl, частей сущностей: блока, элемента или модификатора. БЭМ предоставляет механизм уникального именования и поэтому подходит для создания ключей в хранилище. Итоговый React-компонент склеивался по месту своего использования. Хитрость в том, что decl отрабатывал при импорте модуля. Это позволяло указывать, какие части компонента нужны в каждом конкретном месте, с помощью простого списка импортов. Но вспомните: компоненты сложные, частей много, список импортов длинный, разработчики ленивые. Как вы могли заметить, в примерах кода есть строки import AppHeader from 'b:App e:Header'. Вы сломали стандарт! Так нельзя! Это просто не будет работать! Во-первых, стандарт импортов не оперирует терминами в духе «в строке импорта должен быть путь до реально существующего модуля». Во-вторых, это синтаксический сахар, который преобразовывался с помощью Babel. В-третьих, странные конструкции из знаков препинания в импортах для webpack import txt from 'raw-loader!./file.txt'; почему-то никого не смущали.
Итак, наш блок представлен в двух платформах: desktop, touch. Здесь в коде произойдёт последовательный импорт всех определений компонента Hello, а затем вызов функции applyDecls, которая склеит все декларации блока из глобального хранилища через inherit и создаст новый, уникальный для конкретного места в проекте React-компонент. Плагин для Babel, выполняющий такое преобразование, можно найти здесь. А лоадер для webpack, который искал на файловой системе определения компонентов, вот здесь. В итоге, что было хорошо: А это было плохо: Мы учли опыт использования bem-react-core@v1.0.0 в проектах, отзывы и здравый смысл и попробовали снова. В качестве синтаксиса описания блоков, элементов и модификаторов выбрали классы. Классы отличаются декларативной записью, встроенной поддержкой наследования, они просто великолепно работают с TypeScript/Flow. Внимательный читатель заметил, что мы отказались от inherit и «своих» импортов, что означало более удобную отладку, но и более длинную цепочку прототипов со всеми вытекающими последствиями для производительности. Основными задачами были:
— отказаться от дополнительных надстроек в виде лоадеров для webpack и плагинов для Babel;
— максимально приблизиться к привычному всем языку;
— обзавестись нативной поддержкой всех инструментов для отладки, написания и тестирования кода. Объявление модификаторов мы убрали за всем привычные HOC, внутри них создавали новый класс относительно базового и доопределяли нужные методы базового класса. Спустя некоторое время мы нашли архитектурные проблемы в работе модификаторов и целый ряд недостатков, которые невозможно было решить исправлениями. withMods принимал аргументами базовый класс блока и классы, расширяющие базовый (модификаторы), модификаторы обладали предикатом на входящие пропсы. При отрисовке компонентов, как только срабатывает предикат модификатора, withMods перестраивает цепочку прототипов относительно всех активных модификаторов так, чтобы каждый следующий был наследником предыдущего. Так происходит при каждом изменении пропсов. На первой отрисовке не случается никаких проблем, но, как только начинают включаться модификаторы, базовый блок (его прототип) получает функциональность модификатора. В результате все инстансы на странице будут обладать всей функциональностью модификаторов независимо от входящих пропсов. Кейс может повториться и на второй перерисовке, и на третьей, и на четвёртой — в зависимости от того, когда сработают предикаты модификаторов. Решения, которые не помогли: Дополнительные трудности: Мы были вынуждены прервать разработку v2. Естественно, это нас не остановило. Мы написали манифест. Следуя ему, можно было решить проблемы, которые мы встретили в версиях 1 и 2. Ниже я перескажу часть из этого манифеста. Основная мысль — работаем через полную композицию. Работу с CSS-классами и модификаторы выражаем через HOC, а переопределение кода по платформам и экспериментам — через dependency injection. Необходимое и достаточное от БЭМ в React: Модификатор более не сможет влиять на внутреннее устройство компонента. Дополнительная функциональность должна быть выражена через управляющие компоненты сверху. А сами компоненты могут быть выражены как React.ComponentType по необходимости без базовых БЭМ-компонентов. Подключение модификаторов не отличается от подключения любых других HOC и работает через любой compose и в любом порядке. Модификатор определяет истинность предиката и добавит дополнительный класс через пропсы к базовому классу. Переопределение компонентов и их составляющих выражается через dependency injection, которое реализуется на базе React.ContextAPI и множественных реестров компонентов. Каждый компонент волен регистрировать свои зависимости в реестре и позволять переопределять их сверху, что напрямую не заложено в работу стандартного контекста, но реализуется иным способом вычисления нового значения контекста. По умолчанию зависимости можно переопределять по контексту вниз, что есть стандартный механизм работы контекста. В итоге DI — это HOC, который провайдит реестры в контекст. Реестры могут работать в режимах проваливания и всплытия зависимостей. Это позволяет переопределять что угодно, где угодно, на любом уровне вложенности простым дописыванием компонентов в реестр. То, что у нас получилось, уже можно увидеть в продакшене на странице результатов Поиска. Мы уместили всё, что нам было необходимо от БЭМ, в библиотеку из 4 пакетов, общим весом в 1.5Kb.  На этом историческая часть заканчивается. Спасибо тем, кто дочитал до конца. В следующей статье я расскажу, как мы работаем с React в Яндекс.Поиске сегодня. Разработчик интерфейсов