CSS в JavaScript: будущее компонентных стилей
- четверг, 1 июня 2017 г. в 03:14:28
С помощью встроенных стилей можно получить все программные возможности 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
, а значения становятся строками. Псевдоселекторы, медиазапросы и определения шрифтов — все работает. Кроме того, автоматически добавляются вендорные префиксы.
В результате получаем:
Простая миграция и низкая кривая обучения — преимущества Aphrodite
Давайте рассмотрим на этом примере, как Aphrodite может использоваться для создания базовой системы визуального проектирования. Сосредоточимся на таких основных принципах дизайна, как типографика и интервал.
Начнем с фундаментальной основы дизайна — типографики. Для начала необходимо определить ее константы. В отличие от 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?).
Используйте калькулятор для определения высоты линии
Существует целая наука о выборе значений для строк и размеров шрифта. Для создания потенциальных размерных вариантов могут использоваться математические соотношения. Несколько недель назад я опубликовал статью «Типографика может создать или разрушить дизайн: выбор типа» (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
основного тела.
Поскольку в дизайне интервал управляет вертикальным и горизонтальным ритмом, он играет первостепенную роль в создании системы визуального проектирования. Здесь, как и в разделе типографики, для начала необходимо определить константы интервала.
Чтобы определить интервальные константы для полей между элементами, можно прибегнуть к математическому подходу. Используя константу 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',
},
});
С одинаковыми интервальными константами для полей и для отступов можно добиться большей визуальной согласованности в дизайне.
Результат выглядит следующим образом:
С одинаковыми интервальными константами для полей и для отступов можно добиться большей визуальной согласованности в дизайне
Теперь, когда у вас есть понимание CSS в JavaScript, можете смело экспериментировать. Попробуйте включить встроенные стили JavaScript в свой следующий проект. Думаю, вы по достоинству оцените возможность решать все проблемы стиля и представления (view), работая в одном контексте.