Пару дней назад я переместил очередь запросов в веб-воркер в моем приложении React и столкнулся с некоторыми проблемами. Может, эта статья кому-то поможет, так что вперед!

Зачем мне это нужно?

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

Но что, если клиент потерял интернет-соединение на секунду прямо в тот момент, когда мы что-то запрашиваем? Попросить его «попробовать еще раз»? Что делать, если на странице загружается много чего по отдельности - может потребоваться некоторое время, чтобы щелкнуть «повторить попытку» в каждом разделе, и, возможно, соединение все еще будет отключено.

Или мы можем дождаться установления соединения и затем запросить у сервера данные, которые нам нужны. Мне это нравится!

Создайте очередь

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

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

class RequestsQueue {
   constructor() {
      this.queue = [];
      this.connection = new Connection();
   }
   push(request) {
      return new Promise((resolve, reject) => {
         const queueItem = () => {
            this.connection.doRequest(data)
               .then((data) => ({data}))
               .catch((error) => ({error}))
               .then(({data, error}) => {
                  if (!this.connection.active) {
                     this.queue.push(queueItem);
                     return;
                  }
      
                  if (error) {
                     reject(error);
                     return;
                  }
      
                  resolve(data);
               })
         };
   
         this.queue.push(queueItem);
         setTimeout(this.tryNext);
      })
   }
   tryNext() {
      if (this.queue.length === 0) {
         return;
      }
      const queueItem = this.queue.shift();
      queueItem();
      if (this.queue.length > 0) {
         setTimeout(this.tryNext);
      }
   }
}

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

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

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

Мы будем хранить запрос как функцию, поэтому он будет закрывать функции запроса и обратного вызова Promise.

И последнее - просто поместите функцию в массив и скажите очередь, чтобы проверить ее.

Метод tryNext ничего не делает, кроме рекурсивного вызова сохраненных запросов.

Работа с веб-воркерами

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

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

// requestsQueue.worker.js
const worker = self;
worker.onmessage = (event) => {
   const data = event.data; // Hello, there!
   
   worker.postMessage("Thank you, i've got it")
}

Мы также должны инициализировать воркер, вызвав конструктор воркера - он принимает URL-адрес рабочего файла в качестве аргумента:

const worker = new Worker("./requestsQueue.worker.js");
worker.postMessage("Hello there!");
worker.onmessage = (event) = > {
   const data = event.data; // Thank you, i've got it
   console.log("Worker responds", event.data);
}

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

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

class RequestsWorker {
   requests = {};
   requestsCounter = 0;
 
   constructor() {
      this.worker = new Worker("./requestsQueue.worker.js");
  
      this.worker.onmessage = this.receive;
   }
 
   send(...args) {
      const id = `request-${this.requestsCounter++}`;
  
      return new Promise((resolve, reject) => {
         this.requests[id] = ({data, error}) => {
            if (error) {
               reject(error);
               return;
            }
    
            resolve(data);
         };
   
         this.worker.postMessage({id, data: args})
      });
   }
 
   receive(event) {
      const {id, data, error} = event.data;
  
      if (typeof this.requests[id] === "function") {
         this.requests[id]({data, error});
         delete this.requests[id];
      }
   }
}

Также необходимо обновить рабочий файл:

worker.onmessage = (event) => {
   const {id, data} = event.data;
   
   doRequest(data) // Some fake API request
      .then((data) => ({data}))
      .catch((error)=> ({error}))
      .then(({data, error}) => {
          worker.postMessage({id, data, error});
      })
}

Теперь нас не заботит как запрос - мы просто делаем это и ждем ответа:

const reqestsWorker = new RequestsWorker();
reqestsWorker.send("Hello there!")
   .then((data) => {
      console.log(data);
   })
   .catch((error) => {
      console.log(Something bad happened", error);
   })

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

Исправление проблем

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

Работа с веб-пакетом

Возможно, вы используете webpack как конструктор приложений. Если да, то вам нужно внести некоторые изменения в вашу конфигурацию:

{
   test: /\.worker\.js$/,
   use: {loader: "worker-loader"}
}

Тогда вы должны использовать своего воркера следующим образом:

import Worker from "./requestsQueue.worker.js";

const worker = new Worker();

Проблема с расположением IE

Если вы используете некоторые свойства location глобального объекта внутри worker, то можете столкнуться с проблемой - IE 11 не имеет ни одного из этих свойств. Он возвращает URL-адрес рабочего, как показано ниже:

// requestsQueue.worker.js
const worker = self;
console.log(worker.location) // http://your-domain.com/requestsQueue.worker.js

Чтобы решить эту проблему, вы должны проанализировать этот URL-адрес или передать все необходимые данные о местоположении из основного приложения. Но будьте осторожны - worker.location не строка! Это объект, поэтому вы должны использовать worker.location.toString() для синтаксического анализа.

Резюме

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