Постепенно улучшайте свое прогрессивное веб-приложение

Создаем для современных браузеров и постоянно совершенствуем, как будто на дворе 2003 год

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

Инклюзивный веб-дизайн будущего с прогрессивным улучшением. Титульный слайд из оригинальной презентации Финка и Чампеона.
Слайд: Инклюзивный веб-дизайн будущего с прогрессивным улучшением. ( Источник )

Современный JavaScript

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

Таблица поддержки CanIUse для функций ES6 показывает поддержку во всех основных браузерах.
Таблица поддержки браузерами ECMAScript 2015 (ES6). ( Источник )

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

Таблица поддержки CanIUse для асинхронных функций, показывающая поддержку во всех основных браузерах.
Таблица поддержки браузером асинхронных функций. ( Источник )

И даже супер недавние дополнения языка 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
Знаменитое фоновое изображение зеленой травы Windows XP.
Трава зеленеет, когда дело касается основных функций JavaScript. (Снимок экрана продукта Microsoft, использован с разрешения .)

Пример приложения: Fugu Greetings

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

Fugu Greetings — это приложение для рисования, которое позволяет вам создавать виртуальные поздравительные открытки и отправлять их вашим близким. Оно иллюстрирует основные концепции PWA . Оно надежно и полностью автономно, поэтому даже если у вас нет сети, вы все равно можете им пользоваться. Его также можно установить на домашний экран устройства, и оно легко интегрируется с операционной системой как отдельное приложение.

Fugu Приветствует PWA с рисунком, напоминающим логотип сообщества PWA.
Пример приложения Fugu Greetings .

Прогрессивное улучшение

С этим разобрались, пора поговорить о прогрессивном улучшении . Глоссарий 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.

Safari Web Inspector показывает загрузку устаревших файлов.
Вкладка «Сеть» в Safari Web Inspector.
Инструменты разработчика Firefox, показывающие загрузку устаревших файлов.
Вкладка «Сеть» в инструментах разработчика Firefox.

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

Chrome DevTools показывает загрузку современных файлов.
Вкладка «Сеть» в Chrome DevTools.

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, я могу открыть файл, как и раньше. Импортированный файл рисуется прямо на холсте. Я могу вносить изменения и, наконец, сохранять их с помощью настоящего диалогового окна сохранения, где я могу выбрать имя и место хранения файла. Теперь файл готов к вечному сохранению.

Приложение Fugu Greetings с диалоговым окном открытия файла.
Диалог открытия файла.
Приложение Fugu Greetings теперь с импортированным изображением.
Импортированное изображение.
Приложение Fugu Greetings с измененным изображением.
Сохранение измененного изображения в новый файл.

API Web Share и Web Share Target

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

Панель «Поделиться» в Safari для настольных компьютеров на macOS активируется кнопкой «Поделиться» в статье
API Web Share в Safari для настольных компьютеров на macOS.

Код, который это делает, довольно прост. Я вызываю 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, и виджет составителя писем всплывает с прикрепленным изображением.

Таблица общего доступа на уровне ОС, на которой показаны различные приложения, в которых можно поделиться изображением.
Выбор приложения для отправки файла.
Виджет создания письма Gmail с прикрепленным изображением.
Файл прикрепляется к новому письму в редакторе 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 спрашивает меня, хочу ли я разрешить приложению видеть текст и изображения в буфере обмена.

Приложение Fugu Greetings, отображающее запрос на разрешение доступа к буферу обмена.
Запрос на разрешение доступа к буферу обмена.

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

Приложение macOS Preview с безымянным, только что вставленным изображением.
Изображение, вставленное в приложение macOS Preview.

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');
}

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

Цифры от одного до семи, нарисованные на поздравительной открытке, каждая выполнена всего одним росчерком пера.
Нарисуйте цифры от 1 до 7, используя семь росчерков ручки.
Значок в приложении Fugu Greetings с изображением цифры 7.
Счетчик взмахов пера в виде значка приложения.

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 периодической фоновой синхронизации.

Приложение Fugu Greetings с новым изображением поздравительной открытки дня.
При нажатии кнопки «Обои» отображается изображение дня.

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 с напоминанием пользователю о необходимости закончить отправку поздравительной открытки.
Планирование локального уведомления для напоминания о необходимости закончить поздравительную открытку.

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

Центр уведомлений macOS, отображающий сработавшее уведомление от Fugu Greetings.
Сработавшее уведомление отображается в Центре уведомлений macOS.

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 холст очищается, если установлен флажок «Эфемерный» и пользователь слишком долго бездействует.

Приложение Fugu Greetings с очищенным холстом после слишком долгого бездействия пользователя.
Если флажок «Эфемерный» установлен и пользователь слишком долго бездействует, холст очищается.

Закрытие

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

Панель «Сеть» Chrome DevTools показывает только запросы файлов с кодом, поддерживаемым текущим браузером.
Вкладка «Сеть» в Chrome DevTools показывает только запросы файлов с кодом, поддерживаемым текущим браузером.

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

Fugu Greetings работает на Android Chrome и демонстрирует множество доступных функций.
Fugu Greetings работает на Android Chrome.
Fugu Greetings запущен в Safari для настольных компьютеров, отображает меньше доступных функций.
Fugu Greetings работает в Safari на компьютере.
Fugu Greetings запущен на десктопном Chrome и демонстрирует множество доступных функций.
Fugu Greetings запущен на десктопном Chrome.

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

Репозиторий Fugu Greetings на GitHub.
Приложение Fugu Greetings на GitHub.

Команда Chromium усердно работает над тем, чтобы сделать траву зеленее, когда дело касается расширенных API Fugu. Применяя прогрессивное улучшение в разработке своего приложения, я гарантирую, что все получат хороший, надежный базовый опыт, но что люди, использующие браузеры, которые поддерживают больше API веб-платформы, получат еще лучший опыт. Я с нетерпением жду, чтобы увидеть, что вы сделаете с прогрессивным улучшением в своих приложениях.

Благодарности

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