Pull to refresh
1946.18

JavaScript: редко используемые, но полезные API

Reading time13 min
Views9.3K


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


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



Репозиторий с примерами.


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!




Tags:
Hubs:
+21
Comments2

Articles

Information

Website
timeweb.cloud
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Timeweb Cloud