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

Когда запрос завершится (или завершится с ошибкой), сервисный работник создаст другое событие, чтобы уведомить о статусе.

Мне очень нравится этот подход, потому что он использует сервисного работника по-другому (по сравнению с типичным ПО для автономной поддержки). Его также можно использовать для PWA, которые, возможно, загружаются в автономном режиме, когда строка URL скрыта. Затем это сообщит пользователю, что что-то загружается.

Сразу скажу, что у этого подхода есть некоторые ограничения, и я расскажу о них в посте.

Установка сервис-воркера

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

Остальная логика находится внутри сервис-воркера (и обратите внимание, что нам не нужен app.js из простого метода во вчерашнем посте).

if ('serviceWorker' in navigator) {
  const LOADING = 0;
  const LOADED = 1;
  const FAILED = 2;

  navigator.serviceWorker.register('/sw.js');
  navigator.serviceWorker.addEventListener('message', function (event) {
    if (event.data === LOADING) {
      document.documentElement.className = 'loading';
    }

    if (event.data === LOADED) {
      document.documentElement.className = '';
    }

    if (event.data === FAILED) {
      document.documentElement.classList.add('fail');
    }
  });
}

Работник службы

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

Ниже приведен сервисный работник sw.js, который был зарегистрирован в более раннем фрагменте кода:

// when the browser fetches a url, either response with
// the cached object or go ahead and fetch the actual url
this.addEventListener('fetch', event => {
  if (event.request.mode === 'navigate') {
    // do a fetch, bug also emit the loading state
    event.respondWith(fetchAndEmit(event.request));
  } else {
    event.respondWith(fetch(event.request));
  }
});

function fetchAndEmit(request) {
  // TODO…
}

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

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

Получить и излучать

Суть функциональности находится внутри функции fetchAndEmit в файле sw.js. Задача состоит в том, чтобы сгенерировать событие на любой стороне процесса выборки. Когда выборка возвращается, она может сделать несколько вещей: 200 OK — запрос сработал, или выборка может сбросить, или она может не сработать (хотя я углублюсь в это подробнее).

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

Предостережение № 1: К сожалению, события навигации по выборке не включают в себя event.clientId (хотя все остальные типы включают), хотя в этой части спецификации работает редизайн, но реализации не догнал. Это означает, что приведенный ниже код должен сделать наилучшее предположение относительно клиента.

// replicated from the "client"
const LOADING = 0;
const LOADED = 1;
const FAILED = 2;

function fetchAndEmit(request) {
  // first get all the `window` objects that use this
  // service worker, then send state messages to it.
  return clients.matchAll({
    type: 'window'
  }).then(clients => {
    // attempt to find the client that initiated the
    // request based on whether it's in focus
    const activeClients = clients.filter(_ => _.focused);

    // note that with a navigate operation, there's two
    // clients, kind of like a double buffer effect, the
    // one that initiated the request, and the one that
    // will swap in once the content is parsed.
    activeClients.forEach(postMessage(LOADING));

    // now fetch the request, but before sending it back
    // in the promise, send the appropriate loading state
    return fetch(request).then(res => {
      // res.status is a weird case that's more likely
      // to throw in the future. It's where the user
      // cancelled the request,
      if (res.status === 0) {
        activeClients.forEach(postMessage(FAILED));
      } else {
        activeClients.forEach(postMessage(LOADED));
      }
      return res;
    }).catch(e => {
      // this could be a network timeout
      activeClients.forEach(postMessage(FAILED));

      // 204: No Content (to prevent page load)
      return new Response(null, { status: 204 });
    });
  });
}

// postMessage returns a function that posts
// to the given client (useful for .forEach)
function postMessage(message) {
  return client => client.postMessage(message);
}

Приведенный выше код перехватывает два исключения из ответа 200 OK:

  1. Если запрос выдает ошибку — например, тайм-аут сети или сервер разрывает соединение (т. е. при перезагрузке).
  2. Если запрос был отменен.

В первом случае выборка кидает. Однако, если посетитель отменяет запрос, то из приведенного выше кода видно, что я проверяю res.status === 0 — что на самом деле не имеет никакого смысла. Код состояния HTTP 0 отсутствует.

Если запрос будет отменен…

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

Это может показаться запутанным. Я имею в виду следующее: если посетитель останавливает загрузку страницы до того, как она начнет возвращаться, он отменяет запрос. Нет ничего, что позволило бы нам обнаружить отмененный запрос. Нет ни события, ничего. Чтение API запроса показывает, что ничего не доступно.

Я бы хотел, чтобы это изменилось с помощью запроса функции.

Ниже скриншот проблемы. Я разместил запрос в /hang, который обычно отвечает через 5 секунд, но через 2,5 секунды я отменил запрос, однако вы можете видеть, что запрос на основе сервисного работника все еще ожидает обработки. Это похоже на то, что сервис-воркер не может знать, что браузер больше не хочет запрашивать запрос.

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

Стоит ли использовать сервис-воркер?

Сейчас? Сегодня, в конце 2016 года, именно для этого эффекта: я не уверен. Это выполнимо, и это хорошее прогрессивное усовершенствование, но на самом деле оно предлагает не намного больше, чем упрощенная версия, которую я опубликовал вчера (хотя она обрабатывает отказы запросов).

Тем не менее, отсутствующий идентификатор event.clientId должен появиться в реализации достаточно скоро, и я верю, что я не единственный, кто хочет знать об отмененных запросах, поэтому я ожидаю, что он появится в 2017 году (надеюсь!) .

Первоначально опубликовано в журнале Remy Sharp’s b:log