Pull to refresh

Comments 64

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

UFO just landed and posted this here
Кажется, что "подкручивание" стилей — это не совсем темизация.

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


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

UFO just landed and posted this here
UFO just landed and posted this here

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


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

UFO just landed and posted this here
Что именно по-старинке? Класс с темой на каждый компонент?

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

UFO just landed and posted this here

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

Вот, кстати да, с переменными. Где та грань, когда достаточно переменную поменять в 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, но по мне, это не совсем то.
Привет. Все ± верно. Мы не делаем embeded api, типа Яндекс Карты API, в которым мы бы решали задачи, как максимально защитить стили. И, иногда, позволяем себе «срезать углы» засчет переопределений.

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


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

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

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

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


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

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


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

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

UFO just landed and posted this here

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

Скорей


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

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

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


this.props.bemHelper()

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


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

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


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

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

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


// 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 вы расматривали такой вариант, если да, то почему отвергнули?

Привет. Писал выше, что особо не выбирали дизайн api декоратора — просто стремились к более короткой записи. Вариант с контекстами имеет право на жизнь.
Про более короткую запись. Не рассматривали подобные варианты?
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 не надо объявлять как аргументы.

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


@overrides({
    'Panel.buttons Button:first-child' : MyButtonLeft ,
    'Panel.buttons Button:last-child' : MyButtonRight ,
})
В 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 генерируется. В композиции автоматически так не сделать, остаются только подобные компромиссы.

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


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

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


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


И так далее

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


У нас много мейнтейнеров и мы стараемся переводить контрибьюторов в этот статус. Сейчас по факту их уже около 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

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

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

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

button button_size_xl button_theme_alfa-on-white attach__button
Привет. Это рудименты от первой чистой БЭМ-реализации дизайн-системы. Сейчас бы мы использовали просто каскад.
Дешево и сердито =)
Sign up to leave a comment.