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

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

Эта проблема

Решив использовать переменные CSS (или настраиваемые свойства, как они официально называются), мы знали, что столкнемся с другой проблемой: поддержка браузера.

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

Вот как мы используем переменные CSS в наших файлах SASS:

.dashboard {
  background: var(--theme-primary-color);
}

Наши темы хранятся в виде объектов Javascript, указывающих значения переменных. Эти переменные обновляются, когда пользователь вносит изменения в тему:

const variables = {
  '--theme-primary-color': '#8DC63F',
};

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

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

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

Чтобы избежать замедления работы пользовательского интерфейса при компиляции CSS, мы решили попробовать это в веб-воркере.

Что такое веб-работник?

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

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

После создания из основного потока Javascript общий рабочий процесс веб-воркера обычно следует такой последовательности:

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

Чтобы создать нового рабочего, вы вызываете конструктор Worker(), указывая путь к файлу скрипта, который будет запускаться в рабочем потоке:

const myWorker = new Worker('worker.js');

Для связи с рабочим из основного потока вы используете метод postMessage и onmessage обработчик событий:

// Post a message to the worker
myWorker.postMessage('Hello Worker!');
// Listen for messages sent by the worker
myWorker.onmessage = event => {
  const dataFromWorker = event.data;
  console.log('Message received from worker.');
}

Так как же именно выглядит этот рабочий? Веб-воркер - это файл сценария, который может использовать большую часть языка Javascript и его API. Однако у него нет доступа к DOM, поэтому любые операции DOM должны обрабатываться основным потоком Javascript. Единственное, что вам нужно включить в веб-воркер, - это onmessage прослушиватель, чтобы иметь возможность получать сообщения из основного потока. Слушатель определяется в глобальной области видимости исполнителя:

// Listen to messages from the main thread
onmessage = event => {
  console.log('Message received from main script');
  const dataFromMainThread = event.data;
  
  // Post message back to main script
  postMessage('Hi, I got your message.');
}

Это все, что вам нужно для настройки и запуска базового веб-воркера.

Наше решение

Помня все это, мы решили создать веб-воркера, которому мы могли бы отправлять переменные CSS по мере их изменения пользователем. Рабочий получит CSS (содержащий переменные) и карту переменных для значений, он заменит переменные их значениями и отправит результат обратно в основной Javascript, который затем применит его к странице.

Первоначально у нас был веб-воркер, который извлекал файл CSS, и поначалу казалось, что он работает хорошо. Но после некоторого тестирования мы поняли, что это вызывает проблемы CORS в IE из-за архитектуры нашего приложения. Вместо этого мы решили получить CSS в основном потоке и отправить его в виде строки веб-воркеру вместе с переменными. Это оказалось хорошим решением, так как он упростил код веб-воркера, а также нам не нужно было связывать библиотеку ajax с воркером. Теперь веб-работник принимает CSS как строку и объект с именами переменных и их новыми значениями.

Вот базовое использование, которое создает воркер и обновляет цвет фона.

const worker = new Worker('worker.js')
const cssText = 'body { background: var(--bg-color); }';
const variables = { '--bg-color' : pink };
worker.postMessage({
  cssText,
  variables,
});
// After generating the CSS with the new colors, the worker will 
// send a message back to the main thread:
worker.onmessage = event => {
  const newCSS = event.data;
  // In this case newCSS will be 'body { background: pink; }'
}

Фактический рабочий код довольно прост. Он прослушивает сообщения из основного потока и использует регулярное выражение для замены переменных в CSS.

/*
 * regEx Matches all instances of var(*), which is the syntax for  
 * CSS Variables
 * e.g. "color: var(--text-color);" matches "var(--text-color)"
 * as a group and "--text-color" as a sub-group
 */
const regEx = /var\(([^)]+)\)/g;
const generateCSS = (cssText, variables) => {
  return cssText.replace(regEx, (match, variable) => {
    // Return the value for the variable if present or 
    // leave it untouched.
    return variables[variable] || match;
  });
};
// Listener for messages from main thread
onmessage = event => {
  const { variables, cssText } = event.data;
  
  // Compile the new CSS
  const newCss = generateCSS(cssText, variables);
  
  // Post compiled CSS back to main thread
  postMessage(newCss);
};

Web worker и React (фактическая реализация)

В нашем исходном решении с использованием переменных CSS мы создали компонент React для обработки обновления переменных. Мы создали новый компонент аналогичным образом для отправки обновлений веб-воркеру, когда тема была изменена пользователем. Компонент получает обновленный CSS от рабочего и применяет новые стили.

class CSSWorker extends Component {
  componentDidMount() {
    this.worker = new Worker('css-variables-worker.js');
    
    this.worker.onmessage = event => {
      this.setState({ css: event.data });
    }
    this.variablesDidChange(this.props.variables);
  }
  componentDidRecieveProps() {
    this.variablesDidChange(this.props.variables);
  }
  variablesDidChange(variables) {
    fetch('styles.css').then(response => {
      // CSS was fetched, tell worker to generate new CSS
      const cssText = response.text();
      
      this.worker.postMessage({
        variables,
        cssText,
      });
    });
  }
  render() {
    const { css } = this.state;
    return (
      <style>
        {css}
      </style>
    );
  }
}

Это основа рабочего компонента. Осталось только отобразить его на странице, когда переменные CSS не поддерживаются. Для этого мы используем CSS.supports(), чтобы проверить, можно ли использовать переменные CSS. Когда переменные CSS не поддерживаются, мы заменяем исходный компонент CSSVariableApplicator новым компонентом CSSWorker.

const ThemeApplier = ({ variables }) => {
  // Use lodash attempt() and CSS.supports() to check if CSS
  // variables are supported
  const supportsCSSVars = (
    attempt(() => window.CSS.supports('--foo', 'red')) === true
  );
  const supportsWebWorker = window.Worker;
  if (!supportsCSSVars && supportsWebWorker) {
    return <CSSWorker variables={variables} />;
  } else {
    return <CSSVariableApplicator variables={variables} />;
  }
};

В данном конкретном случае это оказалось неплохим откатом к переменным CSS. Изменение цветов кажется немного медленнее, чем исходное решение в предварительном просмотре, чего и следовало ожидать.

Минусы

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

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

Но мы не можем игнорировать вопрос: действительно ли нам вообще нужно было использовать для этого веб-воркер? Наше первоначальное предположение, что компиляция нового файла CSS на лету будет слишком сложной для потока пользовательского интерфейса. в то время имело смысл обращаться. Мы предположили, что нам потребуется выполнить полный синтаксический анализ CSS, чтобы заменить переменные, а это потенциально могло оказаться дорогостоящей операцией. Однако, как только мы создали веб-воркера, мы поняли, что все, что ему действительно нужно сделать, это выполнить операцию замены строки с использованием регулярного выражения.

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

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

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

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

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

А еще нанимаем!