Создаем для современных браузеров и постоянно совершенствуем, как будто на дворе 2003 год
Еще в марте 2003 года Ник Финк и Стив Чампеон ошеломили мир веб-дизайна концепцией прогрессивного улучшения — стратегии веб-дизайна, которая делает упор на загрузку основного содержимого веб-страницы в первую очередь, а затем постепенно добавляет более тонкие и технически строгие слои представления и функций поверх содержимого. В то время как в 2003 году прогрессивное улучшение было связано с использованием — на тот момент — современных функций CSS, ненавязчивого JavaScript и даже просто масштабируемой векторной графики. Прогрессивное улучшение в 2020 году и далее — это использование современных возможностей браузера .

Современный JavaScript
Говоря о JavaScript, ситуация с поддержкой браузерами новейших основных функций JavaScript ES 2015 великолепна. Новый стандарт включает обещания, модули, классы, шаблонные литералы, стрелочные функции, let
и const
, параметры по умолчанию, генераторы, деструктурирующее назначение, rest и spread, Map
/ Set
, WeakMap
/ WeakSet
и многое другое. Все поддерживается .

Асинхронные функции, функция ES 2017 и одна из моих личных любимых, могут использоваться во всех основных браузерах. Ключевые слова async
и await
позволяют писать асинхронное поведение на основе обещаний в более чистом стиле, избегая необходимости явно настраивать цепочки обещаний.

И даже супер недавние дополнения языка ES 2020, такие как необязательное связывание и нулевая коалесценция, получили поддержку очень быстро. Вы можете увидеть пример кода ниже. Когда дело доходит до основных функций JavaScript, трава не может быть намного зеленее, чем сегодня.
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah',
},
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0

Пример приложения: Fugu Greetings
Для этой статьи я работаю с простым PWA, называемым Fugu Greetings ( GitHub ). Название этого приложения — дань уважения проекту Fugu 🐡, попытке дать вебу все возможности приложений Android/iOS/desktop. Вы можете прочитать больше о проекте на его целевой странице .
Fugu Greetings — это приложение для рисования, которое позволяет вам создавать виртуальные поздравительные открытки и отправлять их вашим близким. Оно иллюстрирует основные концепции PWA . Оно надежно и полностью автономно, поэтому даже если у вас нет сети, вы все равно можете им пользоваться. Его также можно установить на домашний экран устройства, и оно легко интегрируется с операционной системой как отдельное приложение.

Прогрессивное улучшение
С этим разобрались, пора поговорить о прогрессивном улучшении . Глоссарий MDN Web Docs определяет эту концепцию следующим образом:
Прогрессивное улучшение — это философия дизайна, которая обеспечивает базовый уровень необходимого контента и функциональности как можно большему числу пользователей, при этом обеспечивая наилучшие возможности только для пользователей самых современных браузеров, которые могут выполнять весь необходимый код.
Обнаружение функций обычно используется для определения того, могут ли браузеры обрабатывать более современные функции, в то время как полизаполнения часто используются для добавления недостающих функций с помощью JavaScript.
[…]
Прогрессивное улучшение — это полезный метод, который позволяет веб-разработчикам сосредоточиться на разработке наилучших возможных веб-сайтов, одновременно заставляя эти веб-сайты работать на нескольких неизвестных пользовательских агентах. Грациозная деградация связана, но это не одно и то же, и часто рассматривается как идущая в противоположном направлении по отношению к прогрессивному улучшению. В действительности оба подхода являются допустимыми и часто могут дополнять друг друга.
Участники MDN
Начинать каждую поздравительную открытку с нуля может быть действительно обременительно. Так почему бы не иметь функцию, которая позволяет пользователям импортировать изображение и начинать с него? При традиционном подходе вы бы использовали элемент <input type=file>
чтобы это произошло. Сначала вы бы создали элемент, установили его type
на 'file'
и добавили бы типы MIME в свойство accept
, а затем программно "щелкнули" его и слушали изменения. Когда вы выбираете изображение, оно импортируется прямо на холст.
const importImage = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
Если есть функция импорта , вероятно, должна быть и функция экспорта , чтобы пользователи могли сохранять свои поздравительные открытки локально. Традиционный способ сохранения файлов — создание ссылки-якоря с атрибутом download
и URL-адресом blob в качестве href
. Вы также программно «щелкаете» по ней, чтобы запустить загрузку, и, чтобы предотвратить утечки памяти, не забываете отозвать URL-адрес объекта blob.
const exportImage = async (blob) => {
const a = document.createElement('a');
a.download = 'fugu-greeting.png';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
Но подождите минутку. Мысленно вы не «скачали» поздравительную открытку, вы ее «сохранили». Вместо того, чтобы показать вам диалоговое окно «сохранить», позволяющее выбрать, куда поместить файл, браузер напрямую скачал поздравительную открытку без взаимодействия с пользователем и поместил ее прямо в папку «Загрузки». Это не очень здорово.
Что если бы был лучший способ? Что если бы вы могли просто открыть локальный файл, отредактировать его, а затем сохранить изменения либо в новый файл, либо обратно в исходный файл, который вы изначально открыли? Оказывается, есть. API доступа к файловой системе позволяет открывать и создавать файлы и каталоги, а также изменять и сохранять их.
Итак, как мне обнаружить API? API доступа к файловой системе предоставляет новый метод window.chooseFileSystemEntries()
. Следовательно, мне нужно условно загружать различные модули импорта и экспорта в зависимости от того, доступен ли этот метод. Я показал, как это сделать ниже.
const loadImportAndExport = () => {
if ('chooseFileSystemEntries' in window) {
Promise.all([
import('./import_image.mjs'),
import('./export_image.mjs'),
]);
} else {
Promise.all([
import('./import_image_legacy.mjs'),
import('./export_image_legacy.mjs'),
]);
}
};
Но прежде чем я углублюсь в детали File System Access API, позвольте мне быстро осветить здесь прогрессивный шаблон улучшения. В браузерах, которые в настоящее время не поддерживают File System Access API, я загружаю устаревшие скрипты. Ниже вы можете увидеть сетевые вкладки Firefox и Safari.


Однако в Chrome, браузере, поддерживающем API, загружаются только новые скрипты. Это стало элегантно возможным благодаря динамическому import()
, который поддерживают все современные браузеры. Как я уже говорил ранее, трава в наши дни довольно зеленая.

API доступа к файловой системе
Итак, теперь, когда я это рассмотрел, пришло время рассмотреть фактическую реализацию на основе API доступа к файловой системе. Для импорта изображения я вызываю window.chooseFileSystemEntries()
и передаю ему свойство accepts
, где я говорю, что мне нужны файлы изображений. Поддерживаются как расширения файлов, так и типы MIME. Это приводит к дескриптору файла, из которого я могу получить фактический файл, вызвав getFile()
.
const importImage = async () => {
try {
const handle = await window.chooseFileSystemEntries({
accepts: [
{
description: 'Image files',
mimeTypes: ['image/*'],
extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
},
],
});
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
Экспорт изображения почти такой же, но на этот раз мне нужно передать параметр типа 'save-file'
в метод chooseFileSystemEntries()
. Из этого я получаю диалог сохранения файла. При открытом файле это было не нужно, так как 'open-file'
является значением по умолчанию. Я устанавливаю параметр accepts
аналогично предыдущему, но на этот раз ограничиваюсь только изображениями PNG. Я снова получаю обратно дескриптор файла, но вместо получения файла, на этот раз я создаю записываемый поток, вызывая createWritable()
. Затем я записываю blob, который является изображением моей поздравительной открытки, в файл. Наконец, я закрываю записываемый поток.
Все может выйти из строя: на диске может не хватить места, может возникнуть ошибка записи или чтения, или пользователь просто отменяет диалог файла. Вот почему я всегда оборачиваю вызовы в оператор try...catch
.
const exportImage = async (blob) => {
try {
const handle = await window.chooseFileSystemEntries({
type: 'save-file',
accepts: [
{
description: 'Image file',
extensions: ['png'],
mimeTypes: ['image/png'],
},
],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
} catch (err) {
console.error(err.name, err.message);
}
};
Используя прогрессивное улучшение с API File System Access, я могу открыть файл, как и раньше. Импортированный файл рисуется прямо на холсте. Я могу вносить изменения и, наконец, сохранять их с помощью настоящего диалогового окна сохранения, где я могу выбрать имя и место хранения файла. Теперь файл готов к вечному сохранению.



API Web Share и Web Share Target
Помимо хранения навечно, возможно, я действительно хочу поделиться своей поздравительной открыткой. Это то, что мне позволяют делать Web Share API и Web Share Target API . Мобильные, а в последнее время и настольные операционные системы получили встроенные механизмы обмена. Например, ниже приведена таблица обмена Safari для настольных компьютеров на macOS, запущенная из статьи в моем блоге . Когда вы нажимаете кнопку «Поделиться статьей» , вы можете поделиться ссылкой на статью с другом, например, через приложение macOS Messages.

Код, который это делает, довольно прост. Я вызываю navigator.share()
и передаю ему необязательные title
, text
и url
в объекте. Но что, если я хочу прикрепить изображение? Уровень 1 API Web Share пока не поддерживает это. Хорошей новостью является то, что в Web Share Level 2 добавлены возможности обмена файлами.
try {
await navigator.share({
title: 'Check out this article:',
text: `"${document.title}" by @tomayac:`,
url: document.querySelector('link[rel=canonical]').href,
});
} catch (err) {
console.warn(err.name, err.message);
}
Позвольте мне показать вам, как это работает с приложением Fugu Greeting card. Сначала мне нужно подготовить объект data
с массивом files
, состоящим из одного blob, а затем title
и text
. Далее, в качестве лучшей практики, я использую новый метод navigator.canShare()
, который делает то, что следует из его названия: он сообщает мне, может ли объект data
, которым я пытаюсь поделиться, технически быть общим для браузера. Если navigator.canShare()
сообщает мне, что данные могут быть общими, я готов вызвать navigator.share()
как и раньше. Поскольку все может потерпеть неудачу, я снова использую блок try...catch
.
const share = async (title, text, blob) => {
const data = {
files: [
new File([blob], 'fugu-greeting.png', {
type: blob.type,
}),
],
title: title,
text: text,
};
try {
if (!(navigator.canShare(data))) {
throw new Error("Can't share data.", data);
}
await navigator.share(data);
} catch (err) {
console.error(err.name, err.message);
}
};
Как и прежде, я использую прогрессивное улучшение. Если и 'share'
, и 'canShare'
существуют в объекте navigator
, только тогда я иду вперед и загружаю share.mjs
через динамический import()
. В браузерах, таких как мобильный Safari, которые удовлетворяют только одному из двух условий, я не загружаю функционал.
const loadShare = () => {
if ('share' in navigator && 'canShare' in navigator) {
import('./share.mjs');
}
};
В Fugu Greetings, если я нажимаю кнопку «Поделиться» в поддерживающем браузере, например Chrome на Android, открывается встроенный лист «Поделиться». Я могу, например, выбрать Gmail, и виджет составителя писем всплывает с прикрепленным изображением.


API выбора контактов
Далее я хочу поговорить о контактах, то есть об адресной книге устройства или приложении для управления контактами. Когда вы пишете поздравительную открытку, не всегда может быть легко правильно написать чье-то имя. Например, у меня есть друг Сергей, который предпочитает, чтобы его имя было написано кириллическими буквами. Я использую немецкую клавиатуру QWERTZ и понятия не имею, как набрать его имя. Эту проблему может решить API Contact Picker . Поскольку мой друг сохранен в приложении контактов моего телефона, через API Contacts Picker я могу подключаться к своим контактам из Интернета.
Сначала мне нужно указать список свойств, к которым я хочу получить доступ. В этом случае мне нужны только имена, но для других случаев использования мне могут быть интересны телефонные номера, адреса электронной почты, значки аватаров или физические адреса. Затем я настраиваю объект options
и устанавливаю multiple
в true
, чтобы я мог выбрать более одной записи. Наконец, я могу вызвать navigator.contacts.select()
, который возвращает желаемые свойства для выбранных пользователем контактов.
const getContacts = async () => {
const properties = ['name'];
const options = { multiple: true };
try {
return await navigator.contacts.select(properties, options);
} catch (err) {
console.error(err.name, err.message);
}
};
И к настоящему моменту вы, вероятно, уже усвоили эту схему: я загружаю файл только тогда, когда API действительно поддерживается.
if ('contacts' in navigator) {
import('./contacts.mjs');
}
В Fugu Greeting, когда я нажимаю кнопку Контакты и выбираю двух моих лучших друзей, Сергей Михайлович Брин и劳伦斯·爱德华·"拉里"·佩奇, вы можете видеть, что выбор контактов ограничен и показывает только их имена, но не адреса электронной почты или другую информацию, например, номера телефонов. Затем их имена рисуются на моей поздравительной открытке.


API асинхронного буфера обмена
Далее следует копирование и вставка. Одна из наших любимых операций как разработчиков программного обеспечения — копирование и вставка. Как автор поздравительных открыток, я иногда могу захотеть сделать то же самое. Я могу захотеть либо вставить изображение в поздравительную открытку, над которой я работаю, либо скопировать свою поздравительную открытку, чтобы продолжить ее редактирование из другого места. API Async Clipboard поддерживает как текст, так и изображения. Позвольте мне рассказать вам, как я добавил поддержку копирования и вставки в приложение Fugu Greetings.
Чтобы скопировать что-то в буфер обмена системы, мне нужно записать в него. Метод navigator.clipboard.write()
принимает массив элементов буфера обмена в качестве параметра. Каждый элемент буфера обмена по сути является объектом с blob в качестве значения и типом blob в качестве ключа.
const copy = async (blob) => {
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
} catch (err) {
console.error(err.name, err.message);
}
};
Чтобы вставить, мне нужно перебрать элементы буфера обмена, которые я получаю, вызывая navigator.clipboard.read()
. Причина этого в том, что в буфере обмена могут находиться несколько элементов буфера обмена в разных представлениях. У каждого элемента буфера обмена есть поле types
, которое сообщает мне типы MIME доступных ресурсов. Я вызываю метод getType()
элемента буфера обмена, передавая тип MIME, который я получил ранее.
const paste = async () => {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
try {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
return blob;
}
} catch (err) {
console.error(err.name, err.message);
}
}
} catch (err) {
console.error(err.name, err.message);
}
};
И это почти излишне говорить сейчас. Я делаю это только в поддерживающих браузерах.
if ('clipboard' in navigator && 'write' in navigator.clipboard) {
import('./clipboard.mjs');
}
Так как же это работает на практике? У меня есть изображение, открытое в приложении macOS Preview, и я копирую его в буфер обмена. Когда я нажимаю «Вставить» , приложение Fugu Greetings спрашивает меня, хочу ли я разрешить приложению видеть текст и изображения в буфере обмена.

Наконец, после принятия разрешения, изображение вставляется в приложение. Обратный способ тоже работает. Давайте скопируем поздравительную открытку в буфер обмена. Когда я затем открою Preview и нажму File , а затем New from Clipboard , поздравительная открытка будет вставлена в новое изображение без названия.

API бейджинга
Еще один полезный API — API Badging . Как устанавливаемый PWA, Fugu Greetings, конечно же, имеет значок приложения, который пользователи могут разместить на панели приложений или на главном экране. Забавный и простой способ продемонстрировать API — использовать его в Fugu Greetings в качестве счетчика штрихов пера. Я добавил прослушиватель событий, который увеличивает счетчик штрихов пера всякий раз, когда происходит событие pointerdown
, а затем устанавливает обновленный значок значка. Всякий раз, когда холст очищается, счетчик сбрасывается, а значок удаляется.
let strokes = 0;
canvas.addEventListener('pointerdown', () => {
navigator.setAppBadge(++strokes);
});
clearButton.addEventListener('click', () => {
strokes = 0;
navigator.setAppBadge(strokes);
});
Эта функция представляет собой прогрессивное улучшение, поэтому логика загрузки остается прежней.
if ('setAppBadge' in navigator) {
import('./badge.mjs');
}
В этом примере я нарисовал цифры от одного до семи, используя один росчерк пера на цифру. Счетчик значков на иконке теперь на семи.


API периодической фоновой синхронизации
Хотите начинать каждый день по-новому? Удобная функция приложения Fugu Greetings заключается в том, что оно может вдохновлять вас каждое утро новым фоновым изображением для начала вашей поздравительной открытки. Для этого приложение использует API периодической фоновой синхронизации .
Первый шаг — зарегистрировать периодическое событие синхронизации в регистрации service worker. Он прослушивает тег синхронизации, называемый 'image-of-the-day'
, и имеет минимальный интервал в один день, поэтому пользователь может получать новое фоновое изображение каждые 24 часа.
const registerPeriodicBackgroundSync = async () => {
const registration = await navigator.serviceWorker.ready;
try {
registration.periodicSync.register('image-of-the-day-sync', {
// An interval of one day.
minInterval: 24 * 60 * 60 * 1000,
});
} catch (err) {
console.error(err.name, err.message);
}
};
Вторым шагом является прослушивание события periodicsync
в service worker. Если тег события — 'image-of-the-day'
, то есть тот, который был зарегистрирован ранее, изображение дня извлекается с помощью функции getImageOfTheDay()
, а результат распространяется на всех клиентов, чтобы они могли обновить свои холсты и кэши.
self.addEventListener('periodicsync', (syncEvent) => {
if (syncEvent.tag === 'image-of-the-day-sync') {
syncEvent.waitUntil(
(async () => {
const blob = await getImageOfTheDay();
const clients = await self.clients.matchAll();
clients.forEach((client) => {
client.postMessage({
image: blob,
});
});
})()
);
}
});
Опять же, это действительно прогрессивное улучшение, поэтому код загружается только тогда, когда API поддерживается браузером. Это относится как к клиентскому коду, так и к коду service worker. В неподдерживающих браузерах ни один из них не загружается. Обратите внимание, как в service worker вместо динамического import()
(который пока не поддерживается в контексте service worker) я использую классический importScripts()
.
// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
importScripts('./image_of_the_day.mjs');
}
В Fugu Greetings нажатие кнопки «Обои» открывает изображение поздравительной открытки дня, которое обновляется каждый день с помощью API периодической фоновой синхронизации.

