Статьи

Создание многоразовой системы проектирования с React

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

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

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

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

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

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

Структура папок проекта

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//src/App.js
 
import React, { Component } from ‘react’;
import logo from ‘./logo.svg’;
import ‘./App.scss’;
import { Flex, Page, Box, BoxStyle } from ‘./design_system/layouts/Layouts’;
 
class App extends Component {
  render() {
    return (
      <div className=»App»>
        <header className=»App-header»>
          <img src={logo} className=»App-logo» alt=»logo» />
          <h1 className=»App-title»>Build a design system with React</h1>
        </header>
        <Page>
          <Flex lastElRight={true}>
            <Box boxStyle={BoxStyle.doubleSpace} >
              A simple flexbox
            </Box>
            <Box boxStyle={BoxStyle.doubleSpace} >Middle</Box>
            <Box fullWidth={false}>and this goes to the right</Box>
          </Flex>
        </Page>
      </div>
    );
  }
}
 
export default App;

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//design-system/layouts/Layout.js
import React from ‘react’;
import ‘./layout.scss’;
 
export const BoxBorderStyle = {
    default: ‘ds-box-border—default’,
    light: ‘ds-box-border—light’,
    thick: ‘ds-box-border—thick’,
}
 
export const BoxStyle = {
    default: ‘ds-box—default’,
    doubleSpace: ‘ds-box—double-space’,
    noSpace: ‘ds-box—no-space’
}
 
export const Page = ({children, fullWidth=true}) => {
    const classNames = `ds-page ${fullWidth ?
    return (<div className={classNames}>
        {children}
    </div>);
 
};
 
export const Flex = ({ children, lastElRight}) => {
    const classNames = `flex ${lastElRight ?
    return (<div className={classNames}>
        {children}
    </div>);
};
 
export const Box = ({
    children, borderStyle=BoxBorderStyle.default, boxStyle=BoxStyle.default, fullWidth=true}) => {
    const classNames = `ds-box ${borderStyle} ${boxStyle} ${fullWidth ?
    return (<div className={classNames}>
        {children}
    </div>);
};

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

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

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

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

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

Сначала мы определим некоторые константы стиля и класс-оболочку.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// design-system/type/Type.js
import React, { Component } from ‘react’;
import ‘./type.scss’;
 
export const TextSize = {
    default: ‘ds-text-size—default’,
    sm: ‘ds-text-size—sm’,
    lg: ‘ds-text-size—lg’
};
 
export const TextBold = {
    default: ‘ds-text—default’,
    semibold: ‘ds-text—semibold’,
    bold: ‘ds-text—bold’
};
 
export const Type = ({tag=’span’, size=TextSize.default, boldness=TextBold.default, children}) => {
    const Tag = `${tag}`;
    const classNames = `ds-text ${size} ${boldness}`;
    return <Tag className={classNames}>
        {children}
    </Tag>
};

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* design-system/type/type.scss*/
 
@import ‘../../styles/variables.scss’;
$base-font: $base-px * 4;
 
.ds-text {
    line-height: 1.8em;
     
    &.ds-text-size—default {
        font-size: $base-font;
    }
    &.ds-text-size—sm {
        font-size: $base-font — $base-px;
    }
    &.ds-text-size—lg {
        font-size: $base-font + $base-px;
    }
    &strong, &.ds-text—semibold {
        font-weight: 600;
    }
    &.ds-text—bold {
        font-weight: 700;
    }
}

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

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

Сначала мы определяем класс компонента для модального окна.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// design-system/Portal.js
import React, {Component} from ‘react’;
import ReactDOM from ‘react-dom’;
import {Box, Flex} from ‘./layouts/Layouts’;
import { Type, TextSize, TextAlign} from ‘./type/Type’;
import ‘./portal.scss’;
 
export class Portal extends React.Component {
    constructor(props) {
        super(props);
        this.el = document.createElement(‘div’);
    }
 
    componentDidMount() {
        this.props.root.appendChild(this.el);
    }
 
    componentWillUnmount() {
        this.props.root.removeChild(this.el);
    }
 
    render() {
        return ReactDOM.createPortal(
            this.props.children,
            this.el,
        );
    }
}
 
 
export const Modal = ({ children, root, closeModal, header}) => {
    return <Portal root={root} className=»ds-modal»>
        <div className=»modal-wrapper»>
        <Box>
            <Type tagName=»h6″ size={TextSize.lg}>{header}</Type>
            <Type className=»close» onClick={closeModal} align={TextAlign.right}>x</Type>
        </Box>
        <Box>
            {children}
        </Box>
        </div>
    </Portal>
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#modal-root {
    .modal-wrapper {
        background-color: white;
        border-radius: 10px;
        max-height: calc(100% — 100px);
        max-width: 560px;
        width: 100%;
        top: 35%;
        left: 35%;
        right: auto;
        bottom: auto;
        z-index: 990;
        position: absolute;
    }
    > div {
        background-color: transparentize(black, .5);
        position: absolute;
        z-index: 980;
        top: 0;
        right: 0;
        left: 0;
        bottom: 0;
    }
    .close {
        cursor: pointer;
    }
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//src/App.js
 
import React, { Component } from ‘react’;
//…
import { Type, TextBold, TextSize } from ‘./design_system/type/Type’;
import { Modal } from ‘./design_system/Portal’;
 
class App extends Component {
  constructor() {
    super();
    this.state = {showModal: false}
  }
 
  toggleModal() {
    this.setState({ showModal: !this.state.showModal });
  }
 
  render() {
 
          //…
          <button onClick={this.toggleModal.bind(this)}>
            Show Alert
          </button>
          {this.state.showModal &&
            <Modal root={document.getElementById(«modal-root»)} header=»Test Modal» closeModal={this.toggleModal.bind(this)}>
            Test rendering
          </Modal>}
            //….
    }
}

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

1
2
3
4
5
6
7
8
9
// In design-system/type/Type.js
 
export const Type = ({ tag = ‘span’, size= TextSize.default, boldness = TextBold.default, children, className=», align=TextAlign.default, …rest}) => {
    const Tag = `${tag}`;
    const classNames = `ds-text ${size} ${boldness} ${align} ${className}`;
    return <Tag className={classNames} {…rest}>
        {children}
    </Tag>
};

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

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

Чтобы начать, запустите:

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
import React from ‘react’;
import { storiesOf } from ‘@storybook/react’;
 
import { Type, TextSize, TextBold } from ‘../design_system/type/Type.js’;
 
 
storiesOf(‘Type’, module)
  .add(‘default text’, () => (
    <Type>
      Lorem ipsum
    </Type>
  )).add(‘bold text’, () => (
    <Type boldness={TextBold.semibold}>
      Lorem ipsum
    </Type>
  )).add(‘header text’, () => (
    <Type size={TextSize.lg}>
      Lorem ipsum
    </Type>
  ));

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

Простой тип сборника рассказов

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

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

Я еще не видел хорошую библиотеку компонентов пользовательского интерфейса для React. Мой опыт работы с response-bootstrap и material-ui (то есть библиотекой React, то есть не самой платформой) был не очень удачным. Вместо повторного использования всей библиотеки пользовательского интерфейса может иметь смысл выбрать отдельные компоненты. Например, реализация множественного выбора является сложной проблемой пользовательского интерфейса, и есть множество сценариев, которые необходимо рассмотреть. В этом случае может быть проще использовать такие библиотеки, как React Select или Select2.

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

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

Поделитесь своими мыслями об этой статье в разделе комментариев!