Comments 64
Отличная статья и достойная реализация, но хотелось бы узнать, как вы решаете задачу темизации? Например, меня всё устраивает, но я хочу немного поменять цвета некоторых элементов, как мне быть, по старинке перебивать CSS или есть возможность из JS (на уровне своего проекта) подкрутить базовые переменные?
Кажется, что "подкручивание" стилей — это не совсем темизация.
А что по вашему темизация? Вы же дальше пишите, что у вас две темы (для светлого фона и для тёмного), вот как именно оно организовано (и что такой фон)? Например взять тот же bootstrap, он темизируется через переопределение sass переменных и последующей генерации новой css'ки.
P.S. Ну и если мне нужно сделать свой outline, это уже темизация.
Ага, значит по старинке, жаль конечно :] но спасибо за ответ.
Кстати, есть ещё один вопрос (хотя возможно вы и не сталкивались, кто знает) но всё же. Как я вижу, вы не используете CSS Modules и чего-то подобного, поэтому как боретесь с переопределением или пересечением ваших классов с уже существующими или «злобными» расширениями, которые спокойно могу заиндектить button {background: blue}
, м?
Что именно по-старинке? Класс с темой на каждый компонент?
Это значит, что я не могу из JS переопределить тот же outline
, а придётся по старинке перебивать его через CSS.
Да практически всегда, хочется не заниматься повышением специфичности CSS и генерированием 101 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'));
Для подмены компонентов не обязательно их объявлять в декораторе и прокидывать в render. Еще можно идентифицировать зависимость не по позиции, а ассоциативно, что на мой взгляд, выглядит более понятно и легче типы проверять.
...
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'));
Еще непонятно, почему к реакту прибивается гвоздями то, что не имеет отношения к компонентам. Тот же DI, алиасинг — никакого отношения к компонентам не имеет.
И как такую штуку заставить работать с типами, ведь строковые ключи в декораторах убивают возможность типизации напрочь. Можно конечно поиграться с Symbol, но по мне, это не совсем то.
У меня противоположный вопрос по css-модулям. Как мне в вышестоящем компоненте таки переопределить компонент на несколько уровней внутри. Есть ли что-то лучше, чем следующие костыли?
.date [class*="calendar"] [class*="day"] { ... }
Дерево компонент: Date > Calendar > Day
Ко мне? Если да, то CSS модули были упомянуты только в контексте изоляции от внешней среды.
Ну а к кому же ещё? Я обрисовал проблему, которая возникает при изоляции стилей. Как с ней бороться?
Ну а конфликты имён можно решать не только изоляцией, но и пространствами имён. А с ними имена классов получаются нормальными человекочитаемыми, а не недетерминированной полуабракадаброй.
Наверно это у вас такая специфика, т.к. в наших зачах такого просто быть не может, компоненты в принципе нельзя расширить через CSS, можно только конфигурировать через его свойства. Да, это не гибко, и да, так задумано, чем-то всегда надо жертвовать. Но как только такая задача появится, её несложно реализовать, передав нужные стили стили через тот же контекст.
Ну а конфликты имён можно решать не только изоляцией, но и пространствами имён
Конфликта нет, но если внешняя и агрессивная среда, браузер, в котором могут быть установлены много неприятных расширений, которые берут и завязываются на ваши прекрасные человеко-читаемые, а главное статичные классы/атрибуты.
То есть сторонние компоненты вы не используете, ясно, только велосипеды.
Но, если у вас true css-modules, то воздействовать на такие компоненты через какой-то внешний CSS не получится, ведь названия классов просто неизвестны.
Если, кратко, то незаменимое преимущество BEM — это простота и то, что css — это просто css, он работает почти так как написан (мы все-таки помогаем себе немного при помощи postcss).
Вероятно эксперименты с другими подходами стилизации будем пробовать, но если упремся в какую-то действительно фатальную проблему подхода.
ru.wikipedia.org/wiki/%D0%A4%D0%B0%D1%88%D0%B8%D0%B7%D0%BC
И раз уж написали, что есть статья, то напишите какая именно статья :)
Про фашизм у Фридмана — это для максимального контраста?
Отличная идея проталкивать генератор bem классов с помощью декоратора. Сделаю у себя так. Но у меня несколько вопросов:
- Почему функция, как аргумент render'а, а не prop?
this.props.bemHelper()
- Зачем наследовать, если blockName можно передать как свойство компонента?
@bemHelper('default-button')
class Button extends Component {...}
// usage
<Button blockName="custom-button" />
- Вы используете чистый 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 и нативным приложением.
```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. Можете посмотреть. Хочу себе сделать такую же как у вас обертку в виде декоратора но с возможностями этого хелпера
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 вы расматривали такой вариант, если да, то почему отвергнули?
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 ,
})
Селекторы не типобезопасно, легко выстрелить в ногу. Можно много вариантов придумать, в 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
— все кнопки на любой глубине во всех селектах
И так далее
<Dropdown Toggler={MyButton} Body={MyBody} />
Это не мешает добавлять default-значения для них, и делать компоненты с другими defaults через HOC. В общем, надо меньше магии.
Styled-components выглядят вкусно. Будущее может быть разным…
Мне импонирует подход Styled-components лаконичностью внешнего API. Не импонирует тем, что это большой черный ящик и то, что это по-прежнему tech-lock на React.
Например, когда в коде пишем import cn from 'arui-feather/cn' или extends React.Component или когда бабел генерирует из JSX код с React.createElement, это не tech-lock?
Просто по мне, не tech-lock, когда в коде приложеня импортов нет совсем, только чистые функции и классы без наследования (POJO), а работоспособность и связывание обеспечивается интроспекцией.
Где граница нормы?
Не импонирует тем, что это большой черный ящик и то, что это по-прежнему tech-lock на React.Вот я и попытался узнать, не импонирует только Styled или вообще вся экосистема реакта (да и фронтенд в целом), т.к. пока не существует фреймворков, полностью построенных на интроспекции, где код приложения не переплетался бы с инфраструктурным кодом, хотя задача интересная и вполне осуществимая.
А если у WebComponents хороший архитектурный дизайн, то в чем это заключается?
Почему тогда столько времени он остается непопулярен? Почему много проблем с масштабированием в том же полимере?
WebComponents разве не очередной vendor lockin, только уже от API браузера. А ведь компоненты — более широкое понятие чем веб, применимое и для мобильных платформ.
Что может быть проще чистых функций и mobx-подобных объектов с данными? При этом у функции есть контракт — описание типов аргументов и зависимостей, в отличие от спецификации шаблона. Такая система почти не зависит от внешнего API и код — это чистая верстка с бизнес логикой, без вкрапления фреймворкового API. Что упрощает запуск ее где-либо еще, кроме браузера.
1. Если речь идет о WebComponents, то он не избавляет от когнитивной нагрузки на программиста: код, кроме бизнес логики содержит мусор в виде конструкций для связывания. Как в WebComponents достичь уровня расширяемости, как у вас со стилями и Button/Popup?
2. В случае работы со стейтом, часто навязывается opinionated подход с actions/reducers, setState.
3. Свобода выбора есть, но выбора vendor lockin. Выбрав что-либо из этого, мы завязываемся на реализацию, а не на спецификацию. И поменять реализацию без переписывания не можем (сколько одинаковых бутстрапов есть на разных фреймворках? А ведь если верстка — чистая функция, можно было бы ее адаптировать ко многим фреймворкам).
Следуя этой логике (завязка на спецификации, а не реализации), можно предположить, что чистые компоненты на JSX и mobx — это vendor lockin в меньшей степени, а спецификации в большей, причем простые: композиция функций и классы без наследования.
Как следствие такой ненавязчивости: mobx лучше масштабируется, появляется выбор: использовать чистый mobx или надстроить над ним mobx-state-tree и получить преимущества redux.
export class TodoElement extends HTMLElement {
...
}
визуальное представление с минимальной логикой компонентов-виджетов.
Скажем, компонент с инпутом и выводом его значения минимальная логика? На чистом WebComponents будет более громоздко по сравнению с mobx и чистым компонентом.
Если рассмотреть технологию с позиции правил для хорошей архитектуры, например 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
БЭМ + React: гибкая архитектура дизайн-системы