Подробное руководство по прогрессивным веб-приложениям
Опубликовано: 2022-03-10У моего папы был день рождения, и я хотела заказать ему шоколадный торт и рубашку. Я направился в Google, чтобы найти шоколадные пирожные, и нажал на первую ссылку в результатах поиска. На несколько секунд был пустой экран; Я не понимал, что происходит. После нескольких секунд терпеливого разглядывания экран моего мобильного телефона заполнился восхитительно выглядящими пирожными. Как только я нажал на один из них, чтобы проверить его детали, я получил уродливое толстое всплывающее окно с просьбой установить приложение для Android, чтобы я мог получить шелковистый гладкий опыт при заказе торта.
Это разочаровывало. Совесть не позволяла нажать на кнопку «Установить». Все, что я хотел сделать, это заказать небольшой торт и быть в пути.
Я щелкнул значок креста в самом правом углу всплывающего окна, чтобы выйти из него как можно скорее. Но затем всплывающее окно установки оказалось внизу экрана, занимая четверть места. А с ненадежным пользовательским интерфейсом прокрутка вниз была проблемой. Мне как-то удалось заказать голландский торт.
После этого ужасного опыта моей следующей задачей было заказать рубашку для папы. Как и раньше, я ищу рубашки в Google. Я нажал на первую ссылку, и в мгновение ока весь контент оказался прямо передо мной. Прокрутка была плавной. Нет установочного баннера. Мне казалось, что я просматриваю нативное приложение. Был момент, когда мое ужасное интернет-соединение оборвалось, но я все еще мог видеть контент вместо игры с динозаврами. Даже с моим дерьмовым интернетом мне удалось заказать рубашку и джинсы для папы. Самое удивительное, что мне стали приходить уведомления о моем заказе.
Я бы назвал это шелковистой гладкостью. Эти люди делали что-то правильно. Каждый веб-сайт должен делать это для своих пользователей. Это называется прогрессивным веб-приложением.
Как заявляет Алекс Рассел в одном из своих сообщений в блоге:
«В сети время от времени случается, что мощные технологии появляются без помощи отделов маркетинга или красивой упаковки. Они задерживаются и растут на периферии, становясь старомодными для крошечной группы, оставаясь почти невидимыми для всех остальных. Пока кто-нибудь не назовет их».
Шелковистый гладкий опыт работы в Интернете, иногда известный как прогрессивное веб-приложение
Прогрессивные веб-приложения (PWA) — это скорее методология, включающая комбинацию технологий для создания мощных веб-приложений. С улучшенным пользовательским интерфейсом люди будут проводить больше времени на веб-сайтах и видеть больше рекламы. Они, как правило, покупают больше, а с обновлениями уведомлений они чаще посещают сайт. The Financial Times отказалась от своих нативных приложений в 2011 году и создала веб-приложение, используя лучшие технологии, доступные на тот момент. Теперь продукт превратился в полноценный PWA.
Но зачем после всего этого времени создавать веб-приложение, если нативное приложение достаточно хорошо справляется с этой задачей?
Давайте рассмотрим некоторые показатели, используемые в Google IO 17.
Пять миллиардов устройств подключены к Интернету, что делает Интернет крупнейшей платформой в истории вычислений. В мобильном Интернете 11,4 миллиона уникальных посетителей ежемесячно заходят на 1000 лучших веб-ресурсов, а 4 миллиона — на тысячу лучших приложений. Мобильный Интернет собирает примерно в четыре раза больше пользователей, чем нативные приложения. Но это число резко падает, когда дело доходит до участия.
Пользователь проводит в среднем 188,6 минуты в нативных приложениях и всего 9,3 минуты в мобильном Интернете. Нативные приложения используют возможности операционных систем для отправки push-уведомлений, чтобы предоставить пользователям важные обновления. Они обеспечивают лучший пользовательский интерфейс и загружаются быстрее, чем веб-сайты в браузере. Вместо того, чтобы вводить URL-адрес в веб-браузере, пользователям просто нужно коснуться значка приложения на главном экране.
Большинство посетителей в Интернете вряд ли вернутся, поэтому разработчики придумали обходной путь, показывая им баннеры для установки нативных приложений, пытаясь удержать их вовлечённость. Но тогда пользователям придется пройти через утомительную процедуру установки бинарника нативного приложения. Принуждение пользователей к установке приложения раздражает и еще больше снижает вероятность того, что они вообще его установят. Возможности Интернета очевидны.
Рекомендуемое чтение : Native And PWA: выбор, а не претенденты!
Если веб-приложения поставляются с богатым пользовательским интерфейсом, push-уведомлениями, автономной поддержкой и мгновенной загрузкой, они могут покорить мир. Это то, что делает прогрессивное веб-приложение.
PWA обеспечивает богатый пользовательский интерфейс, потому что у него есть несколько сильных сторон:
Быстро
Пользовательский интерфейс не глючит. Прокрутка плавная. И приложение быстро реагирует на действия пользователя.Надежный
Обычный веб-сайт заставляет пользователей ждать, ничего не делая, в то время как он занят поездками на сервер. Тем временем PWA мгновенно загружает данные из кеша. PWA работает без проблем даже при подключении 2G. Каждый сетевой запрос на получение актива или фрагмента данных проходит через сервис-воркера (подробнее об этом позже), который сначала проверяет, находится ли уже ответ на конкретный запрос в кеше. Когда пользователи получают реальный контент почти мгновенно, даже при плохом соединении, они больше доверяют приложению и считают его более надежным.Вовлечение
PWA может заработать место на главном экране пользователя. Он предлагает нативное приложение, предоставляя полноэкранную рабочую область. Он использует push-уведомления, чтобы поддерживать активность пользователей.
Теперь, когда мы знаем, что PWA приносят на стол, давайте подробно рассмотрим, что дает PWA преимущество перед родными приложениями. PWA создаются с использованием таких технологий, как сервис-воркеры, манифесты веб-приложений, push-уведомления и индексированная база данных/локальная структура данных для кэширования. Давайте рассмотрим каждый подробно.
Сервисные работники
Сервисный работник — это файл JavaScript, который работает в фоновом режиме, не мешая взаимодействию пользователя. Все GET-запросы к серверу проходят через сервис-воркера. Он действует как клиентский прокси. Перехватывая сетевые запросы, он получает полный контроль над ответом, отправляемым обратно клиенту. PWA загружается мгновенно, потому что сервисные работники устраняют зависимость от сети, отвечая данными из кеша.
Service Worker может перехватывать только те сетевые запросы, которые входят в его область действия. Например, сервисный работник корневого уровня может перехватывать все запросы на выборку, поступающие с веб-страницы. Service Worker работает как система, управляемая событиями. Когда он не нужен, он переходит в спящее состояние, тем самым сохраняя память. Чтобы использовать сервис-воркер в веб-приложении, мы сначала должны зарегистрировать его на странице с помощью JavaScript.
(function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()
Сначала мы проверяем, поддерживает ли браузер сервис-воркеров. Чтобы зарегистрировать сервис-воркера в веб-приложении, мы предоставляем его URL-адрес в качестве параметра функции register
, доступной в navigator.serviceWorker
( navigator
— это веб-API, который позволяет скриптам регистрироваться и выполнять свои действия). Service worker регистрируется только один раз. Регистрация не происходит при каждой загрузке страницы. Браузер загружает файл сервис-воркера ( ./service-worker.js
) только в том случае, если существует разница в байтах между существующим активированным сервис-воркером и новым или если его URL-адрес изменился.
Вышеупомянутый сервис-воркер будет перехватывать все запросы, поступающие от корня ( /
). Чтобы ограничить область действия сервис-воркера, мы передаем необязательный параметр с одним из ключей в качестве области действия.
if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }
Приведенный выше сервисный работник будет перехватывать запросы, содержащие /books
в URL-адресе. Например, он не будет перехватывать запросы с /products
, но вполне может перехватывать запросы с /books/products
.
Как уже упоминалось, сервис-воркер работает как система, управляемая событиями. Он прослушивает события (установка, активация, выборка, отправка) и, соответственно, вызывает соответствующий обработчик событий. Некоторые из этих событий являются частью жизненного цикла сервисного работника, который проходит через эти события последовательно, чтобы активироваться.
Установка
После успешной регистрации сервис-воркера запускается событие установки. Это хорошее место для выполнения работы по инициализации, например, для настройки кеша или создания хранилищ объектов в IndexedDB. (IndexedDB станет для вас более понятной, когда мы углубимся в ее детали. Сейчас мы можем просто сказать, что это структура пары ключ-значение.)
self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })
Здесь мы кэшируем некоторые файлы, чтобы следующая загрузка была мгновенной. self
ссылается на экземпляр сервисного работника. event.waitUntil
заставляет сервис-воркер ждать, пока весь код внутри него не завершит выполнение.
Активация
После того, как сервис-воркер был установлен, он еще не может прослушивать запросы на выборку. Вместо этого запускается событие activate
. Если на веб-сайте в той же области не работает ни один активный сервис-воркер, установленный сервис-воркер активируется немедленно. Однако если на веб-сайте уже есть активный сервис-воркер, то активация нового сервис-воркера откладывается до тех пор, пока не будут закрыты все вкладки, работающие на старом сервис-воркере. Это имеет смысл, потому что старый сервис-воркер может использовать экземпляр кеша, который теперь изменен в более новом. Таким образом, шаг активации — хорошее место для избавления от старых кешей.
self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })
В приведенном выше коде мы удаляем старый кеш. Если имя кеша не совпадает с cacheWhitelist
, то он удаляется. Чтобы пропустить фазу ожидания и немедленно активировать сервис-воркера, мы используем skip.waiting()
.
self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })
Как только сервис-воркер активирован, он может прослушивать запросы на выборку и push-события.
Обработчик событий выборки
Всякий раз, когда веб-страница запускает запрос на выборку ресурса по сети, вызывается событие выборки от работника службы. Обработчик события выборки сначала ищет запрошенный ресурс в кэше. Если он присутствует в кеше, то возвращает ответ с кешированным ресурсом. В противном случае он инициирует запрос на выборку на сервер, и когда сервер отправляет ответ с запрошенным ресурсом, он помещает его в кеш для последующих запросов.
/* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })
event.respondWith
позволяет работнику службы отправить настроенный ответ клиенту.
Offline-first теперь вещь. Для любого некритического запроса мы должны обслуживать ответ из кеша, а не ездить на сервер. Если какой-либо актив отсутствует в кеше, мы получаем его с сервера и затем кешируем для последующих запросов.
Сервисные работники работают только с веб-сайтами HTTPS, потому что они могут манипулировать ответом на любой запрос на выборку. Кто-то со злым умыслом может подделать ответ на запрос на веб-сайте HTTP. Таким образом, размещение PWA на HTTPS является обязательным. Сервисные работники не прерывают нормальное функционирование DOM. Они не могут взаимодействовать напрямую с веб-страницей. Чтобы отправить любое сообщение на веб-страницу, он использует почтовые сообщения.
Веб-push-уведомления
Предположим, вы заняты игрой на своем мобильном телефоне, и появляется уведомление о скидке 30% на ваш любимый бренд. Без дальнейших церемоний вы нажимаете на уведомление и делаете покупки на выдохе. Получение обновлений в режиме реального времени, скажем, о крикетном или футбольном матче или получение важных электронных писем и напоминаний в виде уведомлений — это большое дело, когда речь идет о привлечении пользователей к продукту. Эта функция была доступна только в нативных приложениях, пока не появился PWA. PWA использует веб-push-уведомления, чтобы конкурировать с этой мощной функцией, которую нативные приложения предоставляют из коробки. Пользователь по-прежнему будет получать веб-push-уведомления, даже если PWA не открыто ни на одной из вкладок браузера и даже если браузер не открыт.
Веб-приложение должно запрашивать у пользователя разрешение на отправку ему push-уведомлений.
Как только пользователь подтвердит, нажав кнопку «Разрешить», браузер сгенерирует уникальный токен подписки. Этот токен уникален для данного устройства. Формат токена подписки, созданный Chrome, выглядит следующим образом:
{ "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }
endpoint
, содержащаяся в указанном выше токене, будет уникальной для каждой подписки. На среднем веб-сайте тысячи пользователей согласятся получать push-уведомления, и для каждого из них эта endpoint
будет уникальной. Итак, с помощью этой endpoint
приложение может ориентироваться на этих пользователей в будущем, отправляя им push-уведомления. expirationTime
— это время, в течение которого подписка действительна для конкретного устройства. Если expirationTime
составляет 20 дней, это означает, что срок действия push-подписки пользователя истечет через 20 дней, и пользователь не сможет получать push-уведомления по старой подписке. В этом случае браузер создаст новый токен подписки для этого устройства. Ключи auth
и p256dh
используются для шифрования.
Теперь, чтобы отправлять push-уведомления этим тысячам пользователей в будущем, нам сначала нужно сохранить их соответствующие токены подписки. Отправка push-уведомлений этим пользователям — задача сервера приложений (внутреннего сервера, возможно, скрипта Node.js). Это может звучать так же просто, как запрос POST
к URL-адресу конечной точки с данными уведомления в полезной нагрузке запроса. Однако следует отметить, что если пользователь не находится в сети, когда сервер запускает предназначенное для него push-уведомление, он все равно должен получить это уведомление, как только вернется в сеть. Сервер должен был бы позаботиться о таких сценариях, наряду с отправкой тысяч запросов пользователям. Сервер, отслеживающий соединение пользователя, звучит сложно. Таким образом, что-то посередине будет отвечать за маршрутизацию веб-push-уведомлений с сервера на клиент. Это называется push-сервисом, и каждый браузер имеет собственную реализацию push-сервиса. Браузер должен сообщить следующую информацию службе push, чтобы отправить какое-либо уведомление:
- Время жить
Это то, как долго сообщение должно стоять в очереди на случай, если оно не будет доставлено пользователю. По истечении этого времени сообщение будет удалено из очереди. - срочность сообщения
Это сделано для того, чтобы служба push сохраняла заряд батареи пользователя, отправляя только сообщения с высоким приоритетом.
Служба push направляет сообщения клиенту. Поскольку клиент должен получать push-уведомления, даже если соответствующее веб-приложение не открыто в браузере, события push-уведомлений должны прослушиваться чем-то, что постоянно отслеживает их в фоновом режиме. Вы уже догадались: это работа сервисного работника. Service Worker прослушивает push-события и выполняет работу по отображению уведомлений пользователю.
Итак, теперь мы знаем, что браузер, push-сервис, сервис-воркер и сервер приложений работают согласованно, чтобы отправлять push-уведомления пользователю. Давайте рассмотрим детали реализации.
Веб-клиент push-уведомлений
Спросить разрешение у пользователя — это одноразовая вещь. Если пользователь уже предоставил разрешение на получение push-уведомлений, мы не должны спрашивать снова. Значение разрешения сохраняется в Notification.permission
.
/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
В приведенном выше методе subscribe
мы userVisibleOnly
и applicationServerKey
для создания токена подписки. Свойство userVisibleOnly
всегда должно иметь значение true, поскольку оно сообщает браузеру, что любое push-уведомление, отправленное сервером, будет показано клиенту. Чтобы понять назначение applicationServerKey
, давайте рассмотрим сценарий.
Если кто-то завладеет вашими тысячами токенов подписки, он вполне может отправить уведомления на конечные точки, содержащиеся в этих подписках. Конечная точка не может быть связана с вашей уникальной личностью. Чтобы обеспечить уникальную идентификацию токенов подписки, сгенерированных в вашем веб-приложении, мы используем протокол VAPID. При использовании VAPID сервер приложений добровольно идентифицирует себя в службе push-уведомлений при отправке push-уведомлений. Мы генерируем два ключа следующим образом:
const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()
web-push — это модуль npm. vapidKeys
будет иметь один открытый ключ и один закрытый ключ. Используемый выше ключ сервера приложений является открытым ключом.
Веб-сервер push-уведомлений
Работа веб-сервера push-уведомлений (сервера приложений) проста. Он отправляет полезную нагрузку уведомления на токены подписки.
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
Он использует метод sendNotification
из библиотеки веб-push.
Сервисные работники
Service Worker показывает уведомление пользователю как таковое:
self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })
До сих пор мы видели, как сервис-воркер использует кеш для хранения запросов и делает PWA быстрым и надежным, и мы видели, как веб-push-уведомления поддерживают интерес пользователей.
Чтобы хранить кучу данных на стороне клиента для автономной поддержки, нам нужна гигантская структура данных. Давайте посмотрим на PWA Financial Times. Вы должны лично убедиться в силе этой структуры данных. Загрузите URL-адрес в свой браузер, а затем отключите подключение к Интернету. Перезагрузите страницу. Гах! Он все еще работает? Это. (Как я уже сказал, автономный режим — это новый черный цвет.) Данные не поступают по проводам. Подается из дома. Перейдите на вкладку «Приложения» инструментов разработчика Chrome. В разделе «Хранилище» вы найдете «IndexedDB».
Загляните в магазин объектов «Статьи» и разверните любой из элементов, чтобы увидеть магию своими глазами. The Financial Times сохранила эти данные для поддержки в автономном режиме. Эта структура данных, которая позволяет нам хранить огромное количество данных, называется IndexedDB. IndexedDB — это объектно-ориентированная база данных на основе JavaScript для хранения структурированных данных. Мы можем создавать различные хранилища объектов в этой базе данных для различных целей. Например, как мы видим на изображении выше, «Ресурсы», «СтатьиИзображения» и «Статьи» называются хранилищами объектов. Каждая запись в хранилище объектов однозначно идентифицируется ключом. IndexedDB можно использовать даже для хранения файлов и больших двоичных объектов.
Попробуем разобраться с IndexedDB, создав базу данных для хранения книг.
let openIdbRequest = window.indexedDB.open('booksdb', 1)
Если база данных booksdb
еще не существует, приведенный выше код создаст базу данных booksdb
. Второй параметр метода open — это версия базы данных. Указание версии позаботится об изменениях, связанных со схемой, которые могут произойти в будущем. Например, booksdb
сейчас имеет только одну таблицу, но когда приложение разрастется, мы намерены добавить в него еще две таблицы. Чтобы убедиться, что наша база данных синхронизирована с обновленной схемой, мы укажем более позднюю версию, чем предыдущая.
Вызов метода open
не открывает базу данных сразу. Это асинхронный запрос, который возвращает объект IDBOpenDBRequest
. Этот объект имеет свойства успеха и ошибки; нам придется написать соответствующие обработчики для этих свойств, чтобы управлять состоянием нашего соединения.
let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }
Для управления созданием или модификацией хранилищ объектов (хранилища объектов аналогичны таблицам на основе SQL — они имеют структуру ключ-значение) для onupgradeneeded
метод openIdbRequest
. Метод onupgradeneeded
будет вызываться при каждом изменении версии. В приведенном выше фрагменте кода мы создаем хранилище объектов книг с уникальным ключом в качестве идентификатора.
Предположим, что после развертывания этого фрагмента кода нам нужно создать еще одно хранилище объектов, называемое users
. Итак, теперь версия нашей базы данных будет 2
.
let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }
Мы кэшировали dbInstance
в обработчике события успеха запроса на открытие. Чтобы получить или добавить данные в IndexedDB, мы будем использовать dbInstance
. Давайте добавим несколько записей книг в наше хранилище объектов книг.
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }
Мы используем transactions
, особенно при записи записей в хранилище объектов. Транзакция — это просто оболочка операции для обеспечения целостности данных. Если какое-либо из действий в транзакции терпит неудачу, то в базе данных не выполняется никаких действий.
Давайте изменим запись книги с помощью метода put
:
let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }
Давайте получим запись книги с помощью метода get
:
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }
Добавление значка на главный экран
Теперь, когда различий между PWA и нативным приложением практически нет, имеет смысл предложить PWA приоритетное положение. Если ваш веб-сайт соответствует основным критериям PWA (размещен на HTTPS, интегрируется с сервис-воркерами и имеет manifest.json
) и после того, как пользователь провел некоторое время на веб-странице, браузер вызовет подсказку внизу, спрашивая пользователю добавить приложение на главный экран, как показано ниже:
Когда пользователь нажимает «Добавить FT на главный экран», PWA становится на главный экран, а также в панель приложений. Когда пользователь ищет какое-либо приложение на своем телефоне, будут перечислены все PWA, соответствующие поисковому запросу. Они также будут видны в настройках системы, что упрощает пользователям управление ими. В этом смысле PWA ведет себя как родное приложение.
PWA используют manifest.json
для предоставления этой функции. Давайте рассмотрим простой файл manifest.json
.
{ "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }
short_name
отображается на главном экране пользователя и в настройках системы. name
отображается в приглашении Chrome и на заставке. Экран-заставка — это то, что видит пользователь, когда приложение готовится к запуску. start_url
— это главный экран вашего приложения. Это то, что пользователи получают, когда нажимают значок на главном экране. background_color
используется на заставке. theme_color
устанавливает цвет панели инструментов. standalone
значение для режима display
говорит, что приложение должно работать в полноэкранном режиме (скрывая панель инструментов браузера). Когда пользователь устанавливает PWA, его размер измеряется всего в килобайтах, а не в мегабайтах собственных приложений.
Сервисные работники, push-уведомления из Интернета, IndexedDB и положение главного экрана компенсируют автономную поддержку, надежность и вовлеченность. Следует отметить, что сервис-воркер не оживает и не начинает выполнять свою работу при первой же загрузке. Первая загрузка все еще будет медленной, пока все статические активы и другие ресурсы не будут кэшированы. Мы можем реализовать некоторые стратегии для оптимизации первой загрузки.
Объединение активов
Все ресурсы, включая HTML, таблицы стилей, изображения и JavaScript, должны быть получены с сервера. Чем больше файлов, тем больше HTTPS-запросов требуется для их извлечения. Мы можем использовать упаковщики, такие как WebPack, для объединения наших статических ресурсов, тем самым уменьшая количество HTTP-запросов к серверу. WebPack отлично справляется с дальнейшей оптимизацией пакета, используя такие методы, как разделение кода (т. е. объединение только тех файлов, которые необходимы для текущей загрузки страницы, вместо того, чтобы объединять их все вместе) и встряхивание дерева (т. е. удаление повторяющихся зависимостей или зависимости, которые импортируются, но не используются в коде).
Сокращение круговых поездок
Одной из основных причин медлительности в Интернете является задержка в сети. Время, необходимое байту для перемещения от A к B, зависит от сетевого подключения. Например, конкретная поездка туда и обратно по Wi-Fi занимает 50 миллисекунд и 500 миллисекунд для соединения 3G, но 2500 миллисекунд для соединения 2G. Эти запросы отправляются с использованием протокола HTTP, что означает, что пока конкретное соединение используется для запроса, оно не может использоваться для каких-либо других запросов, пока не будет получен ответ на предыдущий запрос. Веб-сайт может одновременно выполнять шесть асинхронных HTTP-запросов, поскольку для выполнения HTTP-запросов к веб-сайту доступно шесть подключений. Средний веб-сайт делает примерно 100 запросов; таким образом, имея максимум шесть доступных подключений, пользователь может потратить около 833 миллисекунд на одно путешествие туда и обратно. (Вычисление: 833 миллисекунды - 100 ⁄ 6 = 1666. Мы должны разделить 1666 на 2, потому что мы вычисляем время, затрачиваемое на поездку туда и обратно.) При наличии HTTP2 время обработки значительно сокращается. HTTP2 не блокирует головку соединения, поэтому можно отправлять несколько запросов одновременно.
Большинство ответов HTTP содержат заголовки last-modified
и etag
. Заголовок last-modified
— это дата последнего изменения файла, а etag
— это уникальное значение, основанное на содержимом файла. Он будет изменен только при изменении содержимого файла. Оба этих заголовка можно использовать, чтобы избежать повторной загрузки файла, если кэшированная версия уже доступна локально. Если браузер имеет локально доступную версию этого файла, он может добавить любой из этих двух заголовков в запрос как таковой:
Сервер может проверить, изменилось ли содержимое файла. If the contents of the file have not changed, then it responds with a status code of 304 ( not modified ).
This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.
Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script>
, it will make a synchronous HTTP request to the server to fetch asset.js
, and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async
keyword in script references, like <script src='asset.js' async></script>
. With the introduction of the async
keyword here, the browser will make an asynchronous request to fetch asset.js
without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer
keyword, like <script src='asset.js' defer></script>
.
Заключение
We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:
- Service workers make good use of the cache to speed up the loading of assets.
- Web push notifications work under the hood.
- We use IndexedDB to store a massive amount of data.
- Some of the optimizations for instant first load, like using HTTP2 and adding headers like
Etag
,last-modified
andIf-None-Match
, prevent the downloading of valid cached assets.
That's all, folks!