Привет, друзья!
В этой статье я хочу рассказать вам о малоизвестных Web API
, которые могут оказаться весьма полезными в "пограничных" ситуациях:
- Page Visibility API
- Web Share API
- Broadcast Channel API
- Internationalization API
- Beacon API
- Clipboard API
- Notifications API
- Performance 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!