API триггеров уведомлений
Иногда даже при большом вдохновении вам нужен толчок, чтобы закончить начатую поздравительную открытку. Эта функция включена API Notification Triggers . Как пользователь, я могу ввести время, когда я хочу, чтобы меня подтолкнули закончить мою поздравительную открытку. Когда это время придет, я получу уведомление о том, что моя поздравительная открытка ждет.
После запроса целевого времени приложение планирует уведомление с помощью showTrigger
. Это может быть TimestampTrigger
с ранее выбранной целевой датой. Напоминание будет запущено локально, не требуется сетевой или серверной части.
const targetDate = promptTargetDate();
if (targetDate) {
const registration = await navigator.serviceWorker.ready;
registration.showNotification('Reminder', {
tag: 'reminder',
body: "It's time to finish your greeting card!",
showTrigger: new TimestampTrigger(targetDate),
});
}
Как и все остальное, что я показал до сих пор, это прогрессивное улучшение, поэтому код загружается только условно.
if ('Notification' in window && 'showTrigger' in Notification.prototype) {
import('./notification_triggers.mjs');
}
Когда я устанавливаю флажок «Напоминание» в Fugu Greetings, появляется запрос с вопросом, когда я хочу получить напоминание о необходимости закончить поздравительную открытку.

Когда в Fugu Greetings срабатывает запланированное уведомление, оно отображается так же, как и любое другое уведомление, но, как я уже писал ранее, для него не требуется подключение к сети.

API блокировки пробуждения
Я также хочу включить Wake Lock API . Иногда вам просто нужно достаточно долго смотреть на экран, пока вдохновение не поцелует вас. Худшее, что может произойти в этом случае, — это отключение экрана. Wake Lock API может предотвратить это.
Первый шаг — получить блокировку пробуждения с помощью navigator.wakelock.request method()
. Я передаю ему строку 'screen'
чтобы получить блокировку пробуждения экрана. Затем я добавляю прослушиватель событий, чтобы быть уведомленным, когда блокировка пробуждения снимается. Это может произойти, например, когда изменяется видимость вкладки. Если это произойдет, я могу, когда вкладка снова станет видимой, повторно получить блокировку пробуждения.
let wakeLock = null;
const requestWakeLock = async () => {
wakeLock = await navigator.wakeLock.request('screen');
wakeLock.addEventListener('release', () => {
console.log('Wake Lock was released');
});
console.log('Wake Lock is active');
};
const handleVisibilityChange = () => {
if (wakeLock !== null && document.visibilityState === 'visible') {
requestWakeLock();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);
Да, это прогрессивное улучшение, поэтому мне нужно загружать его только тогда, когда браузер поддерживает API.
if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
import('./wake_lock.mjs');
}
В Fugu Greetings есть флажок «Бессонница» , при установке которого экран не будет выключаться.

API обнаружения простоя
Иногда, даже если вы смотрите на экран часами, он просто бесполезен, и вы не можете придумать ни малейшей идеи, что делать с вашей поздравительной открыткой. API обнаружения простоя позволяет приложению определять время простоя пользователя. Если пользователь бездействует слишком долго, приложение возвращается в исходное состояние и очищает холст. В настоящее время этот API закрыт разрешением на уведомления , поскольку многие производственные случаи использования обнаружения простоя связаны с уведомлениями, например, для отправки уведомления только на устройство, которое пользователь в данный момент активно использует.
Убедившись, что разрешение на уведомления предоставлено, я создаю экземпляр детектора простоя. Я регистрирую прослушиватель событий, который отслеживает изменения состояния простоя, включая пользователя и состояние экрана. Пользователь может быть активным или бездействующим, а экран может быть разблокирован или заблокирован. Если пользователь бездействует, холст очищается. Я задаю детектору простоя пороговое значение в 60 секунд.
const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
const userState = idleDetector.userState;
const screenState = idleDetector.screenState;
console.log(`Idle change: ${userState}, ${screenState}.`);
if (userState === 'idle') {
clearCanvas();
}
});
await idleDetector.start({
threshold: 60000,
signal,
});
И как всегда, я загружаю этот код только тогда, когда браузер его поддерживает.
if ('IdleDetector' in window) {
import('./idle_detection.mjs');
}
В приложении Fugu Greetings холст очищается, если установлен флажок «Эфемерный» и пользователь слишком долго бездействует.

Закрытие
Уф, какая поездка. Так много API в одном примере приложения. И помните, я никогда не заставляю пользователя платить за загрузку функции, которую его браузер не поддерживает. Используя прогрессивное улучшение, я гарантирую, что загружается только нужный код. И поскольку с HTTP/2 запросы дешевы, этот шаблон должен хорошо работать для многих приложений, хотя вы можете рассмотреть возможность использования сборщика для действительно больших приложений.

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



Если вас заинтересовало приложение Fugu Greetings , найдите его и сделайте форк на GitHub .

Команда Chromium усердно работает над тем, чтобы сделать траву зеленее, когда дело касается расширенных API Fugu. Применяя прогрессивное улучшение в разработке своего приложения, я гарантирую, что все получат хороший, надежный базовый опыт, но что люди, использующие браузеры, которые поддерживают больше API веб-платформы, получат еще лучший опыт. Я с нетерпением жду, чтобы увидеть, что вы сделаете с прогрессивным улучшением в своих приложениях.
Благодарности
Я благодарен Кристиану Либелю и Хеманту ХМ , которые оба внесли свой вклад в Fugu Greetings. Эту статью рецензировали Джо Медли и Кейс Баскес . Джейк Арчибальд помог мне разобраться в ситуации с динамическим import()
в контексте сервис-воркера.