Photo by Indira Tjokorda on Unsplash

Введение

Всем привет! В этой статье я хочу поделиться с вами деталями о моем последнем проекте — npm пакете под названием web-worker-bus. Этот пакет создан для упрощения работы с веб-воркерами и организации обмена данными между основным потоком и веб-воркерами в JavaScript-приложениях.

Пакет уже доступен для установки через npm, и его можно легко интегрировать в ваш проект, независимо от того, используете ли вы Angular, React, Vue или просто чистый JavaScript.

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


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

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

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

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

Определение терминов и концепций

Основной поток в контексте веб-разработки — это поток, в котором выполняется большая часть JavaScript-кода, включая обработку событий, обновление пользовательского интерфейса и так далее. Он отвечает за выполнение всего кода, который влияет на то, что пользователь видит в браузере.

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

Шина (bus) в данном контексте представляет собой механизм, который обеспечивает обмен данными и командами между различными частями системы, такими как веб-воркеры и основной поток.

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

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

Сервис пустышка (Mock Service) — это прокси-класс, который используется в основном потоке для вызова методов веб-воркера. Он облегчает взаимодействие с веб-воркером, делая его прозрачным для основного потока.

ReturnType — это перечисление, определяющее режим работы сервиса (работает ли он с Promise или с Observable объектами).

Архитектура и работа шины

Шина в данном контексте представляет собой систему, которая обеспечивает взаимодействие между основным потоком и веб-воркерами. Вот как это работает:

1. Регистрация и инициализация веб-воркеров:

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

2. Создание фабрики сервисов:

Фабрика сервисов создается на основе зарегистрированного веб-воркера. Она позволяет создавать экземпляры сервисов с нужными параметрами и настройками.

3. Использование сервисов пустышек:

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

4. Отправка сообщений в веб-воркер:

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

5. Получение результатов из веб-воркера:

Веб-воркер обрабатывает полученное сообщение и отправляет результат обратно. В зависимости от режима работы сервиса (ReturnType), результат может быть представлен как Promise или Observable.

6. Управление ресурсами:

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

Схема работы шины:

На схеме слева изображен основной поток, справа — веб-воркеры. Реальный экземпляр сервиса расположен в веб-воркере, а в основном потоке находится его мок. Когда мы вызываем метод, мок сервис перенаправляет этот вызов в часть шины в основном потоке, которая, в свою очередь, перенаправляет его в часть шины в веб-воркере согласно настройкам регистрации воркера в шине. Затем вызов передается реальному экземпляру сервиса. Ответ от сервиса направляется в основной поток по той же логике. На уровне библиотеки используется технология postMessage для передачи сообщений, но вы можете реализовать свою логику и передать ее в шину.

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

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

Классы библиотеки:

// Класс для регистрации веб-воркеров и создания фабрики сервисов пустышек
class MainThreadBus {
  // Регистрация веб-воркеров
  registerBusWorkers(transports: ITransport[]) { /* ... */ }
  
  // Создание фабрики сервисов пустышек
  createFactoryService(transport: ITransport) { /* ... */ }
}

// Класс для регистрации реального сервиса в шине на стороне веб-воркера
class BusWorker {
  getService!: ServiceGetter;
  transport!: ITransport;
  // Подключение к шине
  static connectToBus(transport: ITransport, getService: ServiceGetter, initHandler?: InitEventHandler) { /* ... */ }
}

// Транспортный уровень, использующий postMessage
export class ObjectCopyTransport implements ITransport {
  constructor(private readonly ctx: Worker) { /* ... */ }
  
  // Обработчик сообщений
  protected messageHandler(event: MessageEvent<SendCommand>): void { /* ... */ }
  
  // Отправка сообщения
  sendMsg(msg: unknown): void { this.ctx.postMessage(msg); }
}

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

import { MainThreadBus, ObjectCopyTransport } from 'web-worker-bus';
// Создание веб-воркера
const worker = new Worker(new URL('./Services/UserWorker', import.meta.url));
const userTransport = new ObjectCopyTransport(worker);
// Регистрация воркера
MainThreadBus.instance.registerBusWorkers([userTransport]);
// Создание фабрики, привязанной к воркеру
export const userWorkerFactory = MainThreadBus.instance.createFactoryService(userTransport);

Создание фабрики, привязанной к воркеру UserWorker.

Код в веб-воркере:

import { BusWorker, ObjectCopyTransport, ServiceGetter } from 'web-worker-bus';
import { container } from './UserWorkerContainer';

/* eslint-disable-next-line no-restricted-globals */
const worker = self as unknown as Worker;

const serviceGetter: ServiceGetter = (serviceName) => {
  // Возвращаем экземпляр сервиса UserService, используя любой контейнер или создавая экземпляр класса напрямую
  return container[serviceName as keyof typeof container];
};
// Подключение веб-воркера к шине
BusWorker.connectToBus(new ObjectCopyTransport(worker), serviceGetter);

Реализация для сервиса, который использует Observable:

// Сервис для получения комментариев пользователя
export class UserServiceWithObservable {
  public getUserComments(): Observable<UserComments[]> { /* ... */ }
}

Создание сервиса пустышки в основном потоке, который связан с сервисом в веб-воркере:

const userService = userWorkerFactory<UserService>("UserService", ReturnType.rxjsObservable);

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

Готовые примеры вместе с популярными фреймворками можете увидеть, перейдя по ссылкам ниже:

Обратите внимание, для Angular пришлось создать NgObjectCopyTransport — транспортный уровень, который оборачивает обработку полученных сообщений от веб-воркеров в NgZone для корректной работы рендера Angular.

Основные преимущества

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

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

  3. Гибкость: Благодаря фабрике сервисов пустышек и возможности создавать различные транспортные уровни, данная библиотека может быть использована в различных сценариях и с разными фреймворками.

  4. Поддержка Observable и Promise: Встроенная поддержка наблюдаемых и обещаний облегчает интеграцию с современными приложениями и практиками программирования.

Применение

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

  2. Работа с большими данными: При работе с большими объемами данных, такими как анализ или визуализация, использование веб-воркеров с вашей библиотекой поможет обрабатывать данные эффективнее и быстрее.

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

  4. Масштабируемость: Система удобна для масштабирования и может быть легко расширена для поддержки дополнительных сервисов и воркеров.

  5. Платформенная независимость: Поддержка различных транспортных уровней и интеграция с популярными фреймворками делает библиотеку применимой в различных платформах и средах.

Ограничения первой версии шины

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

  1. Не поддерживаются методы, созданные через symbol: Это может влиять на некоторые особенности и паттерны проектирования, которые вы можете использовать в своем коде.

  2. Транспортный уровень использует копирование сообщения при передаче данных между потоками: Это может увеличить время выполнения кода, в зависимости от объема передаваемых данных. Необходимо учитывать это при работе с большими объемами данных.

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

  4. Можно использовать в рамках одного сервиса только Promise или только RxJs Observable объектами: Это ограничивает гибкость в выборе подходов в разных частях одного и того же сервиса, но обеспечивает последовательность и согласованность в использовании.

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

Заключение

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

Главные преимущества этого подхода включают в себя:

  • Улучшение производительности путем распределения нагрузки на разные потоки.

  • Возможность создания более чистого и модульного кода.

  • Повышение масштабируемости и гибкости в разработке.

Однако также существуют и определенные ограничения, которые следует учитывать при работе с библиотекой.

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

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

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

Если вы заинтересованы в этом проекте, ваш вклад будет очень ценен. Вы можете помочь улучшить библиотеку, исправить ошибки, добавить новые функции или даже написать документацию. Любые предложения и пул-реквесты приветствуются! :)

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

Спасибо за внимание к статье!

Комментарии (7)


  1. RegIon
    05.08.2023 17:41

    А почему не comlink ?

    https://github.com/GoogleChromeLabs/comlink


    1. andry36 Автор
      05.08.2023 17:41

      привет! если честно, не видел этот пакет. Изначально создавал шину для Observable из RxJs для проекта на Angular. Я так понял в comlink нет этой поддержки. С Typescript он окей работает?


      1. eshimischi
        05.08.2023 17:41

        Какой поддержки нет? Comlink написан на Typescript


  1. Bigata
    05.08.2023 17:41

    А кроме лозунгов, будет показано в чём выигрыш например по заявленному "...Улучшение производительности..." или "...Работа с большими данными..."?


    1. andry36 Автор
      05.08.2023 17:41

      ???? в статье есть ссылки на примеры для разных framework-в. В нем 3 вкладки с графиком. на первой пример без без шины, на второй и третье с шиной с Observable и Promise соответственно.


      1. FireWind
        05.08.2023 17:41

        Не вижу вкладок с графиком. Где они могут быть? Например, для Vue. Не могли бы вы дать точный адрес страницы графика, пожалуйста?


      1. Bigata
        05.08.2023 17:41

        тоже не вижу графиков. А есть сравнение с нативным js?