Дизайн — это фашизм. Фашизму нужна питательная среда. Он начинает раскрываться в полной мере только на крупных масштабах. Идеальная среда для фашизма — это большая компания с огромным количеством продуктов. Например, 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

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

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

Комментарии (60)


  1. RubaXa
    23.10.2017 11:34

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


    1. staticlab
      23.10.2017 12:03

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


      1. RubaXa
        23.10.2017 12:12

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

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


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


        1. staticlab
          23.10.2017 12:33

          Промахнулся комментарием, ответил чуть ниже.


  1. staticlab
    23.10.2017 12:32

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

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


    <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, где переписываем все классы — в данном случае выглядит не очень правильно, поскольку есть более удобный механизм.


    1. RubaXa
      23.10.2017 12:39

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


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


      1. staticlab
        23.10.2017 12:44

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

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


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

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


        1. RubaXa
          23.10.2017 12:50

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

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


          1. staticlab
            23.10.2017 12:52

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

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


            1. RubaXa
              23.10.2017 12:57

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


              1. redyuf
                23.10.2017 14:47

                Вот, кстати да, с переменными. Где та грань, когда достаточно переменную поменять в 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'));
                


        1. GREENpoint Автор
          23.10.2017 12:50

          Привет. Все ± верно. Мы не делаем embeded api, типа Яндекс Карты API, в которым мы бы решали задачи, как максимально защитить стили. И, иногда, позволяем себе «срезать углы» засчет переопределений.


      1. vintage
        23.10.2017 18:36

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


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

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


        1. RubaXa
          23.10.2017 18:43

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


          1. vintage
            23.10.2017 19:43

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


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


            1. RubaXa
              23.10.2017 23:40

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


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

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


              1. staticlab
                23.10.2017 23:56

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

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


                1. RubaXa
                  24.10.2017 00:00

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


              1. vintage
                24.10.2017 00:23

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


                1. RubaXa
                  24.10.2017 01:05

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


        1. RubaXa
          23.10.2017 18:56
          +1

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


  1. drai3
    23.10.2017 12:39

    А вы пробовали использовать css-modules или другие способы инкапсуляции css (jss/styled components/etc.)? Или, возможно, у BEM есть какие-то незаменимые преимущества? Субъективно выглядит это всё намного неудобнее любых других способов


    1. GREENpoint Автор
      23.10.2017 12:42

      Привет. Я специально не останавливался на выборе стека технологий, потому что рассказывал уже об этом — можно посмотреть видео доклада: www.youtube.com/watch?v=yfIsPH1jXJc

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

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


  1. AxianLTD
    23.10.2017 13:14

    Автора почитать так — «Фашизм это Здорово!». Автор башкой вообще думает? Статья ведь есть… Или ради красного словца ....?


    1. ruslangavrilov
      23.10.2017 13:29

      Фашизм понятие широкое.
      ru.wikipedia.org/wiki/%D0%A4%D0%B0%D1%88%D0%B8%D0%B7%D0%BC

      И раз уж написали, что есть статья, то напишите какая именно статья :)


      1. AxianLTD
        23.10.2017 13:32

        Печально… Мозги промыты, сайентологи могут торжествовать. Скоро и России кердык…


  1. comerc
    23.10.2017 15:44

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


    1. GREENpoint Автор
      23.10.2017 15:57
      +1

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


  1. vtvz_ru
    23.10.2017 17:40

    Отличная идея проталкивать генератор 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...)? Как вы проталкиваете переменные в стили, например, фирменный цвет?


    1. vtvz_ru
      23.10.2017 17:49

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


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


    1. GREENpoint Автор
      23.10.2017 17:50
      +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 и нативным приложением.


      1. vtvz_ru
        23.10.2017 18:48

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

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

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


        1. RubaXa
          23.10.2017 19:00
          +2

          Скорей


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

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


          1. vtvz_ru
            23.10.2017 23:53

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


            this.props.bemHelper()

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


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

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


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


        1. redyuf
          23.10.2017 20:13

          На мой взгляд, эта идея не годится для больших приложений. Хороший 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, тут далеко. Но я рад, что хоть кто-то копает в этом направлении для экосистемы реакта.


          1. RubaXa
            23.10.2017 23:29

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


            // 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. GREENpoint Автор
              24.10.2017 10:35
              +1

              Привет. Писал выше, что особо не выбирали дизайн api декоратора — просто стремились к более короткой записи. Вариант с контекстами имеет право на жизнь.


              1. redyuf
                24.10.2017 11:18

                Про более короткую запись. Не рассматривали подобные варианты?

                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 не надо объявлять как аргументы.


                1. vintage
                  24.10.2017 12:11

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


                  @overrides({
                      'Panel.buttons Button:first-child' : MyButtonLeft ,
                      'Panel.buttons Button:last-child' : MyButtonRight ,
                  })


                  1. redyuf
                    24.10.2017 12:39

                    В 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 генерируется. В композиции автоматически так не сделать, остаются только подобные компромиссы.


                    1. vintage
                      24.10.2017 14:41

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


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

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


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


                      И так далее


        1. jakobz
          24.10.2017 10:35

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

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


          Это не мешает добавлять default-значения для них, и делать компоненты с другими defaults через HOC. В общем, надо меньше магии.


  1. rockon404
    24.10.2017 10:36

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


    1. GREENpoint Автор
      24.10.2017 10:39
      +1

      Привет.

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

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


      1. redyuf
        24.10.2017 10:58

        А что значит tech-lock?

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

        Просто по мне, не tech-lock, когда в коде приложеня импортов нет совсем, только чистые функции и классы без наследования (POJO), а работоспособность и связывание обеспечивается интроспекцией.


        1. GREENpoint Автор
          24.10.2017 11:02
          +1

          Tech-lock — это, когда ваша дизайн система реализуется только на одной технологии и вы становитесь ограничены в выборе инструментария.


          1. redyuf
            24.10.2017 11:20

            Разве arui-feather/cn, да и сам реакт — не ограничение в выборе инструментария?
            Где граница нормы?


            1. GREENpoint Автор
              24.10.2017 11:21
              +1

              Да, вы правы: React — это просто один из инструментов.


              1. redyuf
                24.10.2017 11:36

                Просто вы написали:

                Не импонирует тем, что это большой черный ящик и то, что это по-прежнему tech-lock на React.
                Вот я и попытался узнать, не импонирует только Styled или вообще вся экосистема реакта (да и фронтенд в целом), т.к. пока не существует фреймворков, полностью построенных на интроспекции, где код приложения не переплетался бы с инфраструктурным кодом, хотя задача интересная и вполне осуществимая.


                1. GREENpoint Автор
                  24.10.2017 12:24
                  +1

                  Если говорить про дизайн системы возможно самое близкое к правде решение — это WebComponents.


                  1. redyuf
                    24.10.2017 12:56
                    +1

                    А почему, можно поинтересоваться? Только потому, что это типа стандарт?

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

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

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

                    Что может быть проще чистых функций и mobx-подобных объектов с данными? При этом у функции есть контракт — описание типов аргументов и зависимостей, в отличие от спецификации шаблона. Такая система почти не зависит от внешнего API и код — это чистая верстка с бизнес логикой, без вкрапления фреймворкового API. Что упрощает запуск ее где-либо еще, кроме браузера.


                    1. GREENpoint Автор
                      24.10.2017 13:00

                      Пока дизайн-система живет в браузерах нас не должно смущать, что мы залочены на его стандартизированные api. Но мы становимся полностью свободными в выборе решений для построения всего остального приложения: Angular 1, Angular 2, React, Vue, mobx, redux, flux — можно смешать со всем.


                      1. redyuf
                        24.10.2017 13:33
                        +1

                        А почему не должно смущать и в любом ли месте приложения?

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

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

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

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

                        Как следствие такой ненавязчивости: mobx лучше масштабируется, появляется выбор: использовать чистый mobx или надстроить над ним mobx-state-tree и получить преимущества redux.


                        1. GREENpoint Автор
                          24.10.2017 14:09

                          Вы сами ответили на свой вопрос — WebComponents — это спецификация, а не реализация. И я говорю только про слой, за который отвечает дизайн-система — визуальное представление с минимальной логикой компонентов-виджетов.


                          1. redyuf
                            24.10.2017 14:37

                            Может я что-то не понимаю, но как тогда создать компонент без наследования от HTMLElement. Это разве не прямая зависимость от реализации?

                            export class TodoElement extends HTMLElement {
                             ...
                            }
                            


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

                            Скажем, компонент с инпутом и выводом его значения минимальная логика? На чистом WebComponents будет более громоздко по сравнению с mobx и чистым компонентом.


                            1. GREENpoint Автор
                              24.10.2017 14:42

                              Здесь можно поиграть словами про реализацию vs спецификацию. Но по-сути все-равно. Пока ваша дизайн-система живет в браузере и, каждый, браузер реализует спецификацию HTMLElement. Нас не должно это беспокоить.


                              1. redyuf
                                24.10.2017 15:30

                                Тут есть куча нюансов. Производительность, масштабируемость, читаемость.

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

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

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

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


                                1. GREENpoint Автор
                                  24.10.2017 15:42

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


  1. dima117
    24.10.2017 15:03

    Привет! Есть несколько нетехнических вопросов…

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


    1. GREENpoint Автор
      24.10.2017 15:23
      +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

      И, да, иногда ломаем обратную совместимость.