Кодирование и декодирование данных: TextEncoder, TextDecoder

В браузерных API работа с криптографией и сетевыми протоколами почти всегда опирается на бинарные данные: ArrayBuffer и типизированные представления вроде Uint8Array. Однако большинство данных в веб-приложениях изначально существует в виде строк. Для корректного преобразования между строками и байтовыми последовательностями используются TextEncoder и TextDecoder — базовые инструменты кодирования текста в UTF-8 и обратно.

Строка в JavaScript — это последовательность символов в формате UTF-16. Криптографические алгоритмы Web Crypto API не работают со строками напрямую. Например, методы crypto.subtle.digest, encrypt, sign принимают данные только в виде ArrayBuffer или ArrayBufferView.

Это приводит к необходимости преобразования:

  • строка → байты (при отправке в криптографические функции)
  • байты → строка (при отображении результата пользователю)

Именно эту задачу решают TextEncoder и TextDecoder.


TextEncoder: преобразование строки в байты

TextEncoder кодирует строку в UTF-8 и возвращает Uint8Array.

Основной сценарий использования — подготовка данных для хеширования, подписи или шифрования.

const encoder = new TextEncoder();

const text = "Пример строки";
const bytes = encoder.encode(text);

console.log(bytes);
// Uint8Array([...])

Особенности TextEncoder

  • Всегда использует UTF-8
  • Не требует указания кодировки
  • Игнорирует суррогатные ошибки UTF-16
  • Возвращает Uint8Array поверх ArrayBuffer

Важно учитывать, что UTF-8 является стандартом де-факто для Web Crypto API, поэтому TextEncoder идеально согласуется с crypto.subtle.


Применение с Web Crypto API

Типичный пример — вычисление SHA-256 хеша:

const encoder = new TextEncoder();

const data = encoder.encode("секретное сообщение");

const hashBuffer = await crypto.subtle.digest("SHA-256", data);

console.log(new Uint8Array(hashBuffer));

Здесь критически важно, что digest принимает именно бинарные данные. Передача строки невозможна.


TextDecoder: преобразование байтов в строку

TextDecoder выполняет обратную операцию — преобразует бинарные данные в строку.

const decoder = new TextDecoder();

const bytes = new Uint8Array([208, 159, 209, 128, 208, 184, 208, 188, 208, 181, 209, 128]);
const text = decoder.decode(bytes);

console.log(text);
// "Пример"

Поддерживаемые кодировки

Хотя по умолчанию используется UTF-8, TextDecoder поддерживает и другие кодировки:

const decoder = new TextDecoder("utf-8");

Дополнительно возможны:

  • utf-8 (основной)
  • utf-16le / utf-16be (частично поддерживаются)
  • windows-1251 (в некоторых окружениях)

Однако для Web Crypto API практически всегда используется UTF-8.


Потоковое декодирование

TextDecoder умеет работать с частичными данными, что важно при обработке потоков.

const decoder = new TextDecoder("utf-8", { stream: true });

let chunk1 = decoder.decode(new Uint8Array([208, 159, 208]));
let chunk2 = decoder.decode(new Uint8Array([184, 208, 188, 0]), { stream: false });

console.log(chunk1 + chunk2);

Это полезно при:

  • загрузке данных по частям (fetch streaming)
  • обработке WebSocket сообщений
  • работе с криптографическими потоками

Разница между encode и decode

TextEncoder и TextDecoder образуют симметричную пару, но работают с разными уровнями представления данных:

Операция Вход Выход
encode string Uint8Array
decode Uint8Array / ArrayBuffer string

Ключевая особенность — отсутствие потерь при корректной кодировке UTF-8.


Взаимодействие с ArrayBuffer

Web Crypto API использует ArrayBuffer как основной контейнер данных. Поэтому часто требуется преобразование Uint8Array → ArrayBuffer:

const encoder = new TextEncoder();
const bytes = encoder.encode("данные");

const buffer = bytes.buffer;

Однако важно учитывать, что .buffer может включать лишний диапазон памяти, если Uint8Array — это view над частью буфера.

Более безопасный способ:

const buffer = encoder.encode("данные").slice().buffer;

Обратное преобразование ArrayBuffer → строка

const decoder = new TextDecoder();

function bufferToString(buffer) {
  return decoder.decode(new Uint8Array(buffer));
}

Это особенно часто используется при:

  • расшифровке данных (decrypt)
  • получении подписи
  • обработке хешей в читаемом виде

Пример полного цикла с Web Crypto API

const encoder = new TextEncoder();
const decoder = new TextDecoder();

const message = "сообщение для хеширования";

const data = encoder.encode(message);

const hash = await crypto.subtle.digest("SHA-256", data);

const hashArray = Array.from(new Uint8Array(hash));
const hex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");

console.log(hex);

Здесь происходит полный цикл:

  • строка → байты
  • байты → криптографическая операция
  • результат → человекочитаемый формат

Ошибки при работе с кодировкой

Распространённые проблемы:

Потеря символов

Происходит при неправильной кодировке или попытке интерпретировать UTF-8 как ASCII.

Неправильное использование ArrayBuffer

Передача строки вместо Uint8Array приводит к исключению в crypto.subtle.

Игнорирование многобайтовых символов

UTF-8 кодирует символы кириллицы и emoji в несколько байт, что важно при расчётах длины данных.


Особенности производительности

TextEncoder и TextDecoder реализованы на уровне движка браузера и работают значительно быстрее ручного преобразования через циклы JavaScript.

Оптимизации:

  • минимизация повторных вызовов encode/decode
  • повторное использование экземпляров encoder/decoder
  • работа с уже подготовленными Uint8Array

Использование в криптографических протоколах

В реальных сценариях Web Crypto API почти всегда комбинируется с TextEncoder:

  • HMAC подписи
  • PBKDF2 ключи
  • AES-GCM шифрование
  • RSA-OAEP операции

Пример подготовки ключевого материала:

const encoder = new TextEncoder();

const password = encoder.encode("пароль пользователя");

const key = await crypto.subtle.importKey(
  "raw",
  password,
  "PBKDF2",
  false,
  ["deriveKey"]
);

Работа с emoji и многобайтовыми символами

UTF-8 корректно кодирует emoji и сложные символы:

const encoder = new TextEncoder();
const decoder = new TextDecoder();

const text = "ключ ?";

const encoded = encoder.encode(text);
const decoded = decoder.decode(encoded);

console.log(decoded);

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


Сравнение с альтернативными подходами

До появления TextEncoder/TextDecoder разработчики использовали:

  • String.charCodeAt
  • ручные преобразования байтов
  • библиотеки вроде Buffer (Node.js)

Эти подходы:

  • медленнее
  • менее безопасны
  • плохо работают с Unicode

Современный стандарт полностью заменяет их в браузере.


Интеграция с потоками и fetch

При работе с fetch API данные часто приходят в бинарном виде:

const response = await fetch("/data");
const buffer = await response.arrayBuffer();

const text = new TextDecoder().decode(buffer);

Или наоборот:

const encoder = new TextEncoder();

await fetch("/api", {
  method: "POST",
  body: encoder.encode("payload")
});

Важные ограничения

  • TextEncoder всегда использует UTF-8 и не поддерживает выбор кодировки
  • TextDecoder может работать некорректно с нестандартными кодировками в некоторых окружениях
  • бинарные данные нельзя интерпретировать как строку без явного декодирования

Роль в архитектуре Web Crypto API

TextEncoder и TextDecoder являются связующим слоем между:

  • человекочитаемым текстом
  • бинарными структурами криптографии
  • сетевыми протоколами

Без этих инструментов большинство операций Web Crypto API было бы невозможно в чистом виде, так как API строго типизирован под бинарные входные данные.