Яндекс corporate blog
Web design
Website development
JavaScript
ReactJS
Comments 42
0
Все это конечно очень классно, но вот недавно у меня назревал на работе новый проект, и я очень хотел использовать готовое из мира БЭМ, посмотрел кучу докладов про bem-react-core, уже хотел внедрять, а тут бах… У меня legacy на Django, с шаблонизацией были проблемы, нужен был SSR, преодолею, подумал я… А тут бах… bem-react-core v2 перестал рекомендоваться к использованию, были какие то архитектурные проблемы, ну я взял и запилил свой велосипед, расширил Django шаблонизатор, прикрутил Nunjucks на клиент, реализовал там недостающее, и у меня изоморфный рендеринг, написал простую компонентную систему на js
Да, рантайм слабоват, не все есть из БЭМ, но текущего ф-нала для обычного интернет магазина хватает с головой
+1
К сожалению, мы не предусмотрели проблемы с v2, надеюсь что новая версия решает все ваши трудности. Подробный гайд по использованию нового API мы опубликуем чуть позже. Было бы интересно глянуть на ваше решение для Django.
0
Мое решение находится внутри компании, но вкратце как то так:
Код
шаблоны:
{% load bem %}

{# isomorphic #}
{% with bem_name='product-card' product=content.product %}
    <div class="{{ bem_name }} {{ mods.type.html_class }} {{ mixes }}" data-key="{{ key }}" data-js='[{"name": "{{ bem_name }}"}]'>
        <div class="{% bem_class 'e:about' %}">
            <div class="{% bem_class 'e:catalog' %}">{{ content.catalog.name }}</div>
            <div class="{% bem_class 'e:art' %}">Артикул: {{ product.id }}</div>
            {% render_bem 'b:product-title' mixes 'e:title' content title=product.title href=product.href%}
            {% render_bem 'b:product-description' mixes 'e:description' content description=product.short_description %}
        </div>

            {% slot as link_slot %}
                {# выбор корректной иконки #}
                {% if product.is_new or product.offer_label_src %}
                    {% render_bem 'b:offer-label m:novelty={{product.is_new}}' mixes 'e:offer-label' content src=product.offer_label_src %}
                {% endif %}
                {% render_bem 'b:img' content src=product.img_href alt=product.short_description width='170' height='170' %}
            {% endslot %}
            {% render_bem 'b:link' mixes 'e:img' content slot=link_slot href=product.href %}
      
        <div class="{% bem_class 'e:usage-and-price' %}">
            {% render_bem 'b:product-price' mixes 'e:price' content benefit=product.sale old=product.old_price current=product.price %}
            {% render_bem 'b:product-usage m:size=m' mixes 'e:usage' %}
        </div>
        {% render_bem 'b:product-available' mixes 'e:available' content available_text=content.product.available %}
    </div>
{% endwith %}
{# endisomorphic #}


В js
import "b:icon";
import {ControlToggle} from "b:control-toggle";
import {Block, Elem} from "b:block-system";


const ANIMATION_DURATION = 400;


class ControlExpand extends Block {
  initState() {
    this.state.define('expanded', this.mods.expanded, true);
  }

  beforeElemsInited() {
    let toggleElem = this.elems.findElemByName('control-toggle');
    toggleElem.mergeWithElem(this.elems.findElemByName('control'));
  }

  beforeElemCallbacksFired() {
    let domNodeMaxHeight = this.domNode.css('max-height');
    let initialHeight = (domNodeMaxHeight === 'none') ? '0' : domNodeMaxHeight;
    this.elems.filter('content').delegate('setInitialHeight', initialHeight);
    this.elems.filter('gradient').delegate('setInitialHeight', initialHeight);
    this.elems.filter('content').delegate('collapse');
    this.elems.filter('content').delegate('display');
  }

  onInit() {
    this.domNode.css('max-height', 'none');
  }

  bindEvents() {
    this.onElemChange({
      'control-toggle': {
        'toggled': event => {
          this.state.expanded = event.eventData;

          let defaultControl = this.elems.findElemByKey('control', 'default-control');
          if (defaultControl) {
            defaultControl.setExpandState(this.state.expanded);
          }

          this.elems.filter('show-text').delegate('setExpandState', this.state.expanded);
          this.elems.filter('gradient').delegate('setExpandState', this.state.expanded);
        }
      }
    });

    this.onModChange({
      'expanded:true': () => {
        this.elems.filter('content').delegate('expand');
        this.emit({type: 'expanded'});
      },
      'expanded:false': () => {
        this.elems.filter('content').delegate('collapse');
        setTimeout(() => {
          this.emit({type: 'collapsed'});
        }, ANIMATION_DURATION)
      }
    })
  }

  expand() {
    this.elems.filter('control-toggle').delegate('toggleOn');
  }

  hide() {
    this.elems.filter('control-toggle').delegate('toggleOff');
  }
}


class ControlExpandControl extends Elem {
  /*
    Встроенный контрол в компонент, так же можно прокинуть и любой другой,
    но обработка будет не на этой стороне, по сутки просто чтобы повернуть стрелочку
  * */
  initState() {
    this.state.define('expanded', this.mods.expanded, true);
  }

  setExpandState(newState) {
    this.state.expanded = newState;
  }
}


class ControlExpandContent extends Elem {
  beforeInit() {
    this.initialHeight = null;
  }

  display() {
    this.domNode.css('visibility', 'visible')
  }

  setInitialHeight(height) {
    this.initialHeight = height;
  }

  expand() {
    this.domNode.css('max-height', this.domNode[0].scrollHeight);
  }

  collapse() {
    this.domNode.css('max-height', this.initialHeight);
  }
}


class ControlExpandShowText extends Elem {
  /*
    Встроенный текст контрола, тут меняется текстовочка
  * */
  setExpandState(newState) {
    let newText = (newState) ? 'Скрыть' : 'Показать';
    this.domNode.text(newText);
  }
}


class ControlExpandGradient extends Elem {
  initState() {
    this.state.define('expanded', this.mods.expanded, true);
  }

  setInitialHeight(height) {
    this.domNode.css('height', height);
  }

  setExpandState(newState) {
    if (newState) {
      this.state.expanded = newState;
    }
    else {
      setTimeout(() => {
        this.state.expanded = newState;
      }, ANIMATION_DURATION)
    }
  }
}


Block.register(ControlExpand);
Elem.register(ControlExpandControl);
Elem.register(ControlExpandContent);
Elem.register(ControlExpandShowText);
Elem.register(ControlExpandGradient);

0
Возможно, в вашем случае, это лучше чем библиотека на JS, если нет особой динамики. Но я очень рекомендую присмотреться к новой версии.
+1
БЭМ это конечно хорошо, но мой личный опыт и опыт нескольких команд, в которых мне довелось работать, говорит вот что — БЭМ хорош, когда у вас есть ресурсы на поддержку этой методологии, если вы молодой стартап или небольшая команда, решающая большую задачу в короткий срок, он вас съест, он съест ваше время, ресурсы и заставит рутинно работать на благо будущего, котрое может не наступить (иногда именно из-за затянутых сроков разработки).

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

Я еще не видел команду, которая в здравом уме в 2018 взяла на вооружение БЭМ в реакт приложении, хотя я не исключаю большой потенциал этой методологии работаботки, однако, как я выше уже писал, вам нужны ресурсы и время, а у молодых команд иногда нет либо одного, либо второго, либо всего и сразу.
+1
Думаю, можно взять и БЭМ на вооружение, но большого смысла нет. Старый добрый CSS Modules покрывает большинство нужд. И даже здесь непонятно, чем эти велосипеды лучше банального JSX+CSS Modules.
+1
В случаях масштаба яндекса я в принципе могу в теории понять, почему CSS Modules им может быть неудобен: CSS Modules «из коробки» не умеет в перекрытие стилей сверху (из HOC или вообще с уровня приложения), а любые решения на перекрытия стилей «врукопашную», как делает тот же react-css-themr (и как можно сделать самому, там идея и код абсолютно тривиальны) — будут заведомо более медленными голого цсс.
0
Варианты есть. Можно тупо пробрасывать классы через пропсы, но это несколько громоздкое и ненадёжное решение. Можно использовать темы через контекст. Можно просто создавать разные компоненты с разными темами.

А вообще нарушать инкапсуляцию — это уже не про БЭМ. Если речь, например, о расположении элементов, то компоненты должны быть отдельно «вещью в себе», а позиционирование и отступы задаваться отдельно.
+3
Можно тупо пробрасывать классы через пропсы, но это несколько громоздкое и ненадёжное решение. Можно использовать темы через контекст. Можно просто создавать разные компоненты с разными темами.


Всё это просто не будет работать, если вы намерены как и мы шарить бандлы компонентов между сервисами. Мы не можем себе позволить 200 раз загружать одно и тоже на разных сервисах. Почему бы не использовать механизмы браузера для ускорения своих сервисов, вместо того чтобы слепо использовать какие-либо решения.
+3
У нас есть противоположный опыт и будем рады им поделится. Не очень понятно какие ресурсы на какую поддержку ты имеешь в виду. Суть методологии как раз в том, что её не надо поддерживать – её надо использовать. Для поддержки есть мы. Обращайтесь напрямую к нам через issue на Github или пишите в Telegram-канал.
0

Интересно, но дока очень (ОЧЕНЬ!) краткая, и нет примеров использования, хотя бы в виде демо-проекта.

+4
Мы обещаем исправится! Мы постараемся сделать её более полезной.
0
к середине статьи у меня возник только один вопрос: а зачем яндексу серверный рендеринг????
+4
Конец немного предсказуем. Где-то с середины статьи было такое чувство, что в ней будет раскрыт и этот вопрос. Так и вышло.

Серверный рендеринг позволяет пользователям получать результат быстрее: не тащить на клиент шаблоны, не ждать пока они отработают, а сразу получить готовую HTML-разметку, которую можно начинать отрисовывать в браузере даже еще до того, как она полностью загрузится.
0
Вплоть до версии 16 React обладал фатальным недостатком. Он был в 10 раз медленнее bem-xjst на сервере.

Очень напомнило классику:
— Армяне лучше, чем евреи!
— Чем лучше?
— Чем евреи!!!
+1
А можно меньше js'а для представления результатов? Мой опыт говорит, что если отключать js на информационных сайтах (т.е. не сайтах-приложениях), то батарейка садится много медленее, страницы открываются быстрее и трафика качается меньше.
+1
Мы постоянно работаем над эффективностью потребления ресурсов устойства. В Яндекс.Браузере например, есть индикатор использования батареи. В данном же случае с Поиском мы загружаем JS-код лениво, когда пользователь уже начал потреблять контент. А «толстый» JS присутствует только там, где есть интерактивные элементы. Как правило, код, который необходим для их работы загружается за каким-либо событием, и практически никогда в лоб.
-3
13 кук, 8 скриптов, 3 XHR — и всё это чтобы выдать результат поиска?

Это не считая запросов на tns-counter (у меня он заблокирован, что там происходит не знаю).

Что-то в интернетах сильно сломано. Почему нельзя просто взять и показать результат?
0

Я так и не понял из статьи зачем бэму Реакт. Вы смотрели как устроен $mol? Там и блочно-элементная метафора есть (только рекурсивная), и переопределения для разных платформ на уровне сборки, и бандлинг только нужного, и кастомизация виджетов под проект, и сами виджеты миниатюрные, и декларативная композиция, и реактивность, и построение онлайн редактора компонента по его коду, и использование любого приложения, как компонента, и ленивый рендеринг, и куча других плюшек. И результирующее приложение получается меньше, чем один только голый Реакт.

+1

Да, только стоимость разработки выше. Несколько лет показали, проект интересный, но не более.

0
Интересно, на чём основаны ваши выводы о стоимости разработки?
И что конкретно устарело в readme?
-1
А, так о том и речь, что движение бэма в сторону $mol снизил бы стоимость разработки. А с Реактом она только ещё больше повысится.
0

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

0
Много историй, много примеров того как что-то сделать неправильно, жирное многоточие в конце, без внятной ссылки на github.com/bem/bem-react и каких либо примеров ну теперь точно правильного подхода.

Обожаю БЕМ с точки зрения CSS, что его js релизации всегда вызывали если не тошноту, то удивление.
+3
Разберем код, который реализует уровни переопределения, в принципе основную задачу
const cnApp = cn('App');
const cnPage = cn('Page');
const cnHeader = cn('Header');
const cnFooter = cn('Footer');
// ^ никто не гарантирует наличия этих ключей, и сигнатуры компонентов

export const App: React.SFC = () => (
    <RegistryConsumer>
        {registries => {
            // Get registry with components
            const registry = registries[cnApp()];
            // ^ а почему бы именно "нужный" регист и не возвращать?
            
            // Get the needed version of the component based on registry
            const Header = registry.get(cnHeader());
            const Footer = registry.get(cnFooter());
            // ^ TypeScript как плакал, так и плачет.

            return(
                <div className={ cnPage() }>
                    <Header /> 
                    <Content />
                    <Footer />
                </div>
            );
        }}
    </RegistryConsumer>
);

Какие другие варианты?
— использование (babel|nodejs requre hooks|webpack loader|webpack resolve) для ресолва «импортов» в нужную тенхлогию.
— повесить хук на `React.createElement` который будет в начале смотреть входящий `type` в `registry`, и если он там есть — заменять на реальное значение что уйдет в реакт (так работает React-Hot-Loader)
— Использовать Proxy|геттер на registry для доступа к нужным элементам. Или просто «конечную» версию регистра, от куда можно просто по ключу прочитать что надо.
export const App: React.SFC = () => (
    <BlockConsumer>
        {({Header, Footer }) => ....
    </BlockConsumer>
);

Осталось за кажром - как работает code splitting/bundle picking. Иметь 100500 имортов в App@mobile.tsx, и далее по дереву. Хотя (я очень надеюсь) эти файла генерятся автоматически на основе структуры директорий (скрыто завесой тайны)
+1
Забегая вперед, скажу, что мы используем Proxy, пример который ты взял, был единственным способом раньше, но если следить за релизами, то будет заметно развитие подхода. Спасибо тебе за предложение разных вариантов работы с реестрами. В следующей статье я расскажу как работаем мы и какие из этих методов нам не подошли и почему.
+1
Как проработавший в Яндексе 6 лет скажу просто — Яндекс совершенно не умеет опенсорс. Ни технически, ни (особенно) с точки документации или сообщества.

Как результат слишком много технологий «заперто» в Яндексе, и особо во внешний мир не уходит. Скажу по честноку — это экономически не выгодно.
Мне через месяц про БЕМ коллегам расказывать. Расказать то раскажу — а вот ссылки на репы будет давать немного стыдно.
+3
Мало кто на российском рынке умеет в опенсорс хорошо. Главное, что мы не перестаем пытаться выносить полезное не только нам в компании, но и другим людям. Делится опытом, вот что главное, и то, что помогает развиваться. Я искренне надеюсь, что в опенсорс у нас это будет получаться всё лучше и лучше.
0
В статье около 5 ссылок на данный репозиторий. Я специально разделил статью на части, потому что практическая часть будет такой же объемной. Читать двойной объем за один раз не так удобно.
0
Не холивара ради, а любопытства из — почему не Angular & БЭМ или Vue.js & БЭМ? Т.е. почему именно Реакт?
0

Скорее всего потому, что react это «просто» библиотека, которую можно использовать достаточно гибко вместе с BEM, в отличие от vue. Ну а angular это фреймворк, как он будет с сочетаться с другим фреймворком (BEM) — не очевидно.

0

На тостере прозвучал интересный вопрос, на который у меня нет даже примерного ответа.


Для чего делают ховеры на js?
При ховере на кнопку "Сохранить на яндекс.диск", на неё вешается класс "button2_hovered_yes" и через него меняется бэкграунд на псевдоэлементе before, почему сделанно имеено так, а не на css?
https://toster.ru/q/602071

Правда, почему? Я не слышал о таких особенностях в БЭМ. Не могу найти причину, по которой так нужно было сделать. Очень интересно.

+1
БЕМ _рекомендует_ использовать один уровень определения селекторов(специфичность), чтобы они друг с другом не дрались. Никаких каскадов.
К сожалению как результат получается «вот это». К счастью это очень хорошо ложиться на модель работы React(точнее state), и дает возможность отобразить компонент в любом нужном состоянии(в стори буке).

Можно пойти еще дальше, от БЕМ к BEViS, где у элемента вообще остается только __один__ класс — github.com/bevis-ui/docs/blob/master/faq/bem-vs-bevis.md. Зачем это нужно хорошо обьясняет вот этот слайд — bevis-ui.github.io/bevis-and-bt-speech/?full#41
+1
БЕМ рекомендует использовать один уровень определения селекторов(специфичность), чтобы они друг с другом не дрались. Никаких каскадов.

Но нет же принципиально разницы, между


.block__element:hover


и


.block__element_hover


только в первом случае, нам не нужно менять DOM, не нужно писать симуляцию hover на js, мы не теряем удобную штуку для отладки в devtools хрома "force element state". Свойства переопределять придется в обоих случаях.
Я на той странице яндекса, нашел только одно частично оправданное использование такой штуки, где нужно было разделить ховер по родителю и ховер по блоку, чтобы они не срабатывали вместе.
Еще там же, много использований обычного :hover.


Не поймите неправильно, я не критикую, мне просто очень интересно разобраться. Спасибо за ответ!

-1
Но нет же принципиально разницы, между...

Во имя консистентности.
hover — частный случай модификации, ведь «логических» стейтов и соответствующих им модификаций может быть гораздо больше, чем это позволяет CSS.
0

Ответ достаточно прост, это по БЭМу и так работает i-bem, логика состояния блока описывается в JS, кроме того, доступность hover может зависеть от других состояний, например disabled и т.п. Поэтому чтоб не писать :not(.disabled):hover, :not(.bla-bla-bal):hover { ... } (ну или отменять эти стили), логику ховера проще завернуть в js.

-1
Прошу помощи.
Хочу понять для себя и выбрать наилучшую практику/стратегию, и никак не могу понять, для чего мне вся инфраструктура БЭМ на сегодняшний момент. Да, идея избавиться от зависимостей — это здорово. Правила для классов, отлично. Но с приходом Angular, React, Vue — это все оказывается несколько лишним — вот к какому выводу я пришел, на протяжении нескольких дней читая мануалы, смотря вебинары и изучая практики. Все что может дать БЭМ оказывается ненужным, если я разрабатываю с использованием vue. Следуя правилу KISS, все это лишнее.
Возможно я не прав, помогите разобраться. Допустим, разрабатываю я некий средней сложности фронт, vue, vuex, vue-router. Для чего бы мне там понадобилось тащить БЭМ?
Only those users with full accounts are able to leave comments. , please.