Добавление пользовательской темы для фронтэнд-приложений

Kate

Administrator
Команда форума
Меня зовут Павел, я фронтэнд-архитектор в компании Itransition. Вот уже более 8 лет я работаю во фронтэнде. В течении этого времени мне довелось поработать с приложениями, как полностью основанными на бэкенд технологиях, так и с классическими сайтами, написанными с использованием нативного JS и различных библиотек и фреймворков. В данной статье я хотел бы провести в некотором роде ретроспективу тех решений, с которыми сталкивался на практике.

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

С развитием интернета всё больше приложений перебираются на мобильные устройства и в браузер. Сегодня мы рассмотрим именно последний способ создания приложений – в виде сайтов – при этом учтем как современные приложения, так и enterprise-приложения, которые должны работать со старыми версиями браузеров.

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

Если постараться разделить возможные решения на какие-то группы, можно выделить следующие варианты:

  • Решение, базирующееся исключительно на бэкенде.
  • Подключение или замена стилей через JS.
  • Реализация замены стилей через препроцессоры и PostCSS.
  • Решение на нативном CSS.

Общая информация​

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

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

Как вариант расширения данного подхода, мы можем добавить промежуточную конвертацию стилей в CSS, основываясь на заранее подготовленных данных в любом формате и данных пользователя, который он может предоставить через систему кастомизации – по сути приложение-стилизатор. При таком подходе я могу предложить хранить данные в формате JSON, поскольку с JSON-объектом можно будет очень просто работать с JS, его поддерживают все современные системы, а его структура довольно проста и позволяет хорошо сжимать его для передачи конечному клиенту.

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

Вариант первый: backend only​

При работе с уже собранными файлами стилей, главным вопросом будет: как именно мы будем отдавать пользователю данные? Мы можем ориентироваться на единый API для отдачи стилей, который будет отдавать нужный файл, определяя его по выбору пользователя, либо же можем отдать в публичный доступ папку со всеми доступными темами, чтобы дать возможность нашему приложению скачать нужный файл по названию темы.

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

7c41795a16316efc6264de8c0d4acbd2.png

Второй вариант больше подходит для случаев, когда сгенерированные стили редко меняются. В этом кейсе мы даем сайту доступ к папке со стилями и грузим нужный файл через название темы, подставляя это названии при генерации index.html-страницы. При таком подходе главное заранее продумать механизм обновления стилей на клиенте после изменения в стилях. Лучшим способом будет генерация нового хэша и добавления его в урл, либо же добавление версионирования для стилей и указания актуальной версии в том же урле (пример – https://test.com/assets/themes/dark.css?v=3).

979f15c2faea32fe48d54a0ef1aab85e.png

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

Вариант второй: JS​

Рассматривать варианты, реализованные на JS, довольно сложно, поскольку многие сайты реализованы на JS библиотеках или фреймворках, а часть также содержат в себе встроенные варианты кастомизации, либо используют какие-то библиотеки. Например, работая с выше упомянутым Material в рамках Angular, мы будем использовать механизм palette, а при работе с Material-UI на React – ThemeProvider.

import * as React from 'react';
import { ThemeProvider} from 'styled-components';
import Theme from './Theme';

export const Root = () => (
<>
<ThemeProvider theme={Theme}>
Put content here
</ThemeProvider>
</>
);
Чтобы не погружаться в специфичные детали реализаций, я упомяну несколько решений, которые лично встречал при работе с сайтами: генерация стилей, подстановка переменных и CSS-in-JS.

Генерация стилей для подстановки в сгенерированный HTML через inline стили – это самый простой вариант “в лоб”, у которого есть главный недостаток: логика пользовательской темы начинает покрывать всё приложение и очень сильно внедряется в его исходный код, нарушая многие принципы чистой разработки.

Чтобы не расставлять код по генерации стилей повсюду, мы можем использовать отдельный сервис, который будет хранить состояние выбранной темы в виде набора переменных, а уже затем использовать его в компонентах для создания inline-стилей, либо для генерации единого CSS-файла. Очевидно, генерация inline-стилей сильно утяжелит итоговый HTML, поэтому я бы не советовал этот подход. Генерация единого файла стилей вручную в данном случае по сути является описанием варианта, который был назван выше.

Самым интересным и популярным на текущий момент решением для тех, кто не любит работать с CSS, является подход CSS-in-JS. Основной его особенностью является то, что мы можем прописывать все стили в виде JS-объектов, свободно подставляя любые настройки как со стороны фронта, так и бэкенда.

CSS-in-JS решения сами генерируют классы для подстановки, поэтому мы не должны беспокоиться о том что где-то опечатались или забыли добавить какой-то дополнительный стиль. Главной проблемой таких решений является то, что они изначально заточены на стилизацию именно внутренних компонентов системы, т. е. добавление стилей для встраиваемых виджетов или сторонних компонентов может вызвать некоторые трудности.

import jss from 'jss';
import preset from 'jss-preset-default';

jss.setup(preset());

const styles = {
'@global': {
body: {
color: 'green';
},
a: {
color: 'red';
}
},
button: {
color: 'blue';
}
}

const { classes } = jss.createStyleSheet(styles).attach();

document.body.innerHTML = `
<div>
<a href="/">Link</a>
<button>Button</button>
</div>
`;
Еще одним довольно популярным решением уже более приближенным к CSS являются Styled Components. При применении стилизованных компонентов мы фактически сразу привязываем стили к компоненту, при этом давая системе возможность подменять состояние этих стилей через JS.

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

Вариант третий: Препроцессоры и PostCSS​

Уже довольно давно мы привыкли работать не с чистым CSS, а с препроцессорами, чаще всего это LESS или SCSS. Основным преимуществом препроцессоров, в сравнении с чистым CSS, долгое время были механизмы миксинов и переменных.

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

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

896d8c4fff055ba7359cf325b7014c72.png

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

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

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

Вариант четвертый: CSS​

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

Переменные в CSS схожи с переменными в препроцессорах, но имеют определенный синтаксис. Преимущество использования этого типа переменных в том, что мы можем подменять значения через JS: element.style.setProperty("--main-color", customTheme.mainColor);

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

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

43f8754349be1b8e62a36714c024a296.png

Выводы​

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

 
Сверху