Заинтересованы в изучении JavaScript? Получите мой справочник по JavaScript

В этом тематическом исследовании объясняется, как я добавил возможность работы в автономном режиме на веб-сайт writeoftware.org (который основан на Grav, отличной CMS на основе PHP для разработчиков). Я сделал это, представив набор технологий, сгруппированных под названием Progressive Web Apps (в частности, Service Workers и Cache API).

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

Я покажу варианты, которые у меня были, и почему я предпочел один подход другим.

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

Первый подход: сначала кэш

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

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

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

Я пройду пару этапов:

  1. Я представляю Service Worker и загружаю его с помощью сценария JS.
  2. При установке Service Worker я кэширую скелет сайта.
  3. Я перехватываю сетевые запросы, ведущие к дополнительным ссылкам, и кэширую их

Представляем сервис-воркера

Я добавляю Service Worker в sw.js файл в корень сайта. Это дает ему достаточно возможностей для работы со всеми вложенными папками сайта, а также с домашней страницей сайта (подробнее о сфере действия Service Workers здесь). Программное обеспечение на данный момент довольно простое, поскольку оно просто регистрирует любой сетевой запрос:

Мне нужно зарегистрировать Service Worker, и я делаю это из сценария, который включаю на каждую страницу:

Если Service Workers доступны, мы регистрируем файл sw.js, и в следующий раз, когда я обновляю страницу, он должен работать нормально:

На этом этапе мне нужно немного поработать над сайтом. Прежде всего, мне нужно придумать способ обслуживания только оболочки приложения: базового набора HTML + CSS и JS, который всегда будет доступен и показан пользователям даже в автономном режиме.

По сути, это урезанная версия веб-сайта с <div class="wrapper row" id="content-wrapper"></div> пустым элементом, который мы заполним содержимым позже, доступным по пути /shell:

Таким образом, при первой загрузке сайта пользователем будет показана обычная версия страницы (полная версия HTML) и Service Worker установлен.

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

Как?

Мы прослушиваем событие install, которое возникает при установке или обновлении Service Worker. Когда это происходит, мы инициализируем кеш содержимым нашей оболочки: базовым HTML-макетом, а также некоторыми CSS, JS и некоторыми внешними ресурсами:

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

Если URL-адрес принадлежит Google Analytics или ConvertKit, я избегаю использования локального кеша и получаю их без использования CORS (поскольку мне не разрешен доступ к ним с помощью этого метода).

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

Если это не часть, мы возвращаем оболочку, которая уже кэшируется при первой установке Service Worker.

Как только выборка завершена, я кэширую ее.

Теперь я редактирую файл script.js, чтобы ввести важную функцию: всякий раз, когда на моих страницах нажимается ссылка, я перехватываю ее и отправляю сообщение на канал вещания.

Поскольку Service Workers в настоящее время поддерживаются только в Chrome, Firefox и Opera, я могу смело полагаться на BroadcastChannel API для этого.

Сначала я подключаюсь к каналу ws_navigation и прикрепляю к нему обработчик событий onmessage. Всякий раз, когда я получаю событие, это сообщение от Service Worker с новым контентом, который будет отображаться в оболочке приложения. Поэтому я просто ищу элемент с идентификатором content-wrapper и помещаю в него частичное содержимое страницы, эффективно изменяя страницу, которую видит пользователь.

Как только Service Worker зарегистрирован, я отправляю сообщение на этот канал с задачей fetchPartial и частичным URL страницы для выборки. Это содержимое начальной загрузки страницы.

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

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

При получении частичного запроса я прикрепляю ?partial=true запрос, чтобы сообщить своему бэкэнду, что он должен обслуживать только контент, а не оболочку.

Теперь мы просто упускаем возможность обработать это событие. На стороне Service Worker я подключаюсь к каналу ws_navigation и прослушиваю событие. Я прислушиваюсь к fetchPartial имени задачи сообщения, хотя я мог бы просто избежать этой проверки условий, так как это единственное событие, которое отправляется сюда. Обратите внимание, что сообщения в Broadcast Channel API не отправляются на ту же страницу, которая их отправляет - они отправляются только между страницей и веб-исполнителем.

Я проверяю, кэширован ли URL. Если так, я просто отправляю его как ответное сообщение по каналу и возвращаюсь.

Если он не кэширован, я получаю его, отправляю обратно как сообщение на страницу, а затем кеширую его для следующего посещения.

Мы почти закончили.

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

И все это прогрессивное улучшение. Старые браузеры и браузеры, не поддерживающие Service Workers, просто работают в обычном режиме.

Теперь захват навигации в браузере создает несколько проблем:

  1. URL-адрес должен измениться при отображении новой страницы. Кнопка «Назад» должна работать нормально, как и история браузера.
  2. Заголовок страницы должен измениться, чтобы отразить новый заголовок страницы.
  3. Нам необходимо уведомить Google Analytics API о загрузке новой страницы, чтобы не пропустить важный показатель, например количество просмотров страниц на посетителя.
  4. фрагменты кода больше не выделяются при динамической загрузке нового содержания.

Давайте решим эти проблемы.

Исправьте URL, заголовок и кнопку возврата с помощью History API

Помимо внедрения HTML партиала в обработчик сообщений в script.js, мы запускаем метод history.pushState():

Это работает, но заголовок страницы не изменяется в пользовательском интерфейсе браузера. Нам нужно как-то получить его со страницы. Я решил поместить в часть содержимого страницы скрытый промежуток, который сохраняет заголовок страницы. Затем мы можем получить его со страницы с помощью DOM API и установить свойство document.title:

Исправить Google Analytics

Google Analytics сразу работает нормально, но при динамической загрузке страницы чудес не творит. Мы должны использовать API, который он предоставляет, чтобы сообщить ему о загрузке новой страницы. Поскольку я использую отслеживание Global Site Tag (gtag.js), мне нужно позвонить:

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

Что делать, если ... пользователь не в сети? В идеале должен быть fetch прослушиватель событий, который кэширует все запросы, поступающие в Google Analytics, и воспроизводит их, как только я снова в сети.

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

Исправить подсветку синтаксиса

Последнее, что мне нужно исправить на моей странице, - это выделение логина фрагментов кода. Я использую подсветку синтаксиса Prism, и они очень упрощают задачу - мне просто нужно добавить вызов Prism.highlightAll() в мой onmessage обработчик:

Полный код script.js:

и sw.js:

Второй подход: сначала сеть, удалите оболочку приложения

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

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

Как только мы получаем страницу, мы кэшируем ее (не проверяя, кэшировали мы ее ранее или нет, мы просто сохраняем последнюю версию).

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

Для этого я просто удалил оболочку приложения из события install Service Worker. Я полагался на Service Workers и Cache API для доставки простых страниц сайта без управления частичными обновлениями. Я также отказался от захвата /shell fetch при загрузке полной страницы. При загрузке первой страницы задержки нет, но мы по-прежнему загружаем частичные данные при переходе на другие страницы позже.

Я по-прежнему использую script.js и sw.js для размещения кода, причем script.js является файлом, который инициализирует Service Worker, а также перехватывает клики на стороне клиента.

Вот script.js:

а вот sw.js:

Третий подход: упрощение и полное отсутствие частичных данных

В качестве эксперимента я отказался от перехватчика кликов, который извлекает частичные данные, и полагался на Service Workers и Cache API, чтобы просто доставить простые страницы сайта без управления частичными обновлениями:

script.js :

sw.js :

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

Что я в итоге реализовал на своем веб-сайте

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

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

Заинтересованы в изучении JavaScript? Получите мой справочник по JavaScript