Как вы структурируете приложение с несколькими темами? Что, если эти темы находятся внутри другой темы?

Недавний проект столкнулся с некоторыми серьезными проблемами. Клиент хотел получить демонстрацию, которая отображала бы ряд различных веб-сайтов в рамках фирменного «контейнера». Эти демонстрационные веб-сайты будут интерактивными в макете рабочего стола или мобильного устройства, каждый со своей собственной индивидуальной темой. В этой статье я подробнее расскажу:

  1. Как мы настраиваем файлы проекта
  2. Создание тем с использованием стилизованных компонентов

Это было достаточно сложно, но, помимо этого, нам нужно было дать пользователю возможность переключаться между имитируемым дисплеем «рабочего стола» и «мобильным» дисплеем внутри этого статического контейнера, без использования медиа-запросы. Я рассмотрю эту проблему в следующей статье.

Вот две иллюстрации того, что мы создали:

Сколько тем нам нужно? 💭

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

  1. Здесь нам нужна одна тема для статического контейнера, которая останется неизменной независимо от активной демонстрации. Это будет фирменная темная тема, соответствующая стилю клиента.
  2. Нам также нужна одна тема для каждой «демонстрации», которая будет меняться в зависимости от того, какая демонстрация активна. Таким образом, три демонстрации означают еще три темы и т. Д.

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

Настройка файлов 📚

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

Давайте создадим это как компонент React под названием DemoContainer. Лично мне нравится использовать следующую файловую структуру при создании компонентов, использующих стилизованные компоненты (здесь мы используем TypeScript):

src
├── components
│   ├── atoms
│   ├── molecules
│   └── organisms
│       └── DemoContainer
│           ├── DemoContainer.tsx
│           ├── Demo.props.ts
│           └── Demo.style.ts

Добавление первой темы

Теперь у нас есть базовый компонент, и нам нужно настроить его тему с помощью стилизованных компонентов ThemeProvider. В этом случае мы создали папку styles/themes для размещения всех наших различных тем.

Это структура, с которой мы работали:

src
├── styles
│   └── themes
│       ├── demoContainer
│       │   ├── colors.ts
│       │   ├── text.ts
│       │   ├── typography.ts
│       │   └── index.ts
│       └── index.ts

/colors.ts 🎨

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

//colors.ts
const colors = {
  primary: {
    primary1: '#172121',
    primary2: '#444554',
    primary3: '#7F7B82',
  },
  secondary: {
    secondary1: '#BFACB5',
    secondary2: '#E5D0CC',
  },
  ...
}
export default colors;

/text.ts 🔠

Здесь мы будем хранить все наши текстовые переменные для темы. Это включает:

  • семейства шрифтов
  • размеры
  • Межбуквенное расстояние
  • толщина шрифта
  • высота линии

и любые другие переменные, которые вы хотите сохранить для своего текста. Вот пример того, что вы можете хранить:

// text.ts
const text = {
  font: {
    heading: '"Merriweather", Georgia, serif',
    body: '"Open Sans", Helvetica, Arial, sans-serif',
  }
  size: {
    heading: {
      mobile: {
        h1: 40,
        h2: 28,
        ...
      },
      desktop: {...},
    },
    body: {
      mobile: {
        regular: 16,
        ...
      },
      desktop: {
        regular: 24,
        ...
      }
    }
  },
  weight: {
    light: 200,
    regular: 400,
    bold: 700,
  }
}
export default text;

/typography.ts 📝

Наконец, типографика - это то место, где мы установим глобальные правила типографики для этой темы. Мы можем использовать css стилизованных компонентов для экспорта некоторых установленных стилей, которые мы хотели бы использовать в этой теме.

// typography.ts
import { css } from 'styled-components';
import text from './text';
const { font, size, weight, lineHeight, letterSpacing } = text;
const typography = css`
  font-family: ${font.body};
  font-weight: ${weight.regular};
  h1 {
    font-size: ${size.heading.mobile.h1}px;
  }
  ...
`;
export default typography;

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

/index.ts

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

// index.ts
import colors from './colors';
import text from './text';
import typography from './typography';
export default {
  name: 'demoContainer',
  colors,
  text,
  typography,
};

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

Экспорт темы

Мы знаем, что здесь нам понадобится несколько тем, поэтому давайте заранее продумаем экспорт этих тем из файла themes/index.ts:

// index.ts
import demoContainer from './demoContainer';
const theme = {
  default: demoContainer,
  demoContainer,
};
export default theme;

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

Использование переменных темы 🖼

Теперь самое интересное - увидеть нашу тему в действии. Первый шаг - обернуть наш DemoContainer компонент в ThemeProvider из стилизованных компонентов.

Добавление ThemeProvider

Наш DemoContainer.tsx файл будет выглядеть примерно так:

// DemoContainer.tsx
import React, { FunctionComponent } from 'react';
import { ThemeProvider } from 'styled-components';
import { DemoContainerProps } from './DemoContainer.props';
import theme from '../../../styles/global/theme';
const DemoContainer: FunctionComponent<DemoContainerProps> = ({ children }) => {
  return (
    <ThemeProvider theme={theme.demoContainer}>
      Demo Container content here!
    </ThemeProvider>
  )
}

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

Мы создадим компонент Wrapper, который будет находиться в нашем ThemeProvider.

// DemoContainer.style.ts
import styled from 'styled-components';
export const Wrapper = styled.main``;

⚡️

// DemoContainer.tsx
import React, { FunctionComponent } from 'react';
import { ThemeProvider } from 'styled-components';
import { DemoContainerProps } from './DemoContainer.props';
import theme from '../../../styles/global/theme';
import Wrapper from './DemoContainer.style.ts';
const DemoContainer: FunctionComponent<DemoContainerProps> = () => {
  return (
    <ThemeProvider theme={theme.demoContainer}>
      <Wrapper>Demo Container content here!</Wrapper>
    </ThemeProvider>
  )
}

Использование переменных темы в наших стилизованных компонентах

Теперь, поскольку наш Wrapper находится внутри ThemeProvider, он также автоматически получает доступ к свойству «тема». Итак, теперь мы можем использовать переменные, которые мы установили в нашей /themes папке, используя объектную деструктуризацию опоры «тема»:

// DemoContainer.style.ts
import styled from 'styled-components';
export const Wrapper = styled.main`
  // let's put all our global typography at the top
  ${({theme}) => theme.typography};
  height: 100vh;
  width: 100%;
  padding: 60px;
  background-color: ${({theme}) => theme.colors.primary.primary1};
  color: ${({theme}) => theme.colors.secondary.secondary2};
`;

Теперь у нас есть стиль DemoContainer с использованием наших переменных. theme.typography обеспечит стилизацию нашего текста, поэтому нет необходимости добавлять дополнительные стили текста в наш Wrapper. Демо теперь должно иметь красивый фон в нашем основном цвете 1, а текст будет иметь вторичный цвет 2.

Добавляем вторую тему (и третью, и четвертую…) 👯‍♀️

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

Когда пользователь нажимает одну из кнопок слева (Демо 1, Демо 2 и т. Д.), Нам нужно изменить тему в макете устройства.

(Мы использовали Redux для этого управления состоянием, но вы также можете посмотреть useContext React, который, возможно, я бы использовал, если бы сделал это снова)

В любом случае, чтобы обслуживать правильную тему, нам просто нужно получить доступ к имени или идентификатору темы от любого используемого поставщика.

Создайте новый компонент, чтобы обернуть динамические темы

Давайте создадим компонент макета устройства и назовем его MockDevice.

src
├── components
│   ├── atoms
│   ├── molecules
│   └── organisms
│       ├── DemoContainer
│       └── MockDevice
│           ├── MockDevice.tsx
│           ├── MockDevice.props.ts
│           └── MockDevice.style.ts

MockDevice будет иметь очень похожую настройку на DemoContainer, и ему потребуется собственный ThemeProvider. Однако мы собираемся поместить ThemeProvider между двумя другими компонентами StyledComponent:

  1. Wrapper, который придаст нашему макету устройства "мобильный эффект".
  2. DeviceContent, который будет содержать все страницы, которые мы хотим обслуживать для рассматриваемого сайта.
// MockDevice.tsx
import React, { FunctionComponent } from 'react';
import { ThemeProvider } from 'styled-components';
import { MockDeviceProps } from './MockDevice.props';
import { Wrapper, DeviceContent } from './MockDevice.style.ts';
const MockDevice: FunctionComponent<MockDeviceProps> = () => {
  return (
    <Wrapper>
      <ThemeProvider theme={???}>
        <DeviceContent>
          Mock Device content here!
        </DeviceContent>
      </ThemeProvider>
    </Wrapper>
  )
}

Добавление новых файлов темы

На данный момент мы не знаем, какую тему передать в наш новый компонент устройства, но мы можем начать настройку этих тем так же, как и тему demoContainer.

src
├── styles
│   └── themes
│       ├── demoContainer
│       ├── demo1
│       │   ├── colors.ts
│       │   ├── text.ts
│       │   ├── typography.ts
│       │   └── index.ts
│       ├── demo2
│       │   ├── colors.ts
│       │   ├── text.ts
│       │   ├── typography.ts
│       │   └── index.ts
│       └── index.ts

не забудьте экспортировать темы в themes/index.ts:

// index.ts
import demoContainer from './demoContainer';
import demo1 from './demo1';
import demo2 from './demo2';
const theme = {
  default: demoContainer,
  demoContainer,
  demo1,
  demo2,
};
export default theme;

Использование динамической темы 🧙‍♀️

Теперь мы можем получить активную тему от нашего провайдера и выбрать правильные стили. Этот бит может немного отличаться в зависимости от того, используете ли вы useContext или Redux или другую систему управления состоянием. Это с Redux и хуками *:

// MockDevice.tsx
import React, { FunctionComponent } from 'react';
import { ThemeProvider } from 'styled-components';
import { useSelector } from 'react-redux';
import { MockDeviceProps } from './MockDevice.props';
import { Wrapper, DeviceContent } from './MockDevice.style.ts';
import themes from '../../../styles/global/theme';
const MockDevice: FunctionComponent<MockDeviceProps> = ({children}) => {
  const themeName = useSelector(
    (state: RootState) => state.theme,
  );
const theme = themes[themeName] || themes.default;
return (
    <Wrapper>
      <ThemeProvider theme={theme}>
        <DeviceContent>
          {children}
        </DeviceContent>
      </ThemeProvider>
    </Wrapper>
  )
}

* Вы раньше не использовали хуки или не хотите их включать? Затем замените useSelector на классический mapStateToProps в подключенном компоненте.

Собираем приложение вместе 💅

Мы знаем, что наш DemoContainer компонент обернет все приложение. Эта тема не изменится и должна оставаться статичной. Мы также знаем, что MockDevice должен находиться в демо-контейнере. Итак, давайте добавим следующее:

// DemoContainer.tsx
import React, { FunctionComponent } from 'react';
import { ThemeProvider } from 'styled-components';
import { DemoContainerProps } from './DemoContainer.props';
import theme from '../../../styles/global/theme';
import Wrapper from './DemoContainer.style.ts';
import MockDevice from '../MockDevice';
const DemoContainer: FunctionComponent<DemoContainerProps> = ({ children }) => {
  return (
    <ThemeProvider theme={theme.demoContainer}>
      <Wrapper>
        <MockDevice>
          {children}
        </MockDevice>
      </Wrapper>
    </ThemeProvider>
  )
}

Поскольку DemoContainer останется полностью статичным, мы можем визуализировать любых дочерних элементов React в нашем MockDevice.

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

Заключение

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

  • Стили DemoContainer останутся статичными и будут зависеть от переменных темы demoContainer. Все эти переменные стиля будут доступны внутри Wrapper DemoContainer.
  • MockDevice переопределит стили demoContainer с активной темой в состоянии приложения. Все дети внутри MockDevice будут иметь доступ только к этой теме.
  • Мы можем использовать деструктуризацию свойства в любом дочернем компоненте для доступа к активным переменным темы.

Примечание о наборе текста

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

Здесь действительно помогает TypeScript, потому что вы можете определить свойства, которые необходимо включить в каждый файл:

// themeTypes.ts
export interface Text {
  font: {
    heading: string;
    body: string;
  };
  size: {
    heading: {
      mobile: {
        h1: number;
        h2: number;
      };
    };
  };
};
// text.ts
import { Text } from './themeTypes.ts';
const text: Text = {...}
export default text;

Если вы не печатаете, убедитесь, что вы следуете одной и той же структуре.

Спасибо! 🤗

Надеюсь, вам понравилась эта статья, и она в чем-то вам помогла. Сталкивались ли вы раньше с подобной проблемой? Я хотел бы услышать, как вы справились с этим, в комментариях ниже.