Давайте поговорим о... архитектуре?
Я собираюсь затронуть важную, но потенциально неправильно понимаемую тему: архитектуру, которую вы используете для своего веб-приложения, и, в частности, то, как ваши архитектурные решения вступают в игру при создании прогрессивного веб-приложения.
«Архитектура» может звучать расплывчато, и может быть не сразу понятно, почему это важно. Итак, один из способов подумать об архитектуре — задать себе следующие вопросы: Когда пользователь посещает страницу моего сайта, какой HTML-код загружается? И потом, что загружается, когда они посещают другую страницу?
Ответы на эти вопросы не всегда однозначны, и как только вы начнете думать о прогрессивных веб-приложениях, они могут стать еще более сложными. Итак, моя цель — рассказать вам об одной возможной архитектуре, которую я считаю эффективной. В этой статье я буду называть принятые мной решения «своим подходом» к созданию прогрессивного веб-приложения.
Вы можете использовать мой подход при создании собственного PWA, но в то же время всегда есть другие действительные альтернативы. Я надеюсь, что вид того, как все детали сочетаются друг с другом, вдохновит вас и вы почувствуете, что можете настроить это в соответствии со своими потребностями.
Переполнение стека PWA
В дополнение к этой статье я создал PWA Stack Overflow . Я трачу много времени на чтение и участие в Stack Overflow , и мне хотелось создать веб-приложение, которое облегчило бы просмотр часто задаваемых вопросов по заданной теме. Он построен на основе общедоступного API Stack Exchange . Это открытый исходный код, и вы можете узнать больше , посетив проект GitHub .
Многостраничные приложения (MPA)
Прежде чем я углублюсь в подробности, давайте определим некоторые термины и объясним основные технологии. Сначала я собираюсь рассказать о том, что мне нравится называть «многостраничными приложениями» или «MPA».
MPA — это причудливое название традиционной архитектуры, используемой с самого начала Интернета. Каждый раз, когда пользователь переходит по новому URL-адресу, браузер постепенно отображает HTML, специфичный для этой страницы. Не предпринимается никаких попыток сохранить состояние страницы или ее содержимое между переходами. Каждый раз, когда вы посещаете новую страницу, вы начинаете все сначала.
Это отличается от модели одностраничных приложений (SPA) для создания веб-приложений, в которой браузер запускает код JavaScript для обновления существующей страницы, когда пользователь посещает новый раздел. И SPA, и MPA являются одинаково подходящими моделями для использования, но в этой статье я хотел изучить концепции PWA в контексте многостраничного приложения.
Надежно быстро
Вы слышали, как я (и многие другие) использую фразу «прогрессивное веб-приложение» или PWA. Возможно, вы уже знакомы с некоторыми справочными материалами на этом сайте .
Вы можете думать о PWA как о веб-приложении, которое обеспечивает первоклассный пользовательский интерфейс и действительно заслуживает места на главном экране пользователя. Аббревиатура « FIRE », что означает « Быстрый , Интегрированный , Надежный и Интересный », суммирует все атрибуты, о которых следует помнить при создании PWA.
В этой статье я собираюсь сосредоточиться на подмножестве этих атрибутов: быстроте и надежности .
Быстро: Хотя слово «быстро» означает разные вещи в разных контекстах, я собираюсь рассказать о преимуществах скорости, связанной с минимальной загрузкой из сети.
Надежность: Но чистой скорости недостаточно. Чтобы чувствовать себя как PWA, ваше веб-приложение должно быть надежным. Он должен быть достаточно устойчивым, чтобы всегда что-то загружать, даже если это просто настроенная страница ошибки, независимо от состояния сети.
Надежно быстро: И, наконец, я собираюсь немного перефразировать определение PWA и посмотреть, что значит создать что-то надежно быстрое. Недостаточно быть быстрым и надежным только тогда, когда вы находитесь в сети с низкой задержкой. Надежная скорость означает, что скорость вашего веб-приложения постоянна, независимо от условий сети.
Включение технологий: Service Workers + Cache Storage API
PWA устанавливают высокую планку скорости и устойчивости. К счастью, веб-платформа предлагает некоторые строительные блоки, позволяющие воплотить такую производительность в жизнь. Я имею в виду сервис-воркеров и API Cache Storage .
Вы можете создать сервис-воркера, который прослушивает входящие запросы, передает их в сеть и сохраняет копию ответа для будущего использования через Cache Storage API.
![Работник службы, использующий API хранилища кэша для сохранения копии ответа сети.](https://developer.chrome.com/static/blog/beyond-spa/image/a-service-worker-using-c-b29eb5eb773.png?authuser=8&hl=ru)
В следующий раз, когда веб-приложение выполнит тот же запрос, его сервис-воркер сможет проверить его кэши и просто вернуть ранее кэшированный ответ.
![Работник службы, использующий Cache Storage API для ответа, минуя сеть.](https://developer.chrome.com/static/blog/beyond-spa/image/a-service-worker-using-c-5d450862f7ef4.png?authuser=8&hl=ru)
По возможности избегайте подключения к сети — это важнейшая часть обеспечения надежной и высокой производительности.
«Изоморфный» JavaScript
Еще одна концепция, которую я хочу осветить, — это то, что иногда называют «изоморфным» или «универсальным» JavaScript . Проще говоря, это идея о том, что один и тот же код JavaScript может использоваться разными средами выполнения. Когда я создавал PWA, я хотел поделиться кодом JavaScript между моим внутренним сервером и сервис-воркером.
Существует множество действенных подходов к совместному использованию кода таким образом, но мой подход заключался в использовании модулей ES в качестве окончательного исходного кода. Затем я транспилировал и связал эти модули для сервера и сервис-воркера, используя комбинацию Babel и Rollup . В моем проекте файлы с расширением .mjs
— это код, который находится в модуле ES.
Сервер
Помня об этих концепциях и терминологии, давайте углубимся в то, как я на самом деле создал PWA с переполнением стека. Я собираюсь начать с описания нашего внутреннего сервера и объяснить, как он вписывается в общую архитектуру.
Я искал сочетание динамического бэкэнда и статического хостинга, и мой подход заключался в использовании платформы Firebase.
Облачные функции Firebase будут автоматически запускать среду на основе Node при поступлении входящего запроса и интегрироваться с популярной платформой Express HTTP , с которой я уже был знаком. Он также предлагает готовый хостинг для всех статических ресурсов моего сайта. Давайте посмотрим, как сервер обрабатывает запросы.
Когда браузер отправляет запрос на навигацию к нашему серверу, он выполняет следующий процесс:
![Обзор создания ответа навигации на стороне сервера.](https://developer.chrome.com/static/blog/beyond-spa/image/an-overview-generating-ee8d81d8ba352.png?authuser=8&hl=ru)
Сервер маршрутизирует запрос на основе URL-адреса и использует логику шаблонов для создания полного HTML-документа. Я использую комбинацию данных из Stack Exchange API, а также частичные фрагменты HTML, которые сервер хранит локально. Как только наш сервис-воркер узнает, как реагировать, он может начать потоковую передачу HTML обратно в наше веб-приложение.
Есть две части этой картины, которые стоит рассмотреть более подробно: маршрутизация и шаблонизация.
Маршрутизация
Когда дело доходит до маршрутизации, я использовал собственный синтаксис маршрутизации платформы Express. Он достаточно гибок, чтобы сопоставлять простые префиксы URL-адресов, а также URL-адреса, которые включают параметры как часть пути. Здесь я создаю сопоставление между именами маршрутов и базовым шаблоном Express для сопоставления.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
Затем я могу ссылаться на это сопоставление непосредственно из кода сервера . При обнаружении соответствия заданному шаблону Express соответствующий обработчик отвечает логикой шаблона, специфичной для соответствующего маршрута.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
// Templating logic.
});
Серверные шаблоны
И как выглядит эта шаблонная логика? Что ж, я выбрал подход, который последовательно собирал частичные фрагменты HTML, один за другим. Эта модель хорошо подходит для потоковой передачи.
Сервер немедленно отправляет обратно некоторый исходный шаблон HTML, и браузер может сразу же отобразить эту частичную страницу. Когда сервер собирает воедино остальные источники данных, он передает их в браузер до тех пор, пока документ не будет завершен.
Чтобы понять, что я имею в виду, взгляните на экспресс-код одного из наших маршрутов:
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
Используя метод write()
объекта response
и ссылаясь на локально сохраненные частичные шаблоны, я могу немедленно запустить поток ответа, не блокируя какой-либо внешний источник данных. Браузер берет этот исходный HTML и сразу же отображает содержательный интерфейс и загружает сообщение.
Следующая часть нашей страницы использует данные из Stack Exchange API . Получение этих данных означает, что нашему серверу необходимо сделать сетевой запрос. Веб-приложение не может отображать что-либо еще, пока не получит ответ и не обработает его, но, по крайней мере, пользователи не смотрят на пустой экран, пока ждут.
Как только веб-приложение получает ответ от API Stack Exchange, оно вызывает специальную функцию шаблонов для перевода данных из API в соответствующий HTML.
Язык шаблонов
Шаблонизация может оказаться на удивление спорной темой, и то, что я выбрал, — это лишь один из многих подходов. Вам захочется заменить собственное решение, особенно если у вас есть устаревшие связи с существующей платформой шаблонов.
В моем случае имело смысл просто полагаться на литералы шаблонов JavaScript с некоторой логикой, разбитой на вспомогательные функции. Одна из приятных особенностей создания MPA заключается в том, что вам не нужно отслеживать обновления состояния и повторно отображать HTML, поэтому мне помог базовый подход, создающий статический HTML.
Итак, вот пример того, как я создаю шаблон динамической HTML-части индекса моего веб-приложения. Как и в случае с моими маршрутами, логика шаблонов хранится в модуле ES , который можно импортировать как на сервер, так и на сервис-воркера.
export function index(tag, items) {
const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
const form = `<form method="GET">...</form>`;
const questionCards = items
.map(item =>
questionCard({
id: item.question_id,
title: item.title,
})
)
.join('');
const questions = `<div id="questions">${questionCards}</div>`;
return title + form + questions;
}
Эти шаблонные функции представляют собой чистый JavaScript, и при необходимости полезно разбить логику на более мелкие вспомогательные функции. Здесь я передаю каждый из элементов, возвращаемых в ответе API, в одну такую функцию, которая создает стандартный элемент HTML со всеми соответствующими установленными атрибутами.
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url="${questionUrl(id)}">${title}</a>`;
}
Особо следует отметить атрибут данных , который я добавляю к каждой ссылке, data-cache-url
, для которого задан URL-адрес Stack Exchange API, который мне нужен для отображения соответствующего вопроса. Запомни. Я вернусь к этому позже.
Возвращаясь к обработчику маршрута , после завершения создания шаблона я передаю последнюю часть HTML-кода моей страницы в браузер и завершаю поток. Это сигнал браузеру о завершении прогрессивного рендеринга.
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
Итак, это краткий обзор настройки моего сервера. Пользователи, которые впервые посещают мое веб-приложение, всегда получат ответ от сервера, но когда посетитель вернется в мое веб-приложение, мой сервис-воркер начнет отвечать. Давайте нырнем туда.
Сервисный работник
![Обзор создания ответа навигации в сервис-воркере.](https://developer.chrome.com/static/blog/beyond-spa/image/an-overview-generating-f46e582b6bca7.png?authuser=8&hl=ru)
Эта диаграмма должна выглядеть знакомой — многие из тех же частей, которые я рассматривал ранее, расположены здесь в несколько ином расположении. Давайте пройдемся по потоку запросов, принимая во внимание сервис-воркера.
Наш сервис-воркер обрабатывает входящий запрос навигации по заданному URL-адресу и, как и мой сервер, использует комбинацию логики маршрутизации и шаблонов, чтобы выяснить, как реагировать.
Подход тот же, что и раньше, но с другими низкоуровневыми примитивами, такими как fetch()
и Cache Storage API . Я использую эти источники данных для создания HTML-ответа, который сервисный работник передает обратно в веб-приложение.
Рабочий ящик
Вместо того, чтобы начинать с нуля с примитивов низкого уровня, я собираюсь построить своего сервис-воркера на основе набора библиотек высокого уровня под названием Workbox . Он обеспечивает прочную основу для логики кэширования, маршрутизации и генерации ответов любого сервисного работника.
Маршрутизация
Как и в случае с моим серверным кодом, мой сервис-воркер должен знать, как сопоставить входящий запрос с соответствующей логикой ответа.
Мой подход заключался в том, чтобы преобразовать каждый экспресс-маршрут в соответствующее регулярное выражение , используя полезную библиотеку под названием regexparam
. Как только этот перевод будет выполнен, я смогу воспользоваться встроенной поддержкой Workbox для маршрутизации регулярных выражений .
После импорта модуля, содержащего регулярные выражения, я регистрирую каждое регулярное выражение в маршрутизаторе Workbox. Внутри каждого маршрута я могу предоставить собственную логику шаблонов для генерации ответа. Создание шаблонов в сервис-воркере требует немного больше усилий, чем на моем внутреннем сервере, но Workbox помогает выполнять большую часть тяжелой работы.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Статическое кэширование активов
Одна из ключевых частей истории создания шаблонов — убедиться, что мои частичные HTML-шаблоны доступны локально через Cache Storage API и поддерживаются в актуальном состоянии при развертывании изменений в веб-приложении. Обслуживание кэша может быть подвержено ошибкам, если выполнять его вручную, поэтому я обращаюсь к Workbox, чтобы выполнить предварительное кэширование как часть процесса сборки.
Я сообщаю Workbox, какие URL-адреса следует предварительно кэшировать, с помощью файла конфигурации , указывая на каталог, который содержит все мои локальные ресурсы вместе с набором шаблонов для сопоставления. Этот файл автоматически считывается CLI Workbox , который запускается каждый раз, когда я перестраиваю сайт.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox делает снимок содержимого каждого файла и автоматически вставляет этот список URL-адресов и версий в мой окончательный рабочий файл службы. Теперь в Workbox есть все необходимое для того, чтобы предварительно кэшированные файлы всегда были доступны и обновлялись. Результатом является файл service-worker.js
, который содержит что-то похожее на следующее:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
Для людей, которые используют более сложный процесс сборки, Workbox имеет как плагин webpack
, так и универсальный модуль узла в дополнение к интерфейсу командной строки .
Потоковое вещание
Далее я хочу, чтобы сервисный работник немедленно передал этот предварительно кэшированный частичный HTML-код обратно в веб-приложение. Это важная часть «надежной скорости» — я всегда сразу вижу на экране что-то значимое. К счастью, использование Streams API в нашем сервис-воркере делает это возможным.
Возможно, вы уже слышали об API Streams. Мой коллега Джейк Арчибальд уже много лет восхваляет его. Он сделал смелое предсказание , что 2016 год станет годом веб-стримов. И API Streams сегодня так же великолепен, как и два года назад, но с существенным отличием.
Тогда только Chrome поддерживал Streams, сейчас Streams API поддерживается более широко . В целом история положительная, и при наличии соответствующего резервного кода ничто не мешает вам использовать потоки в своем сервис-воркере уже сегодня.
Что ж... возможно, вас что-то останавливает, и это понимание того, как на самом деле работает Streams API. Он предоставляет очень мощный набор примитивов, и разработчики, которым удобно его использовать, могут создавать сложные потоки данных, подобные следующим:
const stream = new ReadableStream({
pull(controller) {
return sources[0]
.then(r => r.read())
.then(result => {
if (result.done) {
sources.shift();
if (sources.length === 0) return controller.close();
return this.pull(controller);
} else {
controller.enqueue(result.value);
}
});
},
});
Но понимание всех последствий этого кода может быть доступно не каждому. Вместо того, чтобы анализировать эту логику, давайте поговорим о моем подходе к потоковой передаче Service Worker.
Я использую совершенно новую высокоуровневую оболочку workbox-streams
. С его помощью я могу передать его в виде смеси потоковых источников, как из кешей, так и из данных времени выполнения, которые могут поступать из сети. Workbox заботится о координации отдельных источников и объединении их в единый потоковый ответ.
Кроме того, Workbox автоматически определяет, поддерживается ли Streams API, а если нет, создает эквивалентный непотоковый ответ. Это означает, что вам не нужно беспокоиться о написании резервных вариантов, поскольку потоки постепенно приближаются к 100% поддержке браузера.
Кэширование во время выполнения
Давайте проверим, как мой сервис-воркер обращается с данными времени выполнения из API Stack Exchange. Я использую встроенную поддержку Workbox для стратегии кэширования с устаревшей проверкой , а также срок действия, чтобы гарантировать, что хранилище веб-приложения не будет неограниченно расти.
Я настроил в Workbox две стратегии для обработки различных источников, которые будут составлять ответ потоковой передачи. С помощью нескольких вызовов функций и настройки Workbox позволяет нам делать то, что в противном случае потребовало бы сотен строк рукописного кода.
const cacheStrategy = workbox.strategies.cacheFirst({
cacheName: workbox.core.cacheNames.precache,
});
const apiStrategy = workbox.strategies.staleWhileRevalidate({
cacheName: API_CACHE_NAME,
plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});
Первая стратегия считывает данные, которые были предварительно кэшированы, например, наши частичные HTML-шаблоны.
Другая стратегия реализует логику кэширования устаревших данных при повторной проверке, а также истечение срока действия кэша, который использовался реже всего, когда мы достигаем 50 записей.
Теперь, когда у меня есть эти стратегии, остается только указать Workbox , как их использовать для создания полного потокового ответа. Я передаю массив источников как функции, и каждая из этих функций будет выполнена немедленно. Workbox получает результат из каждого источника и последовательно передает его в веб-приложение, задерживаясь только в том случае, если следующая функция в массиве еще не завершена.
workbox.streams.strategy([
() => cacheStrategy.makeRequest({request: '/head.html'}),
() => cacheStrategy.makeRequest({request: '/navbar.html'}),
async ({event, url}) => {
const tag = url.searchParams.get('tag') || DEFAULT_TAG;
const listResponse = await apiStrategy.makeRequest(...);
const data = await listResponse.json();
return templates.index(tag, data.items);
},
() => cacheStrategy.makeRequest({request: '/foot.html'}),
]);
Первые два источника — это предварительно кэшированные частичные шаблоны, считываемые непосредственно из API Cache Storage, поэтому они всегда будут доступны немедленно. Это гарантирует, что наша реализация сервис-воркера будет надежно и быстро реагировать на запросы, как и мой серверный код.
Наша следующая исходная функция извлекает данные из API Stack Exchange и обрабатывает ответ в HTML-код, который ожидает веб-приложение.
Стратегия «устаревший во время повторной проверки» означает, что если у меня есть ранее кэшированный ответ на этот вызов API, я смогу немедленно передать его на страницу, одновременно обновляя запись кэша «в фоновом режиме» для следующего запроса. .
Наконец, я передаю кэшированную копию нижнего колонтитула и закрываю окончательные HTML-теги, чтобы завершить ответ.
Совместное использование кода обеспечивает синхронизацию
Вы заметите, что некоторые фрагменты кода сервисного работника выглядят знакомо. Частичная логика HTML и шаблонов, используемая моим сервис-воркером, идентична тому, что использует мой серверный обработчик. Такое совместное использование кода гарантирует, что пользователи получают единообразный опыт независимо от того, посещают ли они мое веб-приложение в первый раз или возвращаются на страницу, созданную сервисным работником. В этом прелесть изоморфного JavaScript.
Динамичные, прогрессивные улучшения
Я рассмотрел как сервер, так и сервис-воркера для своего PWA, но остался еще один логический момент, который нужно рассмотреть: на каждой из моих страниц выполняется небольшой объем JavaScript после того, как они полностью переданы в потоковом режиме.
Этот код постепенно улучшает взаимодействие с пользователем, но это не критично — веб-приложение будет работать, даже если его не запустить.
Метаданные страницы
Мое приложение использует JavaScipt на стороне клиента для обновления метаданных страницы на основе ответа API. Поскольку я использую один и тот же начальный бит кэшированного HTML для каждой страницы, веб-приложение в конечном итоге получает общие теги в заголовке моего документа. Но благодаря координации между моим шаблоном и кодом на стороне клиента я могу обновить заголовок окна, используя метаданные, специфичные для страницы.
Мой подход заключается в включении в код шаблона тега сценария, содержащего правильно экранированную строку.
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
Затем, как только моя страница загрузится , я читаю эту строку и обновляю заголовок документа.
if (self._title) {
document.title = unescape(self._title);
}
Если есть другие фрагменты метаданных, специфичных для страницы, которые вы хотите обновить в своем веб-приложении, вы можете использовать тот же подход.
Оффлайн UX
Другое прогрессивное улучшение, которое я добавил, предназначено для привлечения внимания к нашим оффлайн-возможностям. Я создал надежный PWA и хочу, чтобы пользователи знали, что даже в автономном режиме они все равно могут загружать ранее посещенные страницы.
Сначала я использую Cache Storage API, чтобы получить список всех ранее кэшированных запросов API, и преобразую его в список URL-адресов.
Помните те специальные атрибуты данных , о которых я говорил , каждый из которых содержит URL-адрес запроса API, необходимого для отображения вопроса? Я могу сопоставить эти атрибуты данных со списком кэшированных URL-адресов и создать массив всех несовпадающих ссылок на вопросы.
Когда браузер переходит в автономное состояние, я просматриваю список некэшированных ссылок и затемняю те, которые не работают. Имейте в виду, что это всего лишь визуальная подсказка пользователю о том, чего ему следует ожидать от этих страниц — на самом деле я не отключаю ссылки и не запрещаю пользователю навигацию.
const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);
const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
return !cachedUrls.includes(card.dataset.cacheUrl);
});
const offlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '0.3';
}
};
const onlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '1.0';
}
};
window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);
Распространенные ловушки
Сейчас я рассмотрел свой подход к созданию многостраничного PWA. Существует множество факторов, которые вам придется учитывать при разработке собственного подхода, и в конечном итоге вы можете сделать другой выбор, чем я. Эта гибкость — одна из замечательных особенностей создания веб-приложений.
Есть несколько распространенных ошибок, с которыми вы можете столкнуться при принятии собственных архитектурных решений, и я хочу избавить вас от некоторых проблем.
Не кэшировать полный HTML-код
Я не рекомендую хранить полные HTML-документы в кеше. Во-первых, это пустая трата места. Если ваше веб-приложение использует одну и ту же базовую структуру HTML для каждой из своих страниц, вам придется снова и снова хранить копии одной и той же разметки.
Что еще более важно, если вы внесете изменение в общую HTML-структуру вашего сайта, каждая из ранее кэшированных страниц все равно останется с вашим старым макетом. Представьте себе разочарование возвращающегося посетителя, увидевшего смесь старых и новых страниц.
Дрейф сервера/сервисного работника
Другая ошибка, которую следует избегать, связана с рассинхронизацией вашего сервера и сервисного работника. Мой подход заключался в использовании изоморфного JavaScript, чтобы в обоих местах запускался один и тот же код. В зависимости от существующей серверной архитектуры это не всегда возможно.
Какие бы архитектурные решения вы ни приняли, у вас должна быть определенная стратегия для запуска эквивалентного кода маршрутизации и шаблонов на вашем сервере и в сервис-воркере.
Худшие сценарии развития событий
Непоследовательная планировка/дизайн
Что произойдет, если вы проигнорируете эти ловушки? Что ж, возможны всевозможные сбои, но худший сценарий заключается в том, что возвращающийся пользователь посещает кэшированную страницу с очень устаревшим макетом — возможно, с устаревшим текстом заголовка или которая использует имена классов CSS, которые больше не действительны.
Худший сценарий: нарушенная маршрутизация.
Альтернативно, пользователь может встретить URL-адрес, который обрабатывается вашим сервером, но не вашим сервис-воркером. Сайт, полный зомби-макетов и тупиков, не является надежным PWA.
Советы для достижения успеха
Но ты не одинок в этом! Следующие советы помогут вам избежать подобных ошибок:
Используйте библиотеки шаблонов и маршрутизации, имеющие многоязычную реализацию.
Попробуйте использовать библиотеки шаблонов и маршрутизации, имеющие реализации JavaScript. Я знаю, что не каждый разработчик может позволить себе роскошь перейти с вашего текущего веб-сервера и языка шаблонов.
Но ряд популярных платформ шаблонов и маршрутизации имеют реализации на нескольких языках. Если вы сможете найти тот, который работает с JavaScript, а также с языком вашего текущего сервера, вы станете на шаг ближе к синхронизации вашего сервис-воркера и сервера.
Отдавайте предпочтение последовательным, а не вложенным шаблонам.
Далее я рекомендую использовать серию последовательных шаблонов, которые можно транслировать один за другим. Ничего страшного, если последующие части вашей страницы используют более сложную логику шаблонов, при условии, что вы можете передать начальную часть HTML как можно быстрее.
Кэшируйте как статический, так и динамический контент в своем сервис-воркере.
Для достижения наилучшей производительности вам следует предварительно кэшировать все важные статические ресурсы вашего сайта. Вам также следует настроить логику кэширования во время выполнения для обработки динамического контента, например запросов API. Использование Workbox означает, что вы можете основываться на хорошо протестированных, готовых к использованию стратегиях, а не реализовывать все это с нуля.
Блокируйте в сети только в случае крайней необходимости.
И в связи с этим вам следует блокировать сеть только в том случае, если невозможно передать ответ из кеша. Немедленное отображение кэшированного ответа API часто может улучшить взаимодействие с пользователем, чем ожидание свежих данных.