Skip to content

Instantly share code, notes, and snippets.

@danturu
Last active December 28, 2016 06:30
Show Gist options
  • Save danturu/cddd90d97f46ac37725457abd8d5f569 to your computer and use it in GitHub Desktop.
Save danturu/cddd90d97f46ac37725457abd8d5f569 to your computer and use it in GitHub Desktop.

Twin Components

Я также спросил это на 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.

Path mappings based module resolution

Это больная тема и в 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 */
}

Релативный путь для глобальных композиций не подходит:

  1. Приходиться считать насколько папок вверх нам надо продвинуться.
  2. Если мы переносим этот ИЗОЛИРОВАННЫЙ компонент в другое мест, то его стили сломаются.

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

  1. Это значительнее медленнее, нежели чем, если это рендерит Gulp или Webpack.
  2. Невозможно отрендерить модули css для страниц, которые генерируются на стороне сервера.
  3. Тонна багов в 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])) ...
}

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

Isomorphic Components

Эта тема косвенно касается 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);
      }
    }
  });

Это работает, а изоморфизм это удобно и важно, но я думаю, что существует более изящное решение.

Access Modifiers

В итоге я нашел эту тему на GitHub. Допустим, что у нас есть компонент Button со стилями:

button.css:

.base {}

.flat {
  composes: base;
}

.raised {
  composes: base;
}

.base — приватный класс. В теме на GitHub предлагают добавить акцессор :private. Также я думаю в конвенции должно быть строго сказано какой префикс использовать для приватных классов. Например, в JS/TS мы добавляем _, в SCSS — %.

Naming Convention & Contextual Overides & Theming

Про это мы с тобой уже говорили; на 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 есть несколько странных моментов:

  1. Cмесь BEM и CSS Modules: material_button.css, material_button.tsx.

  2. Смесь Global и Local CSS в статичном лендинге, с использованием дочернего комбинатора >: header.css, header.tsx

Я понимаю, что там что-то не так, но пока не знаю как сделать лучше и правильнее.

Static Sites & Landing Pages

На счет статичных сайтов. В 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.

@ai
Copy link

ai commented Jul 9, 2016

Twin Components

Ответ по существу: А почему компонент auth имеет только CSS? пусть он будет иметь и вёрстку. Тогда перенесёшь всё общее из signin и signup туда и не надо будет дублировать стили через compose.

<Auth title="Sign Up">
  <AuthField name="name"></AuthField>
</Auth>

Если хочешь собирать дизайн «из примитивов» то они должны быть семантичные — прятать какую-то сложность внутри. Например, в Материальном дизайне есть 5 типов теней и «атомы» shadow-1, shadow-2 были бы полезны, так как прятали хитрые параметры теней.

Примечание: вообще не понятно, почему тут и в других вопросах ты спрашиваешь про Sass. Если ты уже используешь парсер PostCSS для CSS Modules, то логичнее взять PreCSS или cssnext для задач сахара в CSS — это будет надёжнее и быстрее (парсинг — самый долгий процесс).

Философский ответ: тут правильно обсудить, как сделать авторизацию, а не как делать форму для почты/пароля. Наличие двух экранов, 2/3 полей для пользователя, утечки безопасности (реальные пользователи часто используют один пароль для всего) — всё это проблемы классичсекой авторизации. правильный ответ на твой вопрос — поставить OAuth через ГитХаб и Фейсбук (как совсем запасной вариант для не программистов). Тогда и кода будет меньше и пользователь довольнее.

@ai
Copy link

ai commented Jul 9, 2016

Path mappings based module resolution

Эмоциональное замечание: «atomic css» — очень плохая идея. Ни я, ни мои уважаемые коллеги по цеху не видят в ней смысла на практике. Эта идея кажется неплохой при первом взгляде, но в долгосрочной перспективе она даже опасна для индустрии. На эту тему можно целый пост написать.

composes: upcase center "../../../../../../lib/type.css"; не имеет никаких преимуществ перед более простым кодом (поддержание простота — главный смысл работы программиста):

text-transform: uppercase;
text-align: center;

Ответ по существу: я бы просто ввёл специальный синтаксис поставив свой плагин перед CSS Modules. Он бы разворачивал:

.flat {
  atoms: upcase center;
}

в (путь указывается в настройке плагина)

composes: upcase center "../../../../../../lib/type.css";

Философский ответ: а почему вообще столько папок назад нужно отойти? Для меня любые стили — это компонент. У меня, например, есть компонент base со шрифтом. Так что и подобные общие базовые вещи лежат где-то рядом. Твой сайт на порядок проще — зачем там такая сложность и хитрая структура папок?

@ai
Copy link

ai commented Jul 9, 2016

Isomorphic Components

Ответ по существу: кстати, всегда можно изменить логику postcss-modules через опцию-коллбэк и класти JSON как-то более удобно.

Философский ответ: а смысл строить такую сложную систему только ради запуска ES6 кода на ноде? Я бы лучше запускал там тот же ES5-код. Если есть какие-то другие мелочи (типа заменить полифил для fetch) сделал бы отдельный файл загрузки всей этой магии и только его бы и подменял (заодно по import fetch from './../isomorphic' видно, что будет магия).

@ai
Copy link

ai commented Jul 9, 2016

Access Modifiers

Ответ по существу: :private как-то нарушает философию CSS — я бы просто вложил их в директиву @private.

Философский ответ: но вообще самая идея мне не очень нравится, какое-то усложнее системы и отход от бритвы Оккама. В примере нужно было просто .base {} заменить на .myButton, .myForm.

@ai
Copy link

ai commented Jul 9, 2016

Naming Convention & Contextual Overides & Theming

Вопрос тем очень большой. Сразу говорю — тут я касаюсь только встроенных в компонент темах (а не тех, которые будут сами писать конечные пользователи).

Ответ по существу: композиция { styles.button styles[theme] } внутри стилей компонента кажется мне нормальным — ты же контроилруешь оба класса. Но чаще всего мне кажется что лучше было бы сделать { styles[theme] || styles.button }, а в .danger { compose: base; color: red } — иногда тема так сильно отличается от оригинала, что приходиться сбрасывать слишком много значений. Плюс в самом CSS ты не видишь, что .danger расширет .button.

@ai
Copy link

ai commented Jul 9, 2016

Static Sites & Landing Pages

Ответ по существу: а мне очень понравилось дробить статичные лэндинги тоже на компоненты. Когда хочется добавить префикс — значит надо разбивать :).

Философский ответ: использование классов .h3 и .ul — плохая практика. А что если SEO-коллега попросит тебя заменить h3 на h1? Получится странный код h1.h3. Лучше держать стили и теги независимыми и назвать селекторы по смыслу .title и .list.

@ai
Copy link

ai commented Jul 9, 2016

Monolithic Landing with mixed Local & Global CSS

Ответ по существу: не, так нельзя, так как разрушена изоляция.

<section className={styles.features}>
  <h3>Feature 1</h3>
  <FeatureExample></FeatureExample>
</section>

Мы не знаем, вдруг FeatureExample тоже будет содержать <h3> и стиль может случайно уйти к нему.

Конечно, можно заменить на > h3, но мне кажется, что просто использовать классы — понятнее и проще.

Философский ответ: на самом главный вопрос тут в том, что у JSX ужасный синтаксис. Ты хочешь сократить до h3, так как тяжело писать className={style.h3}. Есть базовое правило во всех языках — слова, что употребляют чаще, должны быть короче.

Поэтому личноя использую react-jade с хаков, который заменяет .title на class=style.title.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment