Clipboard API на практике: чтение/запись буфера обмена браузера и политики безопасности

技术架构

Clipboard API: Современный стандарт взаимодействия с буфером обмена

Операции с буфером обмена в браузере прошли путь от document.execCommand до Async Clipboard API:

Аспект execCommand('copy') Async Clipboard API
Стандарт Устаревший Рекомендован W3C
Асинхронность Нет (синхронная блокировка) Да (на основе Promise)
Разрешения Неявные (жест пользователя) Явная Permissions API
Форматированный текст Не поддерживается Поддерживается (ClipboardItem)
Пользовательские форматы Не поддерживается Поддерживается (веб-пользовательские форматы)
Чтение/запись изображений Не поддерживается Поддерживается
Надёжность Зависит от выделения Прямой доступ к буферу обмена

Подробный разбор основного API

Интерфейсы Clipboard API

interface Clipboard extends EventTarget {
  read(): Promise<ClipboardItem[]>;
  readText(): Promise<string>;
  write(data: ClipboardItem[]): Promise<void>;
  writeText(text: string): Promise<void>;
}

interface ClipboardItem {
  readonly types: string[];
  getType(type: string): Promise<Blob>;
}

Чтение/запись текста

async function copyText(text: string) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('Скопировано в буфер обмена');
  } catch (err) {
    console.error('Ошибка копирования:', err);
    fallbackCopyText(text);
  }
}

async function pasteText(): Promise<string> {
  try {
    const text = await navigator.clipboard.readText();
    return text;
  } catch (err) {
    console.error('Ошибка чтения:', err);
    return '';
  }
}

Запись форматированного текста и мультиформатная запись

Мультиформатный ClipboardItem

async function copyRichText(html: string, plainText: string) {
  const clipboardItem = new ClipboardItem({
    'text/html': new Blob([html], { type: 'text/html' }),
    'text/plain': new Blob([plainText], { type: 'text/plain' }),
  });
  await navigator.clipboard.write([clipboardItem]);
}

copyRichText(
  '<strong>Важно</strong>: Отправить до <em>пятницы</em>',
  'Важно: Отправить до пятницы'
);

Копирование изображения

async function copyImage(pngBlob: Blob) {
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': pngBlob })
  ]);
}

async function copyCanvas(canvas: HTMLCanvasElement) {
  const blob = await new Promise<Blob>((resolve) => {
    canvas.toBlob((b) => resolve(b!), 'image/png');
  });
  await navigator.clipboard.write([
    new ClipboardItem({ 'image/png': blob })
  ]);
}

Вставка изображения

document.addEventListener('paste', async (e) => {
  const items = e.clipboardData?.items;
  if (!items) return;

  for (const item of items) {
    if (item.type.startsWith('image/')) {
      const blob = item.getAsFile()!;
      const url = URL.createObjectURL(blob);
      const img = document.createElement('img');
      img.src = url;
      document.body.appendChild(img);
    }
  }
});

Пользовательские MIME-типы (веб-пользовательские форматы)

async function copyCustomData(data: object) {
  const json = JSON.stringify(data);
  await navigator.clipboard.write([
    new ClipboardItem({
      'web application/json': new Blob([json], { type: 'application/json' }),
      'text/plain': new Blob([json], { type: 'text/plain' }),
    })
  ]);
}

async function pasteCustomData(): Promise<object | null> {
  const items = await navigator.clipboard.read();
  for (const item of items) {
    if (item.types.includes('web application/json')) {
      const blob = await item.getType('web application/json');
      const text = await blob.text();
      return JSON.parse(text);
    }
  }
  return null;
}

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


Разрешения и политики безопасности

Permissions API

async function checkClipboardPermission(): Promise<boolean> {
  try {
    const result = await navigator.permissions.query({
      name: 'clipboard-read' as PermissionName
    });
    return result.state === 'granted';
  } catch {
    return false;
  }
}

Ограничения безопасности

Операция Требование Примечания
writeText Активация пользователя Нужен жест клика/клавиши
write Активация пользователя То же, что выше
readText Активация пользователя + разрешение Запрос авторизации пользователя
read Активация пользователя + разрешение Запрос авторизации пользователя
Окно в фокусе Документ должен быть в фокусе Вызовы в нефокусированном окне отклоняются

Проверка активации пользователя

button.addEventListener('click', async () => {
  // ✅ Внутри события клика — активация пользователя присутствует
  await navigator.clipboard.writeText('скопировано!');
});

setTimeout(async () => {
  await navigator.clipboard.writeText('ошибка'); // ❌ DOMException
}, 1000);

Резервный вариант: execCommand

function fallbackCopyText(text: string): boolean {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.style.position = 'fixed';
  textarea.style.left = '-9999px';
  document.body.appendChild(textarea);
  textarea.select();

  try {
    const success = document.execCommand('copy');
    document.body.removeChild(textarea);
    return success;
  } catch {
    document.body.removeChild(textarea);
    return false;
  }
}

async function safeCopy(text: string) {
  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch {}
  }
  return fallbackCopyText(text);
}

Глубокая обработка события вставки

Перехват и преобразование содержимого вставки

editor.addEventListener('paste', (e) => {
  e.preventDefault();
  const clipboardData = e.clipboardData!;

  const html = clipboardData.getData('text/html');
  if (html) {
    const sanitized = sanitizeHTML(html);
    insertHTML(sanitized);
    return;
  }

  const text = clipboardData.getData('text/plain');
  if (text) {
    const linked = text.replace(
      /https?:\/\/\S+/g,
      (url) => `<a href="${url}">${url}</a>`
    );
    insertHTML(linked);
  }
});

Определение типа данных буфера обмена

editor.addEventListener('paste', (e) => {
  const data = e.clipboardData!;
  console.log('Типы:', [...data.types]);

  const priorityOrder = [
    'text/html',
    'application/json',
    'text/plain',
    'image/png',
  ];

  for (const type of priorityOrder) {
    if (data.types.includes(type)) {
      const content = data.getData(type);
      console.log(`Вставка ${type}:`, content.slice(0, 100));
      break;
    }
  }
});

Практика: Компонент кнопки копирования

class CopyButton {
  private timeoutId: number | null = null;

  constructor(private button: HTMLButtonElement, private getText: () => string) {
    button.addEventListener('click', () => this.handleCopy());
  }

  private async handleCopy() {
    const text = this.getText();
    const success = await safeCopy(text);

    if (success) {
      this.showFeedback('Скопировано!');
    } else {
      this.showFeedback('Ошибка');
    }
  }

  private showFeedback(message: string) {
    const original = this.button.textContent;
    this.button.textContent = message;
    this.button.disabled = true;

    if (this.timeoutId) clearTimeout(this.timeoutId);
    this.timeoutId = window.setTimeout(() => {
      this.button.textContent = original;
      this.button.disabled = false;
    }, 2000);
  }
}

Совместимость браузеров

API Chrome Firefox Safari Edge
writeText 66+ 63+ 13.1+ 79+
readText 66+ 63+ 13.1+ 79+
write (ClipboardItem) 76+ 87+ 13.1+ 79+
read 76+ 87+ 13.1+ 79+
Web Custom Formats 104+ Нет Нет 104+
Буфер обмена изображений 76+ 87+ 13.1+ 79+

Лучшие практики

  1. Всегда предоставляйте резервный вариант: Используйте execCommand, если Clipboard API недоступна
  2. Вызывайте внутри активации пользователя: Копирование должно инициироваться в обработчиках клика/клавиши
  3. Всегда показывайте обратную связь: Визуальная обратная связь как при успехе, так и при неудаче
  4. Читайте с осторожностью: read() затрагивает конфиденциальность — запрашивайте только при необходимости
  5. Очищайте содержимое вставки: HTML-вставка должна фильтроваться от рисков XSS

Итоги

Async Clipboard API — это стандартный подход для современных операций с буфером обмена в браузере, поддерживающий чтение/запись текста, форматированного текста, изображений и пользовательских форматов. Надёжнее, безопаснее и функциональнее, чем execCommand, но требует внимания к активации пользователя и моделям разрешений.

Используйте Clipboard Viewer для отладки содержимого буфера обмена, Замена текста для пакетной обработки вставки и Изменение регистра для нормализации текста из буфера обмена.

Попробуйте эти локальные браузерные инструменты — регистрация не требуется →

#Clipboard API#剪贴板#异步API#浏览器安全#用户交互