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

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

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

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

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

Говоря о JavaScript, ситуация с поддержкой браузерами новейших основных функций JavaScript ES 2015 великолепна. Новый стандарт включает в себя промисы, модули, классы, литералы шаблонов, стрелочные функции, let и const , параметры по умолчанию, генераторы, деструктурирующее присваивание, отдых и распространение, 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, используется с разрешения .)

Пример приложения: Приветствие Фугу

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

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

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

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

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

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

Обнаружение функций обычно используется для определения того, могут ли браузеры обрабатывать более современные функции, а полифилы часто используются для добавления недостающих функций с помощью 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-адресом большого двоичного объекта в качестве 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'),
    ]);
  }
};

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

Веб-инспектор Safari показывает загрузку устаревших файлов.
Вкладка сети 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() . Затем я записываю в файл объект, который является изображением моей поздравительной открытки. Наконец, я закрываю записываемый поток.

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

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

API-интерфейсы веб-ресурсов и целевых веб-ресурсов

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

Лист общего доступа в Desktop Safari на macOS, вызываемый кнопкой «Поделиться» в статье
API Web Share в настольном Safari на macOS.

Код, позволяющий это сделать, довольно прост. Я вызываю navigator.share() и передаю ему необязательный title , text и url в объекте. Но что, если я хочу прикрепить изображение? Уровень 1 API веб-ресурсов пока не поддерживает это. Хорошей новостью является то, что в 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);
  }
};

Как и прежде, я использую прогрессивное улучшение. Если в объекте navigator существуют и 'share' и 'canShare' , только тогда я иду вперед и загружаю 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 выбора контактов . Поскольку мой друг хранится в приложении контактов моего телефона, через API выбора контактов я могу подключиться к своим контактам из Интернета.

Во-первых, мне нужно указать список свойств, к которым я хочу получить доступ. В данном случае мне нужны только имена, но в других случаях меня могут заинтересовать номера телефонов, электронные письма, значки аватаров или физические адреса. Затем я настраиваю объект 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');
}

В «Приветствии Фугу», когда я нажимаю кнопку «Контакты» и выбираю двух своих лучших друзей, Сергея Михайловича Брина и劳伦斯·爱德华·"拉里"·佩奇, вы можете видеть, что средство выбора контактов ограничено отображением только их имен, но не их адреса электронной почты или другую информацию, например номера телефонов. Их имена затем нарисованы на моей поздравительной открытке.

Средство выбора контактов, показывающее имена двух контактов в адресной книге.
Выбор двух имен с помощью средства выбора контактов из адресной книги.
Имена двух ранее выбранных контактов нарисованы на поздравительной открытке.
Затем два имени будут нарисованы на поздравительной открытке.

API асинхронного буфера обмена

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

Чтобы скопировать что-то в буфер обмена системы, мне нужно в него записать. Метод navigator.clipboard.write() принимает в качестве параметра массив элементов буфера обмена. Каждый элемент буфера обмена по существу представляет собой объект с большим двоичным объектом в качестве значения и типом этого большого двоичного объекта в качестве ключа.

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, показывающее запрос на разрешение буфера обмена.
Запрос на разрешение буфера обмена.

Наконец, после принятия разрешения, изображение вставляется в приложение. Обратное тоже работает. Позвольте мне скопировать поздравительную открытку в буфер обмена. Когда я затем открываю «Предварительный просмотр» и нажимаю «Файл» , а затем «Новый из буфера обмена» , поздравительная открытка вставляется в новое изображение без названия.

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

API бейджинга

Еще один полезный API — Badging API . Будучи устанавливаемым 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 периодической фоновой синхронизации .

Первым шагом является регистрация события периодической синхронизации при регистрации сервис-воркера. Он прослушивает тег синхронизации под названием '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 в сервис-воркере. Если тегом события является '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 поддерживается браузером. Это относится как к коду клиента, так и к коду сервисного работника. В неподдерживающих браузерах ни один из них не загружается. Обратите внимание, что в сервис-воркере вместо динамического import() (который пока не поддерживается в контексте сервис-воркера) я использую классический 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 триггеров уведомлений . Как пользователь, я могу указать время, когда мне нужно будет закончить поздравительную открытку. Когда это время придет, я получу уведомление о том, что моя поздравительная открытка ждет.

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

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

API блокировки пробуждения

Я также хочу включить Wake Lock API . Иногда вам просто нужно достаточно долго смотреть на экран, пока вас не поцелует вдохновение. Худшее, что может случиться тогда, это выключение экрана. API Wake Lock может предотвратить это.

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