Skip to main content

Заметка о малоизвестных, но полезных Web API

· 23 min read

Привет, друзья!

В этой статье я хочу рассказать вам о малоизвестных Web API, которые могут оказаться весьма полезными в "пограничных" ситуациях:

Репозитории с примерами:

Page Visibility API

Данный интерфейс позволяет определять, когда пользователь покидает страницу. Точнее, он вызывает событие при каждом изменении состояния видимости (visibility status) страницы, например, когда пользователь сворачивает/разворачивает окно, переходит на другую вкладку и т.д.

Раньше для этого приходилось прибегать к таким уловкам, как обработка событий blur и focus. Соответствующий код выглядел так:

window.addEventListener('blur', () => {
// пользователь покинул страницу
})

window.addEventListener('focus', () => {
// пользователь вернулся на страницу
})

Приведенный код работает, но не совсем так, как ожидается. Поскольку событие blur вызывается, когда страница теряет фокус, оно может возникнуть, когда пользователь нажимает на поиск, диалоговое окно (alert), консоль или границу окна. События blur и focus сообщают нам о том, что страница активна, но не о том, видим или скрыт ее контент.

Случаи использования

Page Visibility API может использоваться для предотвращения выполнения операций, которые имеют значение, только когда пользователь видит страницу, или для выполнения фоновых операций. Еще несколько кейсов:

  • приостановка воспроизведения видео, каруселей изображений (автослайдеров) или анимации, когда пользователь покидает страницу;
  • если на странице отображаются данные в реальном времени, нет смысла их обновлять, если пользователь покинул страницу;
  • отправка аналитических данных о действиях пользователя.

Интерфейс

Page Visibility API предоставляет 2 свойства и одно событие для получения доступа к состоянию видимости страницы:

  • document.hidden - доступное только для чтения глобальное свойство. Признано устаревшим. Если страница скрыта, возвращается true, иначе - false;
  • document.visibilityState - обновленная версия document.hidden. Возвращает 4 возможных значения:
    • visible - страница видима или, если быть точнее, страница не свернута и находится в текущей вкладке;
    • hidden - страница скрыта;
    • prerender - начальное состояние видимой страницы: предварительный рендеринг;
    • unloaded - страница выгружена из памяти;
  • visibilitychange - событие объекта document, возникающее при изменении visibilityState:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// страница видима
} else {
// страница скрыта
}
})

Пример использования

В качестве примера рассмотрим приостановку видео и прекращение получения ресурсов из API, когда пользователь покидает страницу. Создаем шаблон проекта с помощью Vite и Yarn:

# unknown-web-apis - название проекта
# --template vanilla - шаблон на чистом JS
yarn create vite unknown-web-apis --template vanilla

Переходим в созданную директорию, устанавливаем зависимости и запускаем сервер для разработки:

cd unknown-web-apis
yarn
yarn dev

Приложение доступно по адресу http://localhost:3000.

Удаляем шаблонный код из файла main.js и добавляем элемент video в файле index.html:

<div id="app">
<video controls id="video" src="https://joy.videvo.net/videvo_files/video/free/video0467/large_watermarked/_import_61516586ee8571.11252072_preview.mp4"></video>
</div>

Возвращаемся к main.js. Добавляем обработчик события visibilitychange объекта document:

document.addEventListener("visibilitychange", () => {
// выводим состояние видимости страницы в консоль
console.log(document.visibilityState);
});

Попробуйте свернуть/развернуть окно или переключиться на другую вкладку, чтобы увидеть изменения состояния видимости страницы.

Получаем ссылку на элемент video и управляем воспроизведением видео в зависимости от видимости страницы:

const video = document.getElementById("video");

document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
video.play();
} else {
video.pause();
}
});

Прим. пер.: приведенный код работать не будет: получаем ошибку Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first.. Это объясняется тем, что вызов video.play() при изменении состояния видимости страницы на visible равносилен наличию у элемента video атрибута autoplay. Современные браузеры допускают автоматическое (без участия пользователя) воспроизведение видео только в режиме без звука. Соответственно, для того, чтобы код работал, как ожидается, элементу video необходимо добавить атрибут muted.

Когда пользователь покидает страницу, воспроизведение видео приостанавливается. Когда пользователь возвращается на страницу, воспроизведение видео продолжается.

Теперь рассмотрим пример прекращения выполнения запросов к API. Для этого напишем функцию, запрашивающую цитату из quotable.io. Добавляем в index.html элемент div для хранения цитаты:

<div id="quote"></div>

Определяем в main.js функцию для получения произвольной цитаты с помощью Fetch API:

const quote = document.getElementById("quote");

const getQuote = async () => {
try {
const response = await fetch("https://api.quotable.io/random");
const { content, author, dateAdded } = await response.json();
const parsedQuote = `<q>${content}</q> <br> <p>- ${author}</p> <br> <p>Added on ${dateAdded}</p>`;
quote.innerHTML = parsedQuote;
} catch (e) {
console.error(e);
}
};

getQuote();

Приведенный код работает, но функция getQuote вызывается только один раз. Обернем ее в setInterval с интервалом в 10 секунд:

setInterval(getQuote, 10000);

Если пользователь свернет окно или перейдет на другую вкладку, запросы все равно будут отправляться каждые 10 секунд. Давайте это исправим:

const getQuote = async () => {
if (document.visibilityState !== "visible") return;

// остальной код
};

Теперь запросы будут выполняться только при условии, что страница находится в видимом состоянии.

Поддержка - 98.24%.

Web Share API

Web Share API предоставляет разработчикам доступ к встроенному механизму совместного использования (native sharing mechanism) операционной системы, что особенно актуально для мобильных телефонов. Данный интерфейс позволяет делиться текстом, ссылками и файлами без создания собственного механизма или использования сторонних решений.

Случаи использования

Web Share API позволяет делиться содержимым страницы в соцсетях или копировать его в буфер обмена пользователя.

Интерфейс

Web Share API предоставляет 2 метода:

  • navigator.canShare(data): принимает данные для совместного использования и возвращает логическое значение - индикатор того, можно ли этими данными поделиться;

  • navigator.share(data): возвращает промис, который разрешается в случае успешного шаринга (sharing) данных. Данный метод вызывает нативный механизм и принимает данные для шаринга. Обратите внимание: этот метод может вызываться только в ответ на действие пользователя (нажатие кнопки, переход по ссылке и т.п.) (требуется кратковременная активация). data - это объект со следующими свойствами:

  • url - ссылка для шаринга;

  • text - текст;

  • title - заголовок;

  • files - массив объектов File.

Пример использования

Возьмем последний пример и добавим возможность делиться цитатой. Добавляем соответствующую кнопку в index.html:

<button id="share-button">Share Quote</button>

В main.js получаем ссылку на эту кнопку и определяем функцию для шаринга данных:

const shareButton = document.getElementById("share-data");

const shareQuote = async (data) => {
try {
await navigator.share(data);
} catch (e) {
console.error(e);
}
};

Нам также потребуется глобальная переменная для хранения содержимого текущей цитаты:

let quoteText;

const getQuote = async () => {
if (document.visibilityState !== "visible") return;

try {
// ...
quoteText = content;
} catch (e) {
console.error(e);
}
};

Определяем обработчик нажатия кнопки:

shareButton.addEventListener("click", () => {
const data = {
title: "A Beautiful Quote",
text: quoteText,
url: location.href,
};

shareQuote(data);
});

Обратите внимание: Web Share API работает только в безопасном окружении. Это означает, что страница должна обслуживаться по протоколу https или wss.

Поддержка - 89.82%.

Broadcast Channel API

Broadcast Channel API позволяет контекстам браузера (browser contexts) обмениваться данными друг с другом. К браузерным контекстам относятся такие элементы, как окно, вкладка, iframe и т.д. По причинам безопасности контексты, обменивающиеся данными, должны принадлежать одному источнику (same origin). Один источник означает одинаковый протокол, домен и порт.

Случаи использования

Broadcast Channel API обычно используется для синхронизации окон и вкладок браузера для улучшения пользовательского опыта или повышения безопасности. Он также может применяться для уведомления одного контекста о завершении процесса в другом контексте. Другие примеры:

  • авторизация пользователя во всех вкладках;
  • отображение загруженного ресурса во всех вкладках;
  • запуск сервис-воркера для выполнения фоновой задачи.

Интерфейс

Broadcast Channel API предоставляет объект BroadcastChannel, позволяющий обмениваться сообщениями с другими контекстами. Конструктор этого объекта принимает единственный аргумент: строку - идентификатор канала (channel identifier):

const broadcastChannel = new BroadcastChannel("channel_identifier");

BroadcastChannel предоставляет 2 метода:

  • broadcastChannel.postMessage(message): позволяет отправлять сообщения всем подключенным контекстам. В качестве аргумента данный метод принимает любой тип данных:
broadcastChannel.postMessage("Example message");
  • broadcastChannel.close(): закрываем канал коммуникации, что позволяет браузеру выполнить сборку мусора.

При получении сообщения возникает событие message. Это событие содержит свойство data с отправленными данными, а также другие свойства, позволяющие идентифицировать отправителя, такие как origin, lastEventId, source и ports:

broadcastChannel.addEventListener("message", ({ data, origin }) => {
console.log(`${origin} says ${data}`);
});

Пример использования

Возьмем последний пример, создадим новый контекст и добавим возможность обмена цитатами между существующим и новым контекстами.

Создаем директорию new-context в корне проекта. Создаем в ней файл index.html следующего содержания:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="quote"></div>
<script type="module" src="./main.js"></script>
</body>
</html>

Создаем файл new-context/main.js.

Получаем ссылку на div и создаем новый канал коммуникации:

const quote = document.getElementById("quote");
const broadcastChannel = new BroadcastChannel("quote_channel");

Добавляем обработчик события message:

broadcastChannel.addEventListener("message", ({ data }) => {
quote.innerHTML = data;
});

Создаем канал в основном main.js и редактируем функцию getQuote:

const broadcastChannel = new BroadcastChannel("quote_channel");

const getQuote = async () => {
if (document.visibilityState !== "visible") return;

try {
// ...
broadcastChannel.postMessage(parsedQuote);
} catch (e) {
console.error(e);
}
};

Прим. пер.: для того, чтобы запустить данный пример локально, необходимо сделать следующее:

  • в корне проекта создаем файл vite.config.js следующего содержания:
import { defineConfig } from "vite";

export default defineConfig({
appType: "mpa",
});
  • запускаем сервер для разработки с помощью команды yarn dev;
  • открываем 2 вкладки:
    • по адресу http://127.0.0.1:5173/index.html;
    • по адресу http://127.0.0.1:5173/new-context/index.html.

Поддержка - 92.3%.

Internationalization API

Шпаргалка по Internationalization API

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

Предположим, что мы хотим отобразить на странице "10 ноября 2022 года" как "11/10/22". Эта дата в разных странах будет выглядеть по-разному:

  • 11/10/22 или ММ/ДД/ГГ в США;
  • 10/11/22 в Европе и Латинской Америке;
  • 22/11/10 в Японии, Китае и Канаде.

Здесь на помощь приходит Internationalization API (или I18n). Данный интерфейс позволяет решить несколько групп задач, связанных с интернационализацией и локализацией, но в этой статье мы не будем погружаться в него слишком глубоко.

Интерфейс

Для определения страны пользователя в I18n используется идентификатор локали (или просто локаль) (locale identifier, locale). Локаль - это строка, представляющая страну, регион, диалект и другие характеристики. Если точнее, локаль - это строка, состоящая из подтегов (subtags), разделенных дефисом, например:

  • zh - китайский (язык);
  • zh-Hant - китайский (язык), традиционные иероглифы (сценарий - script);
  • zh-Hang-TW - китайский (язык), традиционные иероглифы (сценарий), используемые на Тайване (регион).

Полный список подтегов можно найти в этом RFC.

I18n предоставляет объект Intl, который, в свою очередь, предоставляет несколько специальных конструкторов для работы с чувствительными к языку данными, наиболее интересными из которых являются следующие:

  • Intl.DateTimeFormat - форматирование даты и времени;
  • Intl.DisplayNames - форматирование названий языков, регионов и сценариев;
  • Intl.Locale - генерация и манипуляция идентификаторами локалей;
  • Intl.NumberFormat - форматирование чисел;
  • Intl.RelativeTimeFormat - форматирование относительного времени (завтра, 2 дня назад и т.п.).

Пример использования

В качестве примера рассмотрим использование конструктора Intl.DateTimeFormat для форматирование свойства dateAdded цитат. Данный конструктор принимает 2 аргумента: строку locale для определения правил форматирование даты и объект options для кастомизации форматирования.

Прим. пер.: в качестве первого аргумента Intl.DateTimeFormat также принимает массив локалей. Например, для установки дефолтной локали пользователя в конструктор передается пустой массив ([]).

Объект, возвращаемый Intl.DateTimeFormat, предоставляет метод format, который принимает объект Date с датой для форматирования и объект options для кастомизации отображения форматированной даты:

const logDate = (locale = []) => {
const date = new Date("2022-11-10");
const dateTime = new Intl.DateTimeFormat(locale, { timeZone: "UTC" });
const formattedDate = dateTime.format(date);
console.log(formattedDate);
};

logDate(); // 10.11.2022
logDate("en-US"); // 11/10/2022
logDate("de-DE"); // 10.11.2022
logDate("zh-TW"); // 2022/11/10

Обратите внимание: мы установили настройку timeZone в значение UTC для того, чтобы при форматировании даты не учитывалось локальное время пользователя.

Определяем в main.js функцию для форматирования даты:

function formatDate(dateString) {
const date = new Date(dateString);
const dateTime = new Intl.DateTimeFormat([], { timeZone: "UTC" });
return dateTime.format(date);
}

Вызываем эту функцию внутри функции getQuote для форматирования свойства dateAdded:

const getQuote = async () => {
if (document.visibilityState !== "visible") return;

try {
// ...
const parsedQuote = `<q>${content}</q> <br> <p>- ${author}</p> <br> <p>Added on ${formatDate(
dateAdded
)}</p>`;
// ...
} catch (e) {
console.error(e);
}
};

Поддержка - 97.74%.

Прим. пер.: на днях использовал Intl.DateTimeFormat для отображения даты и времени в коротком формате:

const getDateWithHoursAndMinutes = (date) =>
new Intl.DateTimeFormat([], {
dateStyle: "short",
timeStyle: "short",
}).format(date);

console.log(getDateWithHoursAndMinutes(new Date())); // 23.09.2022, 21:30

Beacon API

Beacon API позволяет отправлять на сервер асинхронные и неблокирующие запросы (методом POST), которые гарантированно завершаются до выгрузки страницы, в отличие от XMLHttpRequest или Fetch API.

Одним из основных вариантов использования Beacon API является логгирование активности пользователей или отправка аналитических данных на сервер.

Раньше для этого приходилось прибегать к таким уловкам, как обработка событий unload или beforeunload глобального объекта Window с помощью синхронного XMLHttpRequest, например:

const someData = {
a: 1,
b: 2,
};

// страница будет выгружена только после отправки данного запроса
window.addEventListener("beforeunload", () => {
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://example.com/beacon");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

const params = new URLSearchParams(Object.entries(someData));

xhr.send(params);
});

Интерфейс

Beacon API расширяет свойство navigator методом sendBeacon, который имеет следующую сигнатуру:

navigator.sendBeacon(url: string | URL, data?: BodyInit | null)
  • url - адрес сервера;
  • data - опциональные данные для отправки, которые могут быть строкой, объектом, ArrayBuffer, Blob, DataView, FormData, TypedArray или URLSearchParams.

sendBeacon возвращает логическое значение (true или false) - индикатор постановки data в очередь для передачи.

Пример использования

Создаем шаблон проекта с помощью Yarn и Vite на чистом JavaScript:

# rare-web-apis - название проекта
# --template vanilla - используемый шаблон
yarn create vite rare-web-apis --template vanilla

Переходим в созданную директорию, устанавливаем зависимости и запускаем сервер для разработки:

cd rare-web-apis
yarn
yarn dev

Определяем в файле main.js следующий обработчик:

const someData = {
a: 1,
b: 2,
};

document.addEventListener("visibilitychange", () => {
// если страница скрыта
if (document.visibilityState === "hidden") {
// формируем параметры запроса
const searchParams = new URLSearchParams(Object.entries(someData));

try {
// и отправляем их на сервер
const result = navigator.sendBeacon(
// такого адреса не существует, поэтому ожидаемо получаем 404
"https://example.com/beacon",
searchParams
);
console.log(result);
} catch (e) {
console.error(e);
}
}
});

Событие visibilitychange объекта document (подробнее о нем можно почитать по ссылке, приведенной в начале статьи) является более надежным способом определения состояния видимости страницы, чем события unload или beforeunload. В обработчике при скрытии страницы, например, при переключении вкладки или сворачивании страницы, с помощью sendBeacon по адресу https://example.com/beacon отправляются некоторые данные в форме URLSearchParams.

Результат переключения вкладки:


Поддержка - 96.8%.

Clipboard API

Clipboard API позволяет выполнять асинхронные операции записи/чтения текстовых и других данных в/из системного буфера обмена, а также обрабатывать события copy, cut и paste (копирование, вырезка и вставка) буфера.

По причинам безопасности Clipboard API доступен только при условии, что:

  • страница обслуживается по протоколу https или localhost;
  • страница находится в активной вкладке браузера (не находится в фоновом режиме);
  • операции записи/чтения инициализируются пользователем (например, с помощью нажатия кнопки).

Разрешение clipboard-write для записи данных предоставляется активной странице автоматически, а разрешение clipboard-read для чтения данных запрашивается у пользователя с помощью Permissions API.

Раньше для работы с содержимым редактируемой области использовался метод document.execCommand. Например, вот как выполнялась запись текста:

function copyText(text) {
const el = document.createElement("textarea");

el.value = text;

el.setAttribute("readonly", "");
el.setAttribute("type", "hidden");

document.body.appendChild(el);

el.select();

document.execCommand("copy");

document.body.removeChild(el);
}

Интерфейс

Clipboard API расширяет свойство navigator интерфейсом Clipboard, экземпляры которого предоставляют следующие методы для работы с буфером:

  • writeText(text: string) - для записи текста, принимает строку;
  • readText() - для чтения текста, возвращает строку;
  • write(data: ClipboardItem[]) - для записи данных, принимает массив объектов ClipboardItem (см. ниже);
  • read() - для чтения данных, возвращает массив объектов ClipboardItem.

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

ClipboardItem - это интерфейс, предназначенный для работы с нетекстовыми данными, который имеет следующую сигнатуру:

new ClipboardItem(
items: Record<string, string | Blob | PromiseLike<string | Blob>>,
options?: ClipboardItemOptions
)
  • items - данные для записи в форме объектов, ключами которых являются MIME-типы, а значениями - строки, Blob или промисы, разрешающиеся строками или Blob;
  • options - опциональные настройки (точнее, одна настройка - presentationStyle ).

В действительности, ClipboardItem можно использовать также для работы с текстовыми данными, но есть один нюанс, о котором чуть позже.

Что касается событий copy, cut и paste, то их обработка обычно выполняется через свойство clipboardData события ClipboardEvent, которое содержит объект DataTransfer, предоставляющий следующие методы:

  • setData(format: string, data: string) - для записи данных;
  • getData(format: string) - для чтения данных;
  • clearData() - для удаления данных и др.

Пример использования

Начнем с записи и чтения текста. Редактируем файл index.html следующим образом:

<div>
<div>
<!-- редактируемый текст для записи/копирования -->
<p id="copy-box" contenteditable="true">Text to copy</p>
<!-- кнопка копирования -->
<button id="copy-btn">Copy text or selection</button>
</div>
<div>
<!-- контейнер для вставки -->
<p id="paste-box"></p>
<!-- кнопка вставки -->
<button id="paste-btn">Paste text</button>
</div>
<!-- контейнер для логов -->
<p id="log-box"></p>
</div>

Получаем ссылки на DOM-элементы:

const [copyBox, copyBtn, pasteBox, pasteBtn, logBox] = [
"copy-box",
"copy-btn",
"paste-box",
"paste-btn",
"log-box",
].map((id) => document.getElementById(id));

Определяем функцию копирования текста:

async function copyText() {
let textToCopy;

// получаем выделение
const selectedText = getSelection().toString().trim();

// текстом для копирования является либо выделенный текст, либо содержимое `copyBox`
selectedText
? (textToCopy = selectedText)
: (textToCopy = copyBox.textContent.trim());

// если текст отсутствует
if (!textToCopy) {
logBox.textContent = "No text to copy";
return;
}

try {
// записываем текст в буфер
await navigator.clipboard.writeText(textToCopy);
logBox.textContent = "Copy success";
} catch (e) {
console.error(e);
logBox.textContent = "Copy error";
}
}

Определяем функцию для вставки текста:

async function pasteText() {
try {
// получаем текст для вставки
const textToPaste = await navigator.clipboard.readText();
// если текст отсутствует
if (!textToPaste) {
logBox.textContent = "No text to paste";
return;
}
// вставляем текст
pasteBox.textContent = textToPaste;
logBox.textContent = "Paste success";
} catch (e) {
console.error(e);
logBox.textContent = "Paste error";
}
}

Наконец, регистрируем соответствующие обработчики:

copyBtn.addEventListener("click", copyText);
pasteBtn.addEventListener("click", pasteText);

Обратите внимание: при первой вставке текста браузер запрашивает разрешение на чтение буфера. При отказе в разрешении выбрасывается исключение DOMException: Read permission denied.

Записать текстовые данные с помощью ClipboardItem можно следующим образом:

const text = "Text to copy";
const type = "text/plain";
const blob = new Blob([text], { type });
const data = {
[type]: blob,
};
const item = new ClipboardItem(data);
await navigator.clipboard.write([item]);

Обратите внимание: несмотря на то, что значением объекта ClipboardItem может быть строка (new ClipboardItem({ [type]: text })), при записи такого объекта в буфер выбрасывается исключение DOMException: Invalid Blob types.

Также обратите внимание, что при программной записи данных в случае, когда страница находится в фоновом режиме, выбрасывается исключение DOMException: Document is not focused.

Для извлечения данных из ClipboardItem используется метод getType:

const blob = await item.getType(type);
const text = await blob.text();
console.log(text); // Text to copy

Добавим возможность копирования и вставки изображения, хранящегося на сервере.

Добавляем кнопки в index.html:

<div>
<button id="copy-img-btn">Copy remote image</button>
<button id="paste-img-btn">Paste remote image</button>
</div>

Определяем тип и функцию для копирования изображения:

const IMG_TYPE = "image/png";

async function copyRemoteImg() {
try {
const response = await fetch(
"https://images.unsplash.com/photo-1529788295308-1eace6f67388?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1171&q=80"
);

// см. ниже
const blob = new Blob([await response.blob()], { type: IMG_TYPE });

// создаем элемент копирования
const item = new ClipboardItem({ [blob.type]: blob });

// записываем его в буфер
await navigator.clipboard.write([item]);

logBox.textContent = "Copy image success";
} catch (e) {
console.error(e);
logBox.textContent = "Copy image error";
}
}

Не уверен насчет других браузеров, но в Chrome наблюдается следующее:

  • при преобразовании изображения из тела ответа в Blob с помощью response.blob() дефолтным типом становится image/jpeg (независимо от типа запрашиваемого изображения);
  • при попытке записи такого Blob в буфер выбрасывается исключение DOMException: Type image/jpeg not supported on write.

Поэтому приходится выполнять двойное преобразование с помощью new Blob([await response.blob()], { type: IMG_TYPE });.

Определяем функцию для чтения данных из буфера и вставки изображения:

async function pasteRemoteImg() {
// блобы изображений
const imageBlobs = [];

try {
// получаем копированные элементы
const items = await navigator.clipboard.read();

// перебираем их
for (const item of items) {
// перебираем типы (см. ниже)
for (const type of item.types) {
// получаем блоб
const blob = await item.getType(type);

// если блоб содержит изображение
if (blob.type.startsWith("image")) {
// помещаем его в массив
imageBlobs.push(blob);
}
}
}

if (imageBlobs.length) {
// перебираем блобы
imageBlobs.forEach((blob) => {
// создаем элемент изображения и добавляем его в тело документа
const img = document.createElement("img");
img.width = 320;
img.src = URL.createObjectURL(blob);
document.body.append(img);
});

logBox.textContent = "Paste image success";
return;
}

logBox.textContent = "No images to paste";
} catch (e) {
console.error(e);
logBox.textContent = "Paste image error";
}
}

Итерация по item.types является безопасной, в отличие от прямого обращения к item.getType() - при отсутствии типа выбрасывается исключение DOMException: Failed to execute 'getType' on 'ClipboardItem': The type was not found.

Регистрируем соответствующие обработчики:

copyImgBtn.addEventListener("click", copyRemoteImg);
pasteImgBtn.addEventListener("click", pasteRemoteImg);

Реализуем модификацию копируемых и вставляемых данных.

Редактируем index.html:

<textarea cols="30" rows="10" id="text-area">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Libero, labore.</textarea>

Определяем функцию модификации копируемых данных:

function onCopy(e) {
e.preventDefault();

const selection = getSelection().toString().trim();

if (!selection) return;

e.clipboardData.setData("text/plain", `${selection}\ncopied from MySite.com`);
}

Данная функция добавляет к копируемому тексту строку copied from MySite.com.

Определяем функцию модификации добавляемых данных:

function onPaste(e) {
e.preventDefault();

const text = e.clipboardData.getData("text").trim();

if (!text) return;

e.target.value += text.toUpperCase();
}

Данная функция переводит добавляемый текст в верхний регистр.

Обратите внимание, что в обоих случаях отключается стандартная обработка события браузером с помощью e.preventDefault().

Регистрируем соответствующие обработчики:

textArea.addEventListener("copy", onCopy);
textArea.addEventListener("paste", onPaste);

Поддержка - 95.08%.

Notifications API

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

Интерфейс

Для запроса разрешения на показ уведомлений используется метод Notification.requestPermission. Данный метод возвращает промис, который разрешается или отклоняется со статусом разрешения. Статус разрешения содержится в свойстве Notification.permission и может иметь одно из трех значений:

  • default - запрос на разрешение не выполнялся, уведомления не отображаются;
  • granted - пользователь предоставил разрешение, уведомления отображаются;
  • denied - пользователь отклонил запрос, уведомления не отображаются.

Для создания уведомления используется конструктор Notification, который имеет следующую сигнатуру:

new Notification(title: string, options?: NotificationOptions | undefined)
  • title: string - заголовок уведомления;
  • options - опциональный объект с настройками, такими как:
    • body: string - тело уведомления;
    • icon: string - ссылка на иконку;
    • tag: string - тег, используемый для идентификации уведомления. Тег позволяет обновлять уведомления без их отображения, что может быть полезным при большом количестве уведомлений;
    • image: string - ссылка на изображение;
    • data: any - данные, ассоциированные с уведомлением и др.

Для закрытия уведомления используется метод notification.close.

Notifications API позволяет обрабатывать следующие события:

  • show - отображение уведомления;
  • close - закрытие уведомления;
  • click - нажатие на уведомление;
  • error.

Пример использования

В качестве примера реализуем отображение уведомлений о скрытии страницы. При клике по уведомлению в контейнер для логгов будет выводиться соответствующее сообщение.

Определяем в index.html кнопку для запроса разрешения на показ уведомлений:

<button id="notification-btn">Enable notifications</button>

Регистрируем соответствующий обработчик в main.js:

notificationBtn.addEventListener("click", () => {
Notification.requestPermission();
});

Определяем переменные для уведомления и идентификатора таймера, а также функцию для создания уведомления:

let notification;
let notificationTimeoutId;

function createNotification() {
// заголовок уведомления
const title = "Page is hidden";
// настройки
const options = {
body: "MySite.com is not visible",
// иконку можно найти в репозитории проекта в директории `public`
icon: "/notification.png",
};
// создаем уведомления
notification = new Notification(title, options);

// регистрируем однократный обработчик
notification.addEventListener(
"click",
(e) => {
// здесь, в частности, можно найти `title` и `options`, переданные в конструктор
console.log(e.target);
logBox.textContent = "Notification clicked";
},
{
once: true,
}
);

// уничтожаем уведомление через 3 секунды (см. ниже)
notificationTimeoutId = setTimeout(() => {
notification.close();
clearTimeout(notificationTimeoutId);
notification = null
notificationTimeoutId = null
}, 3000);
}

Несмотря на то, что большинство браузеров автоматически уничтожают уведомления по прошествии некоторого времени (около 4 сек), рекомендуется делать это явно.

Расширяем обработку изменения состояния видимости страницы:

document.addEventListener("visibilitychange", () => {
// если страница скрыта
if (document.visibilityState === "hidden") {
// ...

// если пользователь предоставил разрешение на показ уведомлений
if (Notification.permission === "granted") {
createNotification();
}
// если страница видима
} else if (document.visibilityState === "visible") {
// если имеется уведомление
if (notification) {
// уничтожаем его
notification.close();
notification = null;
// и очищаем таймер при необходимости
if (notificationTimeoutId) {
clearTimeout(notificationTimeoutId);
notificationTimeoutId = null;
}
}
}
});

Включаем уведомления:


Переключаем вкладку:


При клике по уведомлению на странице приложения появляется сообщение Notification clicked.

Обратите внимание: при нахождении в другой вкладке уведомление уничтожается через 3 сек, а при возвращении в приложение - сразу.

Поддержка оставляет желать лучшего - 79.86%.

Performance API

Performance API позволяет измерять задержку в приложении на стороне клиента. Интерфейсы Performance (интерфейсы производительности) считаются высокоточными (high resolution), поскольку имеют точность, равную тысячным миллисекунды (точность зависит от ограничений аппаратного или программного обеспечения). Данные интерфейсы используются для вычисления частоты кадров (например, в анимации) и бенчмаркинге (например, для измерения времени загрузки ресурса).

Поскольку системные часы (system clock) платформы подвергаются различным корректировкам (таким как коррекция времени по NTP), интерфейсы Performance поддерживают монотонные часы (monotonic clock), т.е. время, которое все время увеличивается. Для этого Performance API определяет тип DOMHighResTimeStamp вместо использования интерфейса Date.now().

DOMHighResTimeStamp представляет высокоточную отметку времени (point in time). Данный тип является double и используется интерфейсами производительности. Значение DOMHighResTimeStamp может быть дискретной отметкой времени или разницей между двумя такими отметками.

Единицей DOMHighResTimeStamp является миллисекунда с точностью до 5 микросекунд. Если браузер не может обеспечить такую точность, допускается представление значения в миллисекундах с точностью до миллисекунды.

Интерфейс

Основным методом, предоставляемым Performance API, является метод now, который возвращает DOMHighResTimeStamp, значение которого зависит от времени создания контекста браузера или воркера (worker).

Кроме этого, рассматриваемый интерфейс содержит два основных свойства:

  • timing - возвращает объект PerformanceTiming, содержащий такую информацию, как время начала навигации, время начала и завершения перенаправлений, время начала и завершения ответов и т.д.;
  • navigation - возвращает объект PerformanceNavigation, представляющий тип навигации, происходящей в текущем контексте браузера, такой как переход к странице из истории, по ссылке и т.п.

Пример использования

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

Редактируем main.js:

const howLong =
(fn) =>
async (...args) => {
const start = performance.now();
const result = await fn(...args);
console.log(`@result of ${fn.name}`, result);
const difference = performance.now() - start;
console.log("@time taken", difference);
};

Определяем функцию вычисления факториала числа и измеряем время ее выполнения:

const getFactorial = (n) => (n <= 1 ? 1 : n * getFactorial(n - 1));
howLong(getFactorial)(12);

Определяем функцию получения данных из сети и измеряем время ее выполнения:

const fetchSomething = (url) => fetch(url).then((r) => r.json());
howLong(fetchSomething)("https://jsonplaceholder.typicode.com/users?_limit=10");

Результат:


Поддержка - 97.17%.

Благодарю за внимание и happy coding!