БЭМ + React: гибкая архитектура дизайн-системы




    Дизайн — это фашизм. Фашизму нужна питательная среда. Он начинает раскрываться в полной мере только на крупных масштабах. Идеальная среда для фашизма — это большая компания с огромным количеством продуктов. Например, Google или… Альфа-Банк. Фашизм априори не гибок…

    Все кнопочки на всех продуктах компании должны носить одинаковые рубашки, только одного номенклатурного цвета #F02823. Любая ссылка также имеет свою униформу: цвет #0A1E32, нижнее подчеркивание на расстоянии 2px. Если мы нажмем на ссылку, она должна незамедлительно выполнить команду — перенести нас на другой раздел приложения. За неподчинение — изгнание из дизайна системы Альфа-Банка в Зеленый Банк или расстрел. И неизвестно, что бы в этом случае выбрала ссылка.

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

    Дизайн — это фашизм во имя Любви


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

    Любой фашизм предполагает идеологию. Любой фашизм предполагает централизованное принятие решений. Для любого фашизма компании необходимо завести Самый Главный Комитет по Цензуре и Унификации или СГКпЦиУ.

    Но, подождите, теперь Альфа-Банк — это бирюзовая компания, в движок которой зашит манифест Agile и Scrum. Это означает, что мы осознанно приняли стратегию, что все решения «зашиты» в команды, а не в комитеты по типу СГКпЦиУ…

    Как сохранить консистентность дизайна и не потерять гибкость разработки?


    Наша библиотека компонентов ARUI Feather базируется на двух хорошо знакомых решениях из мира фронтенда: БЭМ-методологии и React.

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

    Подробнее о том, почему у нас именно БЭМ-методология + React, можно узнать из этого видео с Яндекс.Деньги FrontendMix 2017.

    В основе инженерных решений ARUI Feather лежит философия
    KISS / YAGNI / DRY


    KISS означает, что мы изначально для себя решили избегать сложных решений. Перед нами стояла задача сделать код дизайн-системы, в котором сможет разобраться самостоятельно любая команда. ARUI Feather — это АК-47 мира дизайн-систем. Даже лежа по уши в песке в окопах под Багдадом, вы можете самостоятельно разобрать и собрать ее, не обращаясь в сервис-центр ВМС США.

    Также, следуя YAGNI, мы делаем только необходимое, и последний тренд — мы чаще удаляем компоненты, чем создаем новые, потому что на самом деле многие вещи лежат за зоной ответственности дизайн-системы.

    ARUI Feather — это АК-47 мира дизайн-систем. Даже лежа по уши в песке в окопах под Багдадом, вы можете самостоятельно разобрать и собрать ее, не обращаясь в сервис-центр ВМС США


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

    Технически мы поддерживаем дизайн-систему через наше собственное Open Source решение — cn-decorator, которое позволяет использовать БЭМ-методологии и React вместе.

    Мы используем БЭМ-методологию не в полной реализации,
    исключая из нее миксы и уровни переопределения


    С какими проблемами мы столкнулись при масштабировании дизайн-системы?


    В Альфа-Банке уже более 30 команд, которые разрабатывают своей фронтенд независимо, используя ARUI Feather и cn-decorator.

    У нас нет отдельной выделенной команды, которая сконцентрирована на разработке UI/UX-библиотеки. Разработка ведется по принципам, сложившимся в Open Source: есть мейнтейнеры библиотеки компонентов, есть контрибьюторы и есть, конечно, пользователи. И все эти люди так или иначе участники разных команд. Мы осознанно пошли на этот шаг, чтобы избежать появления в компании узкого звена в виде команды разработки библиотеки, которой другие команды делают заказ и ожидают, когда им помогут.

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

    Далее я расскажу про топ вопросов от команд, которые поступают мейнтейнерам, и как мы их решаем…

    А как вообще мне компонент написать-то?


    Так выглядит самый простой компонент, написанный с использованием cn-decorator.

    import cn from 'cn-decorator';
    
    @cn('button')
    class Button extends React.Component {
        render(cn) {
            return <button className={ cn() } />;
        }
    }
    

    Достаточно просто использовать декоратор @cn и передать название блока, в данном примере ‘button’. Теперь метод render получит свой экземпляр cn, который может быть использован для генерации имен классов. И наш финальный БЭМ-блок в HTML будет выглядеть приблизительно так:

    <button class="button"></button>

    Но ведь в БЭМ-методологии есть еще элементы, модификаторы, миксы и уровни переопределения… Пример чуть сложнее:

    import cn from 'cn-decorator';
    
    @cn('button')
    class Button extends React.Component {
        render(cn) {
            return (
                <button className={ cn({ disabled: true }) }>
                    <span className={ cn('text') }>Text</span>
                </button>
            );
        }
    }
    

    В результате у нас получается следующая верстка:

    
       <button class="button button_disabled">
           <span class="button__text">Text</span>
       </button>
    

    На примере наглядно показано, как cn-decorator умеет обращаться с модификаторами и элементами. Остается добавить немного CSS, и компонент готов!

    Мы тут подумали: если поменять цвет рамочек вот у этой кнопки, то конверсия повысится на 200%! Нам что, кнопку с нуля делать?


    Альфа-Банк — это на 100% продуктовая компания. Наши команды на регулярной основе проводят десятки экспериментов. Иногда даже небольшое изменение цвета рамочки может привести к изменению конверсии.

    Если бы у нас был комитет СГКпЦиУ, то нам бы пришлось вынести решение об таком незначительном эксперименте на его ближайшее собрание, дождаться вердикта и, спустя долгие полгода, все-таки повысить конверсию. Технически мы бы использовали WebComponents и запретили бы любое вмешательство в верстку и API компонента.

    Но жизнь богаче, и каждая из команд имеет полное право на проведение экспериментов с дизайном. Для этого в cn-decorator встроен механизм className proxy

    import Button from 'arui-feather/button';
    
    class App extends React.Component {
        render() {
            return <Button className="my-class" />;
        }
    }
    

    В результате мы получаем следующую верстку:

    
    <button class="button my-class"></button>
    

    Теперь мы можем просто на проекте в селекторе .my-class перекрыть пару свойств нашей кнопки…

    Мы еще подумали и, кажется, знаем, как повысить конверсию на 500%! Но, нам нужна кнопка… Нет, она должна нажиматься как старая, но выглядит-то она совсем по-другому… Нам опять с нуля делать?


    И такое случается. Согласитесь, обидно писать компонент, логика работы которого тебя полностью устраивает, но выглядит он совершенно по-другому. Чуть выше я говорил о том, что мы не любим все паттерны смешивания, а предпочитаем паттерны на основе композиции. Поэтому у нас есть механизм перегрузки имени блока, который позволяет разобрать компонент на составные части: стили и логику.



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



    Достаточно просто передекорировать компонент и написать для него новые стили:

    import cn from 'arui-feather/cn';
    import Button from 'arui-feather/button';
    import './tag-button.css';
    
    @cn('tag-button')
    class TagButton extends Button {};
    

    Результирующая верстка TagButton:

    
    <button class="tag-button tag-button_disabled">
        <span class="tag-button__text">Text</span>
    </button>
    

    Такой несложный паттерн позволяет нам шарить логику компонента между разными представлениями.

    А у нас тут дизайнер нарисовал компонент, который выглядит как Link, но работает как Select… А у вас такого нет! А это повысит конверсию на 1000%!


    Это были простые примеры, но часто наши компоненты составные (помните, мы любим композицию). Например, таким составным компонентов является Select: он состоит из двух компонентов Button и Popup.



    Приблизительно так выглядит код Select:

    import cn from 'arui-feather/cn';
    import Button from 'arui-feather/button';
    import Popup from 'arui-feather/popup';
    
    @cn('select')
    class Select extends React.Component {
        render(cn) {
            return (
                <div className={ cn() }>
                    <Button />
                    <Popup />
                </div>
            );
        }
    }
    

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



    Но у нас модульная система на ES6 modules. Единственная возможность заменить составной компонент — это сделать патч на уровне сборки. Здесь на помощь снова приходит cn-decorator и его фича Dependency Injection Components. Давайте передадим наши составные компоненты через cn:

    import cn from 'arui-feather/cn';
    import Button from 'arui-feather/button';
    import Popup from 'arui-feather/popup';
    
    @cn('select', Button, Popup)
    class Select extends React.Component {
        render(cn, Button, Popup) {
            return (
                <div className={ cn() }>
                    <Button />
                    <Popup />
                </div>
            );
        }
    }
    

    Теперь мы можем сделать собственный Select, заменив в нем Button на наш собственный.

    import cn from 'arui-feather/cn';
    import Select from 'arui-feather/select';
    import Popup from 'arui-feather/popup';
    import MyLinkButton from './my-link-button';
    
    @cn('my-link-select', MyLinkButton, Popup)
    class MyLinkSelect extends Select {};
    



    Ура! Теперь мы можем менять любой составной компонент композиции!

    Теперь вы видели все


    Дизайн-система в большой компании — это не про технологии: это не про холивар Angular vs БЭМ vs React. Дизайн-система — это поиск компромиссов между консистентностью и возможностью проводить быстрые эксперименты. Дизайн-система — это работа с комьюнити и работа с бизнес-требованиями одновременно. Это b2b- и b2c-решение: на одной чаше весов бизнес, который хочет быстро, дешево и качественно, и с другой стороны разработчики, которые хотят гибко, расширяемо, но предсказуемо и надежно.

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

    «Организации, проектирующие системы (здесь имеется в виду более широкое толкование, включающее не только информационные системы), неизбежно производят конструкцию, чья структура является копией структуры взаимодействия внутри самой организации»
    —Закон Конвея

    Наши Open Source-решения:

    ARUI Feather — Библиотека UI-компонентов Альфа Банка
    cn-decorator — Лучший способ использовать БЭМ-методологию с React

    Наши вакансии во фронтенд-разработке и дизайне:

    Дизайнер интерфейсов / Дизайнер цифровых продуктов
    Фронтенд-разработка
    Дизайн

    Альфа-Банк 167,95
    Компания
    Поделиться публикацией
    Комментарии 63
    • 0

      Отличная статья и достойная реализация, но хотелось бы узнать, как вы решаете задачу темизации? Например, меня всё устраивает, но я хочу немного поменять цвета некоторых элементов, как мне быть, по старинке перебивать CSS или есть возможность из JS (на уровне своего проекта) подкрутить базовые переменные?

      • 0

        Кажется, что "подкручивание" стилей — это не совсем темизация. Что же касается тем, то у feather из коробки две темы (для светлого фона и для тёмного), и есть специальный компонент ThemeProvider, который пробрасывает тему к дочерним компонентам.

        • 0
          Кажется, что "подкручивание" стилей — это не совсем темизация.

          А что по вашему темизация? Вы же дальше пишите, что у вас две темы (для светлого фона и для тёмного), вот как именно оно организовано (и что такой фон)? Например взять тот же bootstrap, он темизируется через переопределение sass переменных и последующей генерации новой css'ки.


          P.S. Ну и если мне нужно сделать свой outline, это уже темизация.

    • 0
      вот как именно оно организовано

      предположим, что код выглядит как-то так:


      <ThemeProvider theme='alfa-on-white'>
          <App>
              <Button />
              <Button />
              <Button />
          </App>
      </ThemeProvider>

      ThemeProvider просто передаёт дочернему реакт-контексту параметр theme. А cn-decorator умеет принимать параметр theme из props или контекста, и, например, для компонента App сгенерирует className app app_theme_alfa-on-white, то есть в результате получится такой HTML:


      <div class="app app_theme_alfa-on-white">
          <div class="button button_theme_alfa-on-white"></div>
          <div class="button button_theme_alfa-on-white"></div>
          <div class="button button_theme_alfa-on-white"></div>
      </div>

      Дальше просто CSS всё стилизует основываясь на соответствующий классах. Под темизацией я подразумевал именно это. Если ThemeProvider переключит тему, то всё автоматически перекрасится.


      Если вы на основе cn-decorator сделаете свою библиотеку компонентов, то у вас будут свои темы оформления.


      и что такой фон
      Эмм, фон — это фон — background-color. Просто на тёмном фоне нужны белые компоненты и белый текст, а на цветном — чёрные. Например, на Хабре у панели редактора комментария фон серый, и нужны тёмные кнопки, в данном случае синеватые.

      Никто не запрещает для отдельно взятого компонента переопределить CSS, если того требует дизайн, но бутстраповский подход — есть стандартное оформление, накладываем какой-нибудь red-theme.css, где переписываем все классы — в данном случае выглядит не очень правильно, поскольку есть более удобный механизм.

      • 0

        Ага, значит по старинке, жаль конечно :] но спасибо за ответ.


        Кстати, есть ещё один вопрос (хотя возможно вы и не сталкивались, кто знает) но всё же. Как я вижу, вы не используете CSS Modules и чего-то подобного, поэтому как боретесь с переопределением или пересечением ваших классов с уже существующими или «злобными» расширениями, которые спокойно могу заиндектить button {background: blue}, м?

        • 0
          Ага, значит по старинке, жаль конечно

          Что именно по-старинке? Класс с темой на каждый компонент?


          как боретесь с переопределением или пересечением ваших классов с уже существующими или «злобными» расширениями

          Кажется, что возможность пользователю переопределить стиль на компоненте — это отчасти фича.

          • +1
            Что именно по-старинке? Класс с темой на каждый компонент?

            Это значит, что я не могу из JS переопределить тот же outline, а придётся по старинке перебивать его через CSS.

            • 0
              Это значит, что я не могу из JS переопределить тот же outline

              А не подскажете пример, когда бы это требовалось?

              • +1

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

                • 0
                  Вот, кстати да, с переменными. Где та грань, когда достаточно переменную поменять в css, а когда менять целиком стиль? Много раз задавал себе этот вопрос. Ведь можно рассуждать так: css тесно связан с версткой, а структурные изменения в стилях == структурные изменения в верстке == новый компонент.

                  Я экспериментировал с css-in-js применительно к dependency injection и идее атомов от vintage. У меня стиль — функция с зависимостями от других функций. Причем css реактивно пересоздается при изменении зависимостей.
                  Пример подмены стилей
                  ...
                  class ThemeVars {
                    @mem red = 100
                  }
                  
                  function TodoListTheme(themeVars) {
                    return {
                      wrapper: {
                        background: `rgb(${themeVars.red}, 0, 0)`
                      }
                    }
                  }
                  TodoListTheme.theme = true
                  TodoListTheme.deps = [ThemeVars]
                  
                  function TodoListView({todoList}, {theme, themeVars}) {
                    return <div className={theme.wrapper}>
                      Color via css {store.red}: <input
                        type="range"
                        min="0"
                        max="255"
                        value={themeVars.red}
                        onInput={({target}) => { themeVars.red = Number(target.value) }}
                      />
                  
                      <ul>
                        {todoList.todos.map(todo => 
                           <TodoView
                               todo={todo}
                               key={todo.id} />
                        )}
                      </ul>
                      Tasks left: {todoList.unfinishedTodoCount}
                    </div>
                  }
                  TodoListView.deps = [{theme: TodoListTheme, themeVars: ThemeVars}]
                  const store = new TodoList();
                  
                  ReactDOM.render(<TodoListView todoList={store} />, document.getElementById('mount'));
                  
                  fiddle

                  Для подмены компонентов не обязательно их объявлять в декораторе и прокидывать в render. Еще можно идентифицировать зависимость не по позиции, а ассоциативно, что на мой взгляд, выглядит более понятно и легче типы проверять.
                  Пример подмены компонент с reactive-di
                  ...
                  function SomeView() {
                    return 'SomeView'
                  }
                  
                  function TodoView({todo}) {
                      return <li>
                          <input
                              type="checkbox"
                              checked={todo.finished}
                              onClick={() => todo.finished = !todo.finished}
                          />{todo.title} #{todo.id}
                          <br/><SomeView/>
                      </li>
                  }
                  
                  function MySomeView() {
                    return 'MySomeView'
                  }
                  
                  
                  const ClonedTodoView = cloneComponent(TodoView, [
                    [SomeView, MySomeView]
                  ])
                  
                  const TodoListViewCloned = cloneComponent(TodoListView, [
                    [TodoView, ClonedTodoView]
                  ])
                  const todoList = new TodoList();
                  ReactDOM.render(<TodoListViewCloned todoList={todoList} />, document.getElementById('mount'));
                  

                  fiddle

                  Еще непонятно, почему к реакту прибивается гвоздями то, что не имеет отношения к компонентам. Тот же DI, алиасинг — никакого отношения к компонентам не имеет.

                  И как такую штуку заставить работать с типами, ведь строковые ключи в декораторах убивают возможность типизации напрочь. Можно конечно поиграться с Symbol, но по мне, это не совсем то.
            • +1
              Привет. Все ± верно. Мы не делаем embeded api, типа Яндекс Карты API, в которым мы бы решали задачи, как максимально защитить стили. И, иногда, позволяем себе «срезать углы» засчет переопределений.
            • 0

              У меня противоположный вопрос по css-модулям. Как мне в вышестоящем компоненте таки переопределить компонент на несколько уровней внутри. Есть ли что-то лучше, чем следующие костыли?


              .date [class*="calendar"] [class*="day"] { ... }

              Дерево компонент: Date > Calendar > Day

              • 0

                Ко мне? Если да, то CSS модули были упомянуты только в контексте изоляции от внешней среды.

                • 0

                  Ну а к кому же ещё? Я обрисовал проблему, которая возникает при изоляции стилей. Как с ней бороться?


                  Ну а конфликты имён можно решать не только изоляцией, но и пространствами имён. А с ними имена классов получаются нормальными человекочитаемыми, а не недетерминированной полуабракадаброй.

                  • 0

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


                    Ну а конфликты имён можно решать не только изоляцией, но и пространствами имён

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

                    • 0
                      Конфликта нет, но если внешняя и агрессивная среда, браузер, в котором могут быть установлены много неприятных расширений, которые берут и завязываются на ваши прекрасные человеко-читаемые, а главное статичные классы/атрибуты.

                      К счастью, большинству проектов не нужно бояться адблока.

                      • 0

                        Ну, это не так на самом деле, да и кроме него есть всякие Stylish и другие. Просто когда пользователей больше чем «один», разные сюрпризы всплывают.

                      • 0

                        То есть сторонние компоненты вы не используете, ясно, только велосипеды.

                        • 0

                          Ммм, «гадание на коментам»… вы к чему это? А так да, сторонние либы, они же чьи-то велосипеды, часто не пригодны, из-за невозможности пробросить им классы, хотя и это обойти можно.

                  • +1

                    Но, если у вас true css-modules, то воздействовать на такие компоненты через какой-то внешний CSS не получится, ведь названия классов просто неизвестны.

              • 0
                А вы пробовали использовать css-modules или другие способы инкапсуляции css (jss/styled components/etc.)? Или, возможно, у BEM есть какие-то незаменимые преимущества? Субъективно выглядит это всё намного неудобнее любых других способов
                • +1
                  Привет. Я специально не останавливался на выборе стека технологий, потому что рассказывал уже об этом — можно посмотреть видео доклада: www.youtube.com/watch?v=yfIsPH1jXJc

                  Если, кратко, то незаменимое преимущество BEM — это простота и то, что css — это просто css, он работает почти так как написан (мы все-таки помогаем себе немного при помощи postcss).

                  Вероятно эксперименты с другими подходами стилизации будем пробовать, но если упремся в какую-то действительно фатальную проблему подхода.
                • –5
                  Автора почитать так — «Фашизм это Здорово!». Автор башкой вообще думает? Статья ведь есть… Или ради красного словца ....?
                • 0

                  Про фашизм у Фридмана — это для максимального контраста?

                  • +1
                    Контраст здесь единственный между консистентностью и гибкостью.
                  • 0

                    Отличная идея проталкивать генератор bem классов с помощью декоратора. Сделаю у себя так. Но у меня несколько вопросов:


                    1. Почему функция, как аргумент render'а, а не prop?

                    this.props.bemHelper()

                    1. Зачем наследовать, если blockName можно передать как свойство компонента?

                    @bemHelper('default-button')
                    class Button extends Component {...}
                    // usage
                    <Button blockName="custom-button" />

                    1. Вы используете чистый css с пост-процессорами или пре-процессор у вас тоже какой-то используется (sass, less...)? Как вы проталкиваете переменные в стили, например, фирменный цвет?
                    • 0

                      Простите, цифры должны были идти по порядку, отредактировать уже не даёт. Поправьте, если кто может.


                      У React очень гибкое API. Можно легко перехватывать и переопределять свойства, как это делает, например, react-redux в декораторе connect. Поэтому и интересно, почему не пропсы?

                      • +1
                        Привет. Спасибо!

                        1.
                        Особо не задумывались над дизайном декоратора, когда делали. Хотелось просто получить в `render` максимально короткую запись. Но есть такие запросы от команд — им, кажется, так удобнее. Возможно в будущем добавим честный публичный интерфейс через `this.props`.

                        2.
                        Потребность в DI появилась чуть позже. Опять же хотелось сохранить максимально просту запись без получения конструктора компонентов из `this.props`. Просто добавили добавили проксирование через аргументы, не затрагивая методы `render` всех компонентов.

                        3.
                        Мы используем Postcss с набором плагинов. Проталкиваем просто через css import.

                        Например, вот так выглядят переменные цветов для темы:
                        github.com/alfa-laboratory/arui-feather/blob/master/src/vars/color-theme_alfa-on-color.css

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

                        Помогает по мелочи, когда мы подтягиваем соответствие переменных между WebView и нативным приложением.
                        • 0
                          DI через декоратор — тоже классная идея (очень классная). Но мне кажется, что это уже другая задача. Декоратор для bem должен существовать только для того, чтобы генерировать классы для блоков и элементов. А для DI можно отдельный декоратор написать, который будет заниматься проталкиванием зависимостей в рендер. И никто не запрещает использовать их вместе:

                          ```javascript
                          dic(DependencyOne, DependencyTwo)
                          @bemHelper('block-name')
                          ```

                          Если не прав, поправьте, пожалуйста
                          • +2

                            Скорей


                            @renderInject(bem("input"), Foo, Bar)
                            class ... {
                            }

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

                            • 0

                              У меня немного другая идея. Декоратор bem работает не через аргументы рендера, а через пропсы:


                              this.props.bemHelper()

                              А декоратор DI (или renderInjector) как раз запихивает аргументы в рендер:


                              @renderInject(A, B, C)
                              class Button extends Component {
                                  render(A, B, C) {...}
                              }

                              Таким образом никто никому не мешает. И порядок в данном случае вообще не будет иметь значения.


                              Кстати говоря, мне нравится реализация react-bem-helper. Можете посмотреть. Хочу себе сделать такую же как у вас обертку в виде декоратора но с возможностями этого хелпера

                            • 0
                              На мой взгляд, эта идея не годится для больших приложений. Хороший DI нужен, только если он не прибит к реакту, поддерживает типы и не ломает интерфейс компонент.

                              1. Где гарантия, что аргументы в декораторе расположат в том же порядке, что и в render. Когда их больше 2х-3х, это может больно ударить. Все это напоминает старый добрый require.js.

                              2. Точки расширения тут (переопределяемые компоненты) надо проектировать сразу (перечислять аргументы в render), они не получаются автоматически. Лучше наоборот, по аналогии с классами, где мы если явно не указываем private методы, то можем их переопределить в наследнике.

                              3. Метод render не имеет в flowtype или typescript аргументов. В некоторых клонах реакта, вроде preact, туда приходят 2 аргумента: props и state. А тут еще одна самопальная спецификация: жестко прибиваем к cn, Button и Popup.

                              Для компонент можно попробовать сделать DI через подмену createElement, тогда декораторов не надо. createElement можно использовать как service locator.
                              const aliasMap = new Map([
                                [Button, MyButton]
                              ])
                              
                              function h(el, ...args) {
                                return React.createElement(aliasMap.get(el) || el, ...args)
                              }
                              

                              Еще можно использовать метаданные, сгенерированные бабелом для поиска зависимостей и кидать в контекст реакта.
                              class A { name = 'test' }
                              function MyComponent(props, {a}: {a: A}) {
                                return <div>{a.name}</div>
                              }
                              


                              Можно генерить что-то вроде MyComponent.deps = [{a: A}], а createElement уже по этим данным найдет нужную зависимость. Есть даже плагины вроде babel-plugin-flow-react-proptypes, который подобным занимается, только для других целей.

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

                              • 0

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


                                // Select.js
                                class Select extends React.Component {
                                   render() {
                                      const {cx, Ctrl, Menu) = this.context;
                                      return <div className={cx()}><Ctrl/><Menu/></div>
                                   }
                                }
                                export default inject({
                                   cx: bem("select"),
                                   Ctrl: Button,
                                   Menu: PopUp,
                                })(Select);
                                
                                // SelectWithLink.js
                                return inject({Ctrl: Link})(Select);

                                GREENpoint вы расматривали такой вариант, если да, то почему отвергнули?

                                • +1
                                  Привет. Писал выше, что особо не выбирали дизайн api декоратора — просто стремились к более короткой записи. Вариант с контекстами имеет право на жизнь.
                                  • 0
                                    Про более короткую запись. Не рассматривали подобные варианты?
                                    import bem from 'bem'
                                    const SelectedTheme = bem('SelectedTheme')
                                    
                                    function Select(props, {theme}: {theme: SelectTheme}) {
                                      return <div className={theme}>
                                         <Button />
                                         <Popup />
                                     </div>
                                    }
                                    
                                    const MyLinkSelect = clone(Select, [
                                      [Button, MyButton],
                                      [SelectTheme, MyLinkSelectTheme]
                                    ])
                                    


                                    Здесь можно добиться хорошей типобезопасности, SelectTheme может быть функцией, объектом, классом. Button и Popup не надо объявлять как аргументы.
                                    • 0

                                      У IoC-контейнеров есть типичный косяк: он резолвит зависимости по типу. Но что если у нас есть 2 изначально одинаковые кнопки (Button), а нам нужно левую заменить на MyButtonLeft, а правую на MyButtonRight? Тут уже нужен не просто выбор по типу, а полноценный АОП с выбором по селектору, который может затрагивать: тип, локальное имя, порядковый номер среди братьев, глубина вложенности, специфический родитель и тд и тп. Пример с css селекторами:


                                      @overrides({
                                          'Panel.buttons Button:first-child' : MyButtonLeft ,
                                          'Panel.buttons Button:last-child' : MyButtonRight ,
                                      })
                                      • 0
                                        В IoC не только по типу, это для 80% случаев достаточно типов, в остальных — используются декораторы уточняющие.

                                        Селекторы не типобезопасно, легко выстрелить в ногу. Можно много вариантов придумать, в JSX они будут все корявые. Например, можно сделать уникальные компоненты на основе Button или использовать уточнения:
                                        function Select() {
                                          return <div >
                                             <Button.left />
                                             <Button.right />
                                         </div>
                                        }
                                        const MySelect = clone(Select, [
                                          [Button.left, MyLeftButton],
                                          [Button.right, MyRightButton]
                                        ])
                                        

                                        Button.left — генерирует и кэширует уникальный Button с таким же интерфейсом, но другим id.

                                        У вас в tree — уточнения, это названия методов в классе, который из tree генерируется. В композиции автоматически так не сделать, остаются только подобные компромиссы.
                                        • 0

                                          Почему не сделать? Вполне можно точно так же обязать каждому вложенному компоненту давать уникальное имя в рамках владельца (вместо только лишь key для элементов массивов).


                                          <div>
                                             <Panel id="buttons">
                                               <Button id="ok" />
                                               <Button id="cancel" />
                                             </Panel>
                                           </div>

                                          А в селекторах писать:


                                          Button — все кнопки
                                          Select > #ok — кнопка ok во всех селектах
                                          Select Button — все кнопки на любой глубине во всех селектах


                                          И так далее

                              • 0
                                Зависимости по компонентам лучше все-таки через props передавать — тогда не надо ничего будет наследовать чтобы подменить зависимость, просто передаешь компонент, да и всё:

                                <Dropdown Toggler={MyButton} Body={MyBody} />


                                Это не мешает добавлять default-значения для них, и делать компоненты с другими defaults через HOC. В общем, надо меньше магии.
                          • 0
                            Слишком сложно и нужно ли? На мой взгляд, сейчас самый читаемый, удобный и гибкий вариант для React это styled-components. Забыв вообще, что такое селекторы, ты просто, оперируешь компонентами. Темы, наследование и полная изоляция без головной боли. Уверен, что за этим подходом будущее.
                            • +1
                              Привет.

                              Styled-components выглядят вкусно. Будущее может быть разным…

                              Мне импонирует подход Styled-components лаконичностью внешнего API. Не импонирует тем, что это большой черный ящик и то, что это по-прежнему tech-lock на React.
                              • 0
                                А что значит tech-lock?

                                Например, когда в коде пишем import cn from 'arui-feather/cn' или extends React.Component или когда бабел генерирует из JSX код с React.createElement, это не tech-lock?

                                Просто по мне, не tech-lock, когда в коде приложеня импортов нет совсем, только чистые функции и классы без наследования (POJO), а работоспособность и связывание обеспечивается интроспекцией.
                                • +1
                                  Tech-lock — это, когда ваша дизайн система реализуется только на одной технологии и вы становитесь ограничены в выборе инструментария.
                                  • 0
                                    Разве arui-feather/cn, да и сам реакт — не ограничение в выборе инструментария?
                                    Где граница нормы?
                                    • +1
                                      Да, вы правы: React — это просто один из инструментов.
                                      • 0
                                        Просто вы написали:
                                        Не импонирует тем, что это большой черный ящик и то, что это по-прежнему tech-lock на React.
                                        Вот я и попытался узнать, не импонирует только Styled или вообще вся экосистема реакта (да и фронтенд в целом), т.к. пока не существует фреймворков, полностью построенных на интроспекции, где код приложения не переплетался бы с инфраструктурным кодом, хотя задача интересная и вполне осуществимая.
                                        • +1
                                          Если говорить про дизайн системы возможно самое близкое к правде решение — это WebComponents.
                                          • +1
                                            А почему, можно поинтересоваться? Только потому, что это типа стандарт?

                                            А если у WebComponents хороший архитектурный дизайн, то в чем это заключается?

                                            Почему тогда столько времени он остается непопулярен? Почему много проблем с масштабированием в том же полимере?

                                            WebComponents разве не очередной vendor lockin, только уже от API браузера. А ведь компоненты — более широкое понятие чем веб, применимое и для мобильных платформ.

                                            Что может быть проще чистых функций и mobx-подобных объектов с данными? При этом у функции есть контракт — описание типов аргументов и зависимостей, в отличие от спецификации шаблона. Такая система почти не зависит от внешнего API и код — это чистая верстка с бизнес логикой, без вкрапления фреймворкового API. Что упрощает запуск ее где-либо еще, кроме браузера.
                                            • 0
                                              Пока дизайн-система живет в браузерах нас не должно смущать, что мы залочены на его стандартизированные api. Но мы становимся полностью свободными в выборе решений для построения всего остального приложения: Angular 1, Angular 2, React, Vue, mobx, redux, flux — можно смешать со всем.
                                              • +1
                                                А почему не должно смущать и в любом ли месте приложения?

                                                1. Если речь идет о WebComponents, то он не избавляет от когнитивной нагрузки на программиста: код, кроме бизнес логики содержит мусор в виде конструкций для связывания. Как в WebComponents достичь уровня расширяемости, как у вас со стилями и Button/Popup?

                                                2. В случае работы со стейтом, часто навязывается opinionated подход с actions/reducers, setState.

                                                3. Свобода выбора есть, но выбора vendor lockin. Выбрав что-либо из этого, мы завязываемся на реализацию, а не на спецификацию. И поменять реализацию без переписывания не можем (сколько одинаковых бутстрапов есть на разных фреймворках? А ведь если верстка — чистая функция, можно было бы ее адаптировать ко многим фреймворкам).

                                                Следуя этой логике (завязка на спецификации, а не реализации), можно предположить, что чистые компоненты на JSX и mobx — это vendor lockin в меньшей степени, а спецификации в большей, причем простые: композиция функций и классы без наследования.

                                                Как следствие такой ненавязчивости: mobx лучше масштабируется, появляется выбор: использовать чистый mobx или надстроить над ним mobx-state-tree и получить преимущества redux.
                                                • 0
                                                  Вы сами ответили на свой вопрос — WebComponents — это спецификация, а не реализация. И я говорю только про слой, за который отвечает дизайн-система — визуальное представление с минимальной логикой компонентов-виджетов.
                                                  • 0
                                                    Может я что-то не понимаю, но как тогда создать компонент без наследования от HTMLElement. Это разве не прямая зависимость от реализации?
                                                    export class TodoElement extends HTMLElement {
                                                     ...
                                                    }
                                                    


                                                    визуальное представление с минимальной логикой компонентов-виджетов.

                                                    Скажем, компонент с инпутом и выводом его значения минимальная логика? На чистом WebComponents будет более громоздко по сравнению с mobx и чистым компонентом.
                                                    • 0
                                                      Здесь можно поиграть словами про реализацию vs спецификацию. Но по-сути все-равно. Пока ваша дизайн-система живет в браузере и, каждый, браузер реализует спецификацию HTMLElement. Нас не должно это беспокоить.
                                                      • 0
                                                        Тут есть куча нюансов. Производительность, масштабируемость, читаемость.

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

                                                        Как, например, делать компоненты открытыми для расширения, закрытыми для модификации? Представление верстки в виде шаблона, композиции элементов — не дает ответ на этот вопрос.

                                                        По мне, надо всегда задумываться над тем, что приходится использовать, не важно кто реализует, браузер или кто-то еще.

                                                        Если выбирать между чистым кодом, не зависимым от окружения вовсе и кодом зависимым как-либо, то первое предпочтительнее, при прочих равных.
                                                        • 0
                                                          Да, вы правы. Об этом и был мой пост, что вы должны выбрать, то что актуально для решения вашей задачи в вашей компании. Там же находится ваша собственная граница нормы гибкости.
                            • 0
                              Привет! Есть несколько нетехнических вопросов…

                              — Как мейнтейнеры библиотек находят свободное время для их поддержки, не занятое продуктовыми задачами?
                              — При выпуске новой версии библиотеки каким образом она попадает в места использования старой версии?
                              — Как пользователи, желающие использовать компонент интерфейса, могут узнать, что он уже реализован?
                              — При доработке готового компонента интерфейса как контролируется, что не будут сломаны места, где он используется?
                              • +1
                                Привет!

                                — Как мейнтейнеры библиотек находят свободное время для их поддержки, не занятое продуктовыми задачами?


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

                                — При выпуске новой версии библиотеки каким образом она попадает в места использования старой версии?


                                Мы дистрибьютируемся через npm и старательно следим за semver.

                                — Как пользователи, желающие использовать компонент интерфейса, могут узнать, что он уже реализован?


                                Либо посмотреть на демо странице alfa-laboratory.github.io/arui-feather/styleguide
                                Либо, если компонент родился на продукте, но не был занесен в библиотеку — может просто увидеть на готовом продукте, найти команду, которая его реализовала и забрать в библиотеку.

                                — При доработке готового компонента интерфейса как контролируется, что не будут сломаны места, где он используется?


                                1. Unit тесты
                                2. Регрессионные тесты скриншотами
                                3. Жесткое следование semver
                                4. Жесткое следование deprecation policy github.com/alfa-laboratory/arui-feather/blob/master/DEPRECATION_POLICY.md

                                И, да, иногда ломаем обратную совместимость.
                              • 0
                                Привет! Спасибо за текст, весьма своевременно =)

                                используем БЭМ-методологию не в полной реализации, исключая из нее миксы

                                Видимо, имеются ввиду только миксы «блок — блок»? Потому что без миксования «элемент — блок» не понятно, как располагать компоненты (оборачивать?). Вы вроде миксуете:

                                button button_size_xl button_theme_alfa-on-white attach__button
                                • +1
                                  Привет. Это рудименты от первой чистой БЭМ-реализации дизайн-системы. Сейчас бы мы использовали просто каскад.
                                  • 0
                                    Дешево и сердито =)

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

                                Самое читаемое