Я также спросил это на GitHub. Предположим, есть компоненты Signin
и Signup
. Они близнецы:
Signup:
<main>
<h1 className={styles.h1}>Sign Up</h1>
<form className={styles.form}>
...
</form>
Signin:
<main>
<h1 className={styles.h1}>Sign Up</h1>
<form className={styles.form}>
...
</form>
Разница в разметке этих компонентов заключается только в том, что в форме Signup
есть дополнительные поля, например: name
. Следуя конвенции один компонент / один модуль со стилями:
auth.css:
.h1 {
...
}
.form {
...
}
signup.css
.h1 {
composes: h1 from "./auth.css";
}
.form {
composes: form from "./auth.css";
}
signin.css
.h1 {
composes: h1 from "./auth.css";
}
.form {
composes: form from "./auth.css";
}
Вопрос:
В этом и других подобных случаях, зная, что компоненты сейчас и в будущем будут с минимальными различиями, можно ли использовать общий auth.css
и не создавать дополнительные signup.css
и signin.css
, которые полностью зеркальны auth.css
?
Исследование:
В некоторых обсуждениях мейнтейнеры css-modules
и react-css-modules
предлагают использовать Sass. Косвенно здесь это также применимо, с возможностью переопределять свойства:
auth.css:
@mixin auth() {
.h1 {
...
}
.form {
...
}
}
signin.css:
@include auth();
.h1 {
color: black; /* possible future overrides */
}
Мысли:
Использование Sass выглядит странно, когда есть композиции. Я думаю, что sass вообще не нужен в СSS Modules за исключением некоторых полезных функций, например: nested rules для псевдоклассов :hover
, :active
, :focus
, :before
, :after
, комбинаторов +
, >
и color functions для работы с цветом. Я склоняюсь в пользу полного зеркалирования или к использованию итеративного подхода: пока стили нестабильны и при уверенности, что в гипотетическом будущем такие компоненты так и останутся близнецами не дробить auth.css
на signin.css
и signup.css
.
Это больная тема и в TypeScript (в версии 2.0 проблема должна решиться). Опишу ситуацию, когда проект частично или полностью следует концепции atomic css.
Предположим, есть изолированный модуль forms
для построения форм с множеством кнопок FlatButton
и PlainButton
. Создадим стили flat_button.css
и plain_button.css
и произведем рефакторинг. Так как основа всех кнопок может быть общая, то имеет смысл выделить некоторые общие свойства в композицию.
Тогда структура может быть:
/forms/buttons/
- flat_button.jsx
- flat_button.css
- plain_button.jsx
- plain_button.css
- button.css // our ISOLATED button compositions live here
Важно, что button.css
это не глобальные композиции: они должны использоваться только в подмодуле buttons
модуля forms
. В рамках этого компонента, считаю совершенно естественным использование релативных путей (это добавит изолированности нашему модулю):
button.css:
.button {
...
}
.danger {
/* some colors */
}
flat_button.css:
.flat {
...
}
.danger {
composes: flat;
composes: button danger from "./button.css"; /* here relative path is ok since we're heading to an isolate component */
}
Предположим, что текст кнопки должен быть заглавным и отцентрован. Мы двигаемся в сторону атомности, поэтому выделим эти свойства в глобальные общие композиции (пример из Figure), которые будут использоваться в этих и других компонентах:
type.css
.sans {
font-family: "OpenSans", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
}
.mono {
font-family: "Consolas", "Menlo", "Courier", monospace;
}
.light {
font-weight: 300;
}
.regular {
font-weight: 400;
}
.semibold {
font-weight: 500;
}
.left {
text-align: left;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
.upcase {
text-transform: uppercase;
}
Логично, что это следует поместить в общую папку, например, lib
, к которой имеет доступ все компоненты приложения. На данном этапе исходим из того, что модуль forms
не изоморфный и принадлежит стороне клиента.
Тогда структура может быть следующий:
/app/client/components/ui/forms/buttons...
/lib/styles/type.css
Чтобы отцентровать и сделать текст заглавным применим следующую композицию:
flat_button.css:
.flat {
composes: sans upcase center "../../../../../../lib/type.css"; /* relative path here is HELL */
}
.danger {
composes: flat;
composes: button danger from "./button.css"; /* here relative path is ok since we're heading to an isolate component */
}
Релативный путь для глобальных композиций не подходит:
- Приходиться считать насколько папок вверх нам надо продвинуться.
- Если мы переносим этот ИЗОЛИРОВАННЫЙ компонент в другое мест, то его стили сломаются.
Логично, сделать так, что если путь композиции начинается, например, с styles/
срабатывает некая резолюция нахождения правильного пути:
.flat {
composes: sans upcase center "style/type.css"; /* path module resolution works here */
}
Перед тем, как рефакторить Figure я создал playground и попробовал CSS Modules по отдельности c JSPM, Webpack и Gulp. Если JSPM рендерит CSS Modules, то все довольно просто: в конфиг JSPM добавим форвардинг со следующим правилом "styles/*": "lib/styles/*"
.
Но от процессинга CSS Modules средствами SystemJS пришлось быстро отказаться по следующим причинам:
- Это значительнее медленнее, нежели чем, если это рендерит Gulp или Webpack.
- Невозможно отрендерить модули css для страниц, которые генерируются на стороне сервера.
- Тонна багов в jspm-loader-css. Например, я долго отлавливал вот этот баг, который мне в итоге подтвердили.
Поэтому отдадим эту задачу сборщику; в моем случае это Gulp. Первым удивлением было то, что абсолютные пути для композиций не поддерживаются. Поэтому, я применил хак, с которым я не особо счастлив (выдержка из Figure):
function buildCss() {
const paths = (css, opts) => {
const STYLES_REGEX = '@styles';
const relativeStylesPath = `${path.relative(path.dirname(css.source.input.file), '.')}/lib/styles`;
css.walkAtRules('value', rule => {
rule.params = rule.params.replace(STYLES_REGEX, relativeStylesPath);
});
css.walkDecls('composes', decl => {
decl.value = decl.value.replace(STYLES_REGEX, relativeStylesPath);
});
};
// ... pipe(postcss([paths, modules, autoprefixer])) ...
}
Считаю, что здесь имеет место быть более изящному решению из коробки, то есть стандартизированная резолюция нахождения путей.
Эта тема косвенно касается CSS Modules и больше из области процесса сборки проекта, но все же...
Предположим у нас есть изоморфный компонент отображения загрузки Spinner. Скомпилированный код для сервера и клиента различен, так как Node с флагами harmony может работать с чистым ES6, а для браузера необходимо применить транспиллер типа Babel.
Пусть наша структура проекта следующая:
/app
/client
/server
lib/
/ components
- spinner.jsx // isomorphic component
- spinner.css
/ styles
- type.css // global atomic compositions
Сборщик собирает проект в /dist
:
dist/
client/app/...
client/lib/...
server/app/...
server/lib/...
postcss-modules создает json-файл с картой стилей. В моем случае карта для изоморфных компонентов должна находиться и в dist/client
, и в dist/server
одновременно и быть в одной папку с этими компонентами, так как импорт стилей происходит с помощью релативных путей: import * as styles from './spinner.css
.
Поэтому, я опять применил хак (выдержка из Figure):
const modules = postcssModules({
getJSON: (cssPath, json) => {
const relativeCssPath = path.relative('.', cssPath);
const resolveDestPath = (kind) => path.resolve('./', `${path.join(kind, relativeCssPath)}.json`);
if (relativeCssPath.startsWith('app/server')) {
fs.outputJsonSync(resolveDestPath('dist/server'), json);
}
if (relativeCssPath.startsWith('app/client')) {
fs.outputJsonSync(resolveDestPath('dist/client'), json);
}
if (relativeCssPath.startsWith('lib/components')) {
fs.outputJsonSync(resolveDestPath('dist/client'), json);
fs.outputJsonSync(resolveDestPath('dist/server'), json);
}
}
});
Это работает, а изоморфизм это удобно и важно, но я думаю, что существует более изящное решение.
В итоге я нашел эту тему на GitHub. Допустим, что у нас есть компонент Button со стилями:
button.css:
.base {}
.flat {
composes: base;
}
.raised {
composes: base;
}
.base
— приватный класс. В теме на GitHub предлагают добавить акцессор :private
. Также я думаю в конвенции должно быть строго сказано какой префикс использовать для приватных классов. Например, в JS/TS мы добавляем _
, в SCSS — %
.
Про это мы с тобой уже говорили; на GitHub есть предложение. У меня еще нет четких мыслей и заголовки этого блока взаимосвязаны, поэтому напишу, что есть.
Из теории BEM у нас есть блоки button
, элементы button__flat
и модификаторы button--danger
. Как я понимаю стиль элемента в конвенции CSS-Modules должен описываться одним классом с композициями.
Первый момент — как называть псевдо-модификаторы. Я имею ввиду, что это обычная композиция, а не класс который будет добавлен в разметку. CSS Modules говорит использовать camelCase нотацию. Без проблем:
.button {
}
.buttonFlat {
composes: button;
}
.buttonDanger {
color: red;
}
.flatDanger???????????? {
composes: buttonFlat;
composes: buttonDanger;
}
или поскольку мы изолированны:
.button {
}
.flat {
composes: button;
}
.danger {
color: red;
}
.flatDanger???????????? {
composes: buttonFlat;
composes: buttonDanger;
}
В рамках BEM button
— блок, buttonFlat
— элемент, buttonDanger
— модификатор. Но из имени в CSS Modules совершенно не ясно, что есть где
Как я заметил выше, стиль компонента описывается одним классом с композициями, поэтому будет ли следующее использование на примере React-компонента некорректным:
button.jsx:
import * as styles from './button';
const Button = ({ theme = '' }) => (
<button type="button" className={ styles.button styles[theme]}>{children}</button>
)
button.css:
.button {
/* some basic button styles */
}
.danger {
color: red;
}
<Button theme="danger" />
В рефакторинге Figure есть несколько странных моментов:
-
Cмесь BEM и CSS Modules:
material_button.css
,material_button.tsx
. -
Смесь Global и Local CSS в статичном лендинге, с использованием дочернего комбинатора
>
:header.css
,header.tsx
Я понимаю, что там что-то не так, но пока не знаю как сделать лучше и правильнее.
На счет статичных сайтов. В React и Angular мы разбиваем приложение на модули, модули на компоненты, компоненты на подкомпоненты — это логично и удобно. Но в статичных сайтах я нахожу более удобным не дробить все на микрокомпоненты, за исключением header'а, footer'а, и некоторых других блоков. Сравним подходы...
Global CSS Monolithic Landing without BEM
<main className="landing">
<section className="features">
<h3>Feature 1</h3>
<h3>Feature 2</h3>
<h3>Feature 3</h3>
</section>
<section className="conspros">
<h3>Cons</h3>
<ul>...</ul>
<h3>Pros</h3>
<ul>...</ul>
</section>
</main>
main.landing section.features h3 {
color: black;
}
main.landing section.conspros h3 {
color: green;
}
В данном случае, я не вижу смысла и считаю излишним дробить страницу на компоненты Features и ProsCons. Теперь пример CSS Modules, но при важном условии монолитности страницы (компонента).
CSS Modules Monolithic Landing
<main className={styles.landing}>
<section className={styles.features}>
<h3 className={styles.featuresH3}>Feature 1</h3>
<h3 className={styles.featuresH3}>Feature 2</h3>
<h3 className={styles.featuresH3}>Feature 3</h3>
</section>
<section className={styles.conspros}>
<h3 className={styles.consprosH3}>Cons</h3>
<ul className={styles.consprosUl}>...</ul>
<h3 className={styles.consprosH3}>Pros</h3>
<ul className={styles.consprosUl}>...</ul>
</section>
</main>
.features {
}
.featuresH3{
color: black;
}
.conspros {
}
.consprosH3 {
color: green;
}
.consprosUl {
}
В данном случае меня крайне смущает префиксирование и похоже, что тогда теряется смысл CSS Modules. Понятно, что если мы раздробим это на компоненты Features и ProsCons все будет логично и чище:
landing_features.html:
<section className={styles.container}>
<h3 className={styles.h3}>Feature 1</h3>
<h3 className={styles.h3}>Feature 2</h3>
<h3 className={styles.h3}>Feature 3</h3>
</section>
landing_features.css:
.container {
}
.h3 {
color: black;
}
landing_conspros.html:
<section className={styles.container}>
<h3 className={styles.h3}>Cons</h3>
<ul className={styles.ul}>...</ul>
<h3 className={styles.h3}>Pros</h3>
<ul className={styles.ul}>...</ul>
</section>
landing_conspros.css:
.container {
}
.h3 {
color: black;
}
**Monolithic Landing with mixed Local & Global CSS **
И теперь вопрос: имеет ли место быть такому варианту в случаях со статичными сайтами и монолитности?
<main className={styles.landing}>
<section className={styles.features}>
<h3>Feature 1</h3>
<h3>Feature 2</h3>
<h3>Feature 3</h3>
</section>
<section className={styles.conspros}>
<h3>Cons</h3>
<ul>...</ul>
<h3>Pros</h3>
<ul>...</ul>
</section>
</main>
.features {
& :global { /* I don't know why it doesn't pass with : & > :global
h3 {
}
}
}
.features {
& :global {
h3 {
}
ul {
}
}
}
По сути мы изолировали компоненты. Корректен ли этот вариант в рамках изолированности? Именно такой вариант я применил, в месте, где упоминал, что в рефакторинге Figure есть несколько странных моментов.
Я не знаю почему возникает ошибка с использованием дочернего комбинатора
>
.
Мне нравится CSS Modules; я описал именно те моменты, которые мне не понятны или смущают. В иных случаях все отлично. Уверен, многие вопросы разрешатся сами собой — со временем и опытом. Буду дальше использовать CSS Modules и тщательно следить за топиками на GitHub.