javascript

CSS в JavaScript: будущее компонентных стилей

  • четверг, 1 июня 2017 г. в 03:14:28
https://habrahabr.ru/company/mailru/blog/329710/
  • Разработка веб-сайтов
  • ReactJS
  • JavaScript
  • CSS
  • Блог компании Mail.Ru Group



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


Чтобы получить больше информации о проблемах CSS, решаемых в JavaScript, вы можете посмотреть презентацию «React CSS в JS» (React CSS in JS), а для того чтобы изучить улучшение производительности с помощью Aphrodite, прочитайте статью Inline CSS at Khan Academy: Aphrodite. Если же вы хотите узнать больше о лучших практиках CSS в JavaScript, ознакомьтесь с руководством Airbnb (Airbnb’s styleguide).


Здесь речь пойдет об использовании встроенных стилей JavaScript для создания компонентов, позволяющих решить основные проблемы дизайна, о которых я рассказывал ранее в статье «Прежде чем осваивать дизайн, необходимо ознакомиться с основами» (Before you can master design, you must first master the fundamentals).


Мотивирующий пример


Начнем с простого примера: создание и стилизация кнопки. Как правило, компонент и связанные с ним стили находятся в одном файле: Button и ButtonStyles. Причина в том, что они относятся к одной и той же вещи — к представлению (view). Однако в примере я разбил код на несколько составляющих, чтобы сделать его более наглядным.


Рассмотрим элемент кнопки:


...

function Button(props) {
  return (
    <input
      type="button"
      className={css(styles.button)}
      value={props.text}
    />
  );
}

Ничего необычного — просто React-компонент. Свойство className — вот где Aphrodite вступает в игру. Функция CSS принимает объект styles и преобразует его в CSS. Объект styles создается с помощью функции StyleSheet.create Aphrodite ({...}). Вы можете посмотреть результат StyleSheet.create ({...}) на странице Aphrodite (Aphrodite playground).


Ниже приведена таблица стилей кнопок:


...

const gradient = 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)';

const styles = StyleSheet.create({
  button: {
    background: gradient,
    borderRadius: '3px',
    border: 0,
    color: 'white',
    height: '48px',
    textTransform: 'uppercase',
    padding: '0 25px',
    boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .30)',
  },
});

Простая миграция и низкая кривая обучения — вот преимущества Aphrodite. Свойства border-radius преобразуются в borderRadius, а значения становятся строками. Псевдоселекторы, медиазапросы и определения шрифтов — все работает. Кроме того, автоматически добавляются вендорные префиксы.


В результате получаем:


image
Простая миграция и низкая кривая обучения — преимущества Aphrodite


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


Основной принцип № 1. Типографика


Начнем с фундаментальной основы дизайна — типографики. Для начала необходимо определить ее константы. В отличие от Sass или Less, константы для Aphrodite могут быть в файлах JavaScript или JSON.


Определение констант типографики


При создании констант используйте семантические имена для переменных. Например, для названия одного из размеров шрифта необходимо имя, описывающее его роль, наподобие displayLarge, а не h2. Аналогично, чтобы обозначить одно из значений жирности, берите название вроде semibold, описывающее эффект, а не значение 600.


export const fontSize = {
  // heading
  displayLarge: '32px',
  displayMedium: '26px',
  displaySmall: '20px',
  heading: '18px',
  subheading: '16px',

  // body
  body: '17px',
  caption: '15px',
};

export const fontWeight = {
  bold: 700,
  semibold: 600,
  normal: 400,
  light: 200,
};

export const tagMapping = {
  h1: 'displayLarge',
  h2: 'displayMedium',
  h3: 'displaySmall',
  h4: 'heading',
  h5: 'subheading',
};

export const lineHeight = {
  // heading
  displayLarge: '48px',
  displayMedium: '48px',
  displaySmall: '24px',
  heading: '24px',
  subheading: '24px',

  // body
  body: '24px',
  caption: '24px',
};

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


Более подробно о вертикальном ритме вы можете прочитать в статье «Почему вертикальный ритм — это важная практика типографики?» (Why is Vertical Rhythm an Important Typography Practice?).


image
Используйте калькулятор для определения высоты линии


Существует целая наука о выборе значений для строк и размеров шрифта. Для создания потенциальных размерных вариантов могут использоваться математические соотношения. Несколько недель назад я опубликовал статью «Типографика может создать или разрушить дизайн: выбор типа» (Typography can make or break your design: a process for choosing type), в которой подробно изложена методология. Для определения размеров шрифтов используйте модульную шкалу (Modular Scale), а чтобы установить высоту линий, можно применять калькулятор вертикального ритма (vertical rhythm calculator).


Определение компонента заголовка


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


import React, { PropTypes } from 'react';
import { StyleSheet, css } from 'aphrodite/no-important';
import { tagMapping, fontSize, fontWeight, lineHeight } from '../styles/base/typography';

function Heading(props) {
  const { children, tag: Tag } = props;
  return <Tag className={css(styles[tagMapping[Tag]])}>{children}</Tag>;
}

export default Heading;

export const styles = StyleSheet.create({
  displayLarge: {
    fontSize: fontSize.displayLarge,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.displayLarge,
  },
  displayMedium: {
    fontSize: fontSize.displayMedium,
    fontWeight: fontWeight.normal,
    lineHeight: lineHeight.displayLarge,
  },
  displaySmall: {
    fontSize: fontSize.displaySmall,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.displaySmall,
  },
  heading: {
    fontSize: fontSize.heading,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.heading,
  },
  subheading: {
    fontSize: fontSize.subheading,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.subheading,
  },
});

Компонент Heading — это функция, которая принимает тег как свойство и возвращает его со связанным стилем. Это стало возможным благодаря тому, что ранее мы определили сопоставления тегов в файле констант.


...
export const tagMapping = {
  h1: 'displayLarge',
  h2: 'displayMedium',
  h3: 'displaySmall',
  h4: 'heading',
  h5: 'subheading',
};

В нижней части файла компонента мы определяем объект styles. Здесь используются константы типографики.


export const styles = StyleSheet.create({
  displayLarge: {
    fontSize: fontSize.displayLarge,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.displayLarge,
  },

  ...
});

А компонент Heading будет применяться следующим образом:


function Parent() {
  return (
    <Heading tag="h2">Hello World</Heading>
  );
}

При таком подходе мы снижаем вероятность возникновения неожиданной изменчивости в системе типов. Устраняя потребность в глобальных стилях и стандартизируя заголовки кодовой базы, мы избегаем проблем с различными размерами шрифтов. К тому же подход, который мы использовали при построении компонента Heading, можно применить и для построения компонента Text основного тела.


Основной принцип № 2. Интервал


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


Определение интервальных констант


Чтобы определить интервальные константы для полей между элементами, можно прибегнуть к математическому подходу. Используя константу spacingFactor, мы можем сгенерировать набор расстояний на основе общего коэффициента. Такой подход обеспечивает логическое и согласованное расстояние между элементами.


const spacingFactor = 8;
export const spacing = {
  space0: `${spacingFactor / 2}px`,  // 4
  space1: `${spacingFactor}px`,      // 8
  space2: `${spacingFactor * 2}px`,  // 16
  space3: `${spacingFactor * 3}px`,  // 24
  space4: `${spacingFactor * 4}px`,  // 32
  space5: `${spacingFactor * 5}px`,  // 40
  space6: `${spacingFactor * 6}px`,  // 48

  space8: `${spacingFactor * 8}px`,  // 64
  space9: `${spacingFactor * 9}px`,  // 72
  space13: `${spacingFactor * 13}px`, // 104
};

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


Золотое сечение(1:1.618)
8.0 x (1.618 ^ 0) = 8.000
8.0 x (1.618 ^ 1) = 12.94
8.0 x (1.618 ^ 2) = 20.94
8.0 x (1.618 ^ 3) = 33.89
8.0 x (1.618 ^ 4) = 54.82
8.0 x (1.618 ^ 5) = 88.71

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


const spacingFactor = 8;
export const spacing = {
  space0: `${computeGoldenRatio(spacingFactor, 0)}px`,  // 8
  space1: `${computeGoldenRatio(spacingFactor, 1)}px`,  // 13
  space2: `${computeGoldenRatio(spacingFactor, 2)}px`,  // 21
  space3: `${computeGoldenRatio(spacingFactor, 3)}px`,  // 34
  space4: `${computeGoldenRatio(spacingFactor, 4)}px`,  // 55
  space5: `${computeGoldenRatio(spacingFactor, 5)}px`,  // 89
};

function computeGoldenRatio(spacingFactor, exp) {
  return Math.round(spacingFactor * Math.pow(1.618, exp));
}

После определения интервальных констант мы можем использовать их для добавления полей к элементам дизайна. Один из вариантов — это импортировать промежуточные константы и применять их в компонентах.


Например, добавим marginBottom к компоненту Button.


import { spacing } from '../styles/base/spacing';

...

const styles = StyleSheet.create({
  button: {
    marginBottom: spacing.space4, // adding margin using spacing constant
    ...
  },
});

Это работает в большинстве случаев. Однако что произойдет, если мы решим изменить свойство кнопки marginBottom в зависимости от ее расположения?


Один из способов получения переменных полей — переопределить стиль поля из потребляющего родительского компонента. Также можно создать компонент Spacing для управления вертикальными полями элементов.


import React, { PropTypes } from 'react';
import { spacing } from '../../base/spacing';

function getSpacingSize(size) {
  return `space${size}`;
}

function Spacing(props) {
  return (
    <div style={{ marginBottom: spacing[getSpacingSize(props.size)] }}>
      {props.children}
    </div>
  );
}

export default Spacing;

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


Это работает за счет таких компонентов, как кнопки, инпуты и карточки (cards), которые могут нуждаться в margin, заданном переменными. Например, для кнопки формы может потребоваться большее поле, чем для кнопки панели навигации.


Вы, наверное, заметили, что в приведенных примерах используется только marginBottom. Это связано с тем, что определение всех вертикальных полей в одном направлении позволяет избежать их сбрасывания, а также дает возможность отслеживать вертикальный ритм дизайна. Узнать об этом больше вы можете из статьи Гарри Роберта «Описание одностороннего поля» (Single-direction margin declarations).


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


import React, { PropTypes } from 'react';
import { StyleSheet, css } from 'aphrodite/no-important';
import { spacing } from '../../styles/base/spacing';

function Card(props) {
  return (
    <div className={css(styles.card)}>
      {props.children}
    </div>
  );
}

export default Card;

export const styles = StyleSheet.create({
  card: {
    padding: spacing.space4}, // using spacing constants as padding

    background: 'rgba(255, 255, 255, 1.0)',
    boxShadow: '0 3px 17px 2px rgba(0, 0, 0, .05)',
    borderRadius: '3px',
  },
});

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


Результат выглядит следующим образом:


image
С одинаковыми интервальными константами для полей и для отступов можно добиться большей визуальной согласованности в дизайне


Теперь, когда у вас есть понимание CSS в JavaScript, можете смело экспериментировать. Попробуйте включить встроенные стили JavaScript в свой следующий проект. Думаю, вы по достоинству оцените возможность решать все проблемы стиля и представления (view), работая в одном контексте.