1. Code
  2. JavaScript
  3. React

Создаем универсальную дизайн систему с помощью React

Scroll to top

Russian (Pусский) translation by Oleg Kiselevich (you can also view the original English article)

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

React упростил написание красивого, выразительного кода. Однако без четких шаблонов повторного использования компонентов, код с течением времени начинает разветвляться и становится очень трудно его поддерживать. Я видел проекты, где один и тот же элемент пользовательского интерфейса имел десять различных реализаций! Другой проблемой является то, что, очень часто разработчики связывают пользовательский интерфейс и бизнес-функциональность слишком плотно, что приводит к сложностям в будущем, когда интерфейс меняется.

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

Приступая к работе

Для начала вам понадобится пустой проект React. Самый быстрый способ его создать - это через команду create-react-app, но потребуется немного постараться, чтобы настроить для этого Sass. Я создал базовое приложение, которое вы можете клонировать из GitHub. Вы также можете найти окончательный проект в репозитории GitHub нашего урока.

Для запуска, выполните команду yarn-install, чтобы установить все зависимости, и затем выполните команду yarn start для запуска приложения.

Все визуальные компоненты будут находиться в папке design_system наряду с соответствующими стилями. Все глобальные стили или переменные будут располагаться в src/styles.

Project folder structureProject folder structureProject folder structure

Определяем основы дизайна

Помните последний раз, когда ваши коллеги дизайнеры смотрели на вас неодобрительным взглядом за то, что вы ошиблись на пол пикселя в расчетах отступа или не смогли отличить два разных оттенка серого цвета? (Оказывается есть разница между #eee и #efefef, как мне сказали, и однажды я может и сам ее замечу.)

Одной из целей создания библиотеки пользовательского интерфейса является улучшение отношений между командами дизайна и разработки. Front-end разработчики уже определенное время сотрудничают с разработчиками API и теперь способны неплохо вырабатывать API соглашения. Но почему-то этот момент ускользает от нас при координации с командой дизайнеров. Если задуматься, то существует только определенное число состояний, в которых может существовать элемент пользовательского интерфейса. Если нам нужно создать компонент заголовка, например, то это будет элемент от h1 до h6, который может быть полужирным, курсивным или подчеркнутым. Это должно быть несложно описать в коде.

Grid разметка

Первое, что нужно сделать, прежде чем приступать к созданию любого дизайн проекта - это определиться со структурой grid сетки. Для многих приложений она носит случайных характер. И это приводит к тому, что нет четкой системы расположения элементов, что вызывает затруднения у разработчиков при определении какую систему использовать. Так что выберите конкретную систему! Мне очень понравилась система grid разметки 4px - 8px, когда я впервые прочитал о ней. То, что я придерживался именно ее, помогло мне упростить решение многих вопросов с оформлением.

Начнем с того, что создадим базовую систему grid разметки в коде. Мы начнем с компонента приложения, который определяет макет.

1
//src/App.js

2
3
import React, { Component } from 'react';
4
import logo from './logo.svg';
5
import './App.scss';
6
import { Flex, Page, Box, BoxStyle } from './design_system/layouts/Layouts';
7
8
class App extends Component {
9
  render() {
10
    return (
11
      <div className="App">
12
        <header className="App-header">
13
          <img src={logo} className="App-logo" alt="logo" />
14
          <h1 className="App-title">Build a design system with React</h1>

15
        </header>

16
        <Page>
17
          <Flex lastElRight={true}>
18
            <Box boxStyle={BoxStyle.doubleSpace} >
19
              A simple flexbox
20
            </Box>

21
            <Box boxStyle={BoxStyle.doubleSpace} >Middle</Box>

22
            <Box fullWidth={false}>and this goes to the right</Box>

23
          </Flex>

24
        </Page>

25
      </div>

26
    );
27
  } 
28
}
29
30
export default App;

Далее мы определяем несколько компонентов для стилей и блоков контента.

1
//design-system/layouts/Layout.js

2
import React from 'react';
3
import './layout.scss';
4
5
export const BoxBorderStyle = {
6
    default: 'ds-box-border--default',
7
    light: 'ds-box-border--light',
8
    thick: 'ds-box-border--thick',
9
}
10
11
export const BoxStyle = {
12
    default: 'ds-box--default',
13
    doubleSpace: 'ds-box--double-space',
14
    noSpace: 'ds-box--no-space'
15
}
16
17
export const Page = ({children, fullWidth=true}) => {
18
    const classNames = `ds-page ${fullWidth ? 'ds-page--fullwidth' : ''}`;
19
    return (<div className={classNames}>
20
        {children}
21
    </div>);

22
23
};
24
25
export const Flex = ({ children, lastElRight}) => {
26
    const classNames = `flex ${lastElRight ? 'flex-align-right' : ''}`;
27
    return (<div className={classNames}> 
28
        {children}
29
    </div>);

30
};
31
32
export const Box = ({
33
    children, borderStyle=BoxBorderStyle.default, boxStyle=BoxStyle.default, fullWidth=true}) => {
34
    const classNames = `ds-box ${borderStyle} ${boxStyle} ${fullWidth ? 'ds-box--fullwidth' : ''}` ;
35
    return (<div className={classNames}>
36
        {children}
37
    </div>);

38
};

Наконец мы создаем наши CSS стили в SCSS.

1
/*design-system/layouts/layout.scss */
2
@import '../../styles/variables.scss';
3
$base-padding: $base-px * 2;
4
5
.flex {
6
    display: flex;
7
    &.flex-align-right > div:last-child {
8
        margin-left: auto;
9
    }
10
}
11
12
.ds-page {
13
    border: 0px solid #333;
14
    border-left-width: 1px;
15
    border-right-width: 1px;
16
    &:not(.ds-page--fullwidth){
17
        margin: 0 auto;
18
        max-width: 960px;
19
    }
20
    &.ds-page--fullwidth {
21
        max-width: 100%;
22
        margin: 0 $base-px * 10;
23
    }
24
}
25
26
.ds-box {
27
    border-color: #f9f9f9;
28
    border-style: solid;
29
    text-align: left;
30
    &.ds-box--fullwidth {
31
        width: 100%;
32
    }
33
34
    &.ds-box-border--light {
35
        border: 1px;
36
    }
37
    &.ds-box-border--thick {
38
        border-width: $base-px;
39
    }
40
41
    &.ds-box--default {
42
        padding: $base-padding;
43
    }
44
45
    &.ds-box--double-space {
46
        padding: $base-padding * 2;
47
    }
48
49
    &.ds-box--default--no-space {
50
        padding: 0;
51
    }
52
}

Тут есть много над чем поработать. Начнем с основ. variables.scss - это файл, в котором мы задаем наши глобальные значения, такие как цвет и настройка разметки. Поскольку мы используем сетку 4px-8px, наша базовая величина будет 4px. Родительский компонент - Page, и он контролирует разметку страницы. Затем элемент нижнего уровня - это Box, который определяет, как содержимое отображается на странице. Это просто div, который знает, как отображаться правильно в определенном контексте.

Теперь нам нужен компонент Container, который объединяет несколько divов. Мы выбрали flex-box, отсюда и название компонента Flex.

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

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

Во-первых мы определим некоторые стилевые константы и создадим класс оболочки.

1
// design-system/type/Type.js

2
import React, { Component } from 'react';
3
import './type.scss';
4
5
export const TextSize = {
6
    default: 'ds-text-size--default',
7
    sm: 'ds-text-size--sm',
8
    lg: 'ds-text-size--lg'
9
};
10
11
export const TextBold = {
12
    default: 'ds-text--default',
13
    semibold: 'ds-text--semibold',
14
    bold: 'ds-text--bold'
15
};
16
17
export const Type = ({tag='span', size=TextSize.default, boldness=TextBold.default, children}) => {
18
    const Tag = `${tag}`; 
19
    const classNames = `ds-text ${size} ${boldness}`;
20
    return <Tag className={classNames}>
21
        {children}
22
    </Tag>

23
};

Далее мы определим стили CSS, которые будут использоваться для текстовых элементов.

1
/* design-system/type/type.scss*/
2
3
@import '../../styles/variables.scss';
4
$base-font: $base-px * 4;
5
6
.ds-text {
7
    line-height: 1.8em;
8
    
9
    &.ds-text-size--default {
10
        font-size: $base-font;
11
    }
12
    &.ds-text-size--sm {
13
        font-size: $base-font - $base-px;
14
    }
15
    &.ds-text-size--lg {
16
        font-size: $base-font + $base-px;
17
    }
18
    &strong, &.ds-text--semibold {
19
        font-weight: 600;
20
    }
21
    &.ds-text--bold {
22
        font-weight: 700;
23
    }
24
}

Это простой компонент Text, определяющий различные UI состояния, в которых может находиться текст. Мы можем в дальнейшем расширять его, дополняя возможности обрабатывать микро взаимодействия, как например отображение подсказок при обрезке текста, или отображение другого элемента в особых случаях, например при выводе электронной почты, времени и т.д.

Молекулы состоят из атомов

Пока мы построили только самые основные элементы, которые могут существовать в веб-приложении, но они не используются сами по себе. Давайте расширим наш пример и создадим простое модальное окно.

Во-первых мы определяем класс компонента для модального окна.

1
// design-system/Portal.js

2
import React, {Component} from 'react';
3
import ReactDOM from 'react-dom';
4
import {Box, Flex} from './layouts/Layouts';
5
import { Type, TextSize, TextAlign} from './type/Type';
6
import './portal.scss';
7
8
export class Portal extends React.Component {
9
    constructor(props) {
10
        super(props);
11
        this.el = document.createElement('div');
12
    }
13
14
    componentDidMount() {
15
        this.props.root.appendChild(this.el);
16
    }
17
18
    componentWillUnmount() {
19
        this.props.root.removeChild(this.el);
20
    }
21
22
    render() {  
23
        return ReactDOM.createPortal(
24
            this.props.children,
25
            this.el,
26
        );
27
    }
28
}
29
30
31
export const Modal = ({ children, root, closeModal, header}) => {
32
    return <Portal root={root} className="ds-modal">
33
        <div className="modal-wrapper">
34
        <Box>
35
            <Type tagName="h6" size={TextSize.lg}>{header}</Type>

36
            <Type className="close" onClick={closeModal} align={TextAlign.right}>x</Type>

37
        </Box>

38
        <Box>
39
            {children}
40
        </Box>

41
        </div>

42
    </Portal>

43
}

Далее мы можем определить стили CSS для модального окна.

1
#modal-root {
2
    .modal-wrapper {
3
        background-color: white;
4
        border-radius: 10px;
5
        max-height: calc(100% - 100px);
6
        max-width: 560px;
7
        width: 100%;
8
        top: 35%;
9
        left: 35%;
10
        right: auto;
11
        bottom: auto;
12
        z-index: 990;
13
        position: absolute;
14
    }
15
    > div {
16
        background-color: transparentize(black, .5);
17
        position: absolute;
18
        z-index: 980;
19
        top: 0;
20
        right: 0;
21
        left: 0;
22
        bottom: 0;
23
    } 
24
    .close {
25
        cursor: pointer;
26
    }
27
}

Для непосвященных createPortal очень похож на метод render, за исключением того, что он создает дочерние элементы в ячейке, которая существует за пределами DOM иерархии родительского компонента. Такой функционал был добавлен в React 16.

Использование компонента Modal

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

1
//src/App.js

2
3
import React, { Component } from 'react';
4
//...

5
import { Type, TextBold, TextSize } from './design_system/type/Type';
6
import { Modal } from './design_system/Portal';
7
8
class App extends Component {
9
  constructor() {
10
    super();
11
    this.state = {showModal: false}
12
  }
13
14
  toggleModal() {
15
    this.setState({ showModal: !this.state.showModal });
16
  }
17
18
  render() {
19
20
          //...

21
          <button onClick={this.toggleModal.bind(this)}>
22
            Show Alert
23
          </button>

24
          {this.state.showModal && 
25
            <Modal root={document.getElementById("modal-root")} header="Test Modal" closeModal={this.toggleModal.bind(this)}>
26
            Test rendering
27
          </Modal>}

28
            //....

29
    }
30
}

Мы можем использовать модальное окно везде и поддерживать его состояние в вызывающем объекте. Просто, правда? Но здесь есть ошибка. Не работает кнопка закрытия. Это потому, что мы создали все компоненты в виде закрытой системы. Он просто принимает те параметры, которые ему нужны, и игнорирует все остальное. В данном контексте, компонент текста игнорирует обработчик события onClick. К счастью это легко исправить.

1
// In  design-system/type/Type.js

2
3
export const Type = ({ tag = 'span', size= TextSize.default, boldness = TextBold.default, children, className='', align=TextAlign.default, ...rest}) => {
4
    const Tag = `${tag}`; 
5
    const classNames = `ds-text ${size} ${boldness} ${align} ${className}`;
6
    return <Tag className={classNames} {...rest}>
7
        {children}
8
    </Tag>

9
};

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

Создаем средства поиска доступных компонентов

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

Чтобы начать, выполните:

1
npm i -g @storybook/cli
2
3
getstorybook

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

1
import React from 'react';
2
import { storiesOf } from '@storybook/react';
3
4
import { Type, TextSize, TextBold } from '../design_system/type/Type.js';
5
6
7
storiesOf('Type', module)
8
  .add('default text', () => (
9
    <Type>
10
      Lorem ipsum
11
    </Type>

12
  )).add('bold text', () => (
13
    <Type boldness={TextBold.semibold}>
14
      Lorem ipsum
15
    </Type>

16
  )).add('header text', () => (
17
    <Type size={TextSize.lg}>
18
      Lorem ipsum
19
    </Type>

20
  ));
21
22

API очень простой. storiesOf определяет новую запись, обычно это ваш компонент. Затем можно создать новый подраздел с помощью add, для обозначения различных состояний этого компонента.

A simple Type storybookA simple Type storybookA simple Type storybook

Конечно это довольно простой пример, но для сборников есть несколько дополнений, которые помогут вам добавить функциональность для вашей документации. А я рассказывал, что они имеют поддержку emoji? 😲