Кодировка (encoding) и декодирование (decoding) в Base64 — распространенный способ преобразования двоичных данных в безопасный текст. Он часто используется в Data URL, таких как встроенные (inline) изображения. Прим. пер.: с помощью data URL можно решить проблему (ошибку) отсутствующей фавиконки в браузере. Что происходит при кодировке и декодировании в base64 строк в JS? В этой статье мы рассмотрим некоторые особенности и ловушки, связанные с этими процессами. Основными функциями для кодировки и декодирования строк в base64 в JS являются btoa() и atob(), соответственно. btoa() (binary to ASCII) кодирует строку в base64, а atob() (ASCII to binary) декодирует. Пример: К сожалению, как отмечается в статье MDN, это работает только со строками, содержащими символы ASCII или символы, которые могут быть представлены одним байтом. Другими словами, это не будет работать с Unicode. Рассмотрим следующий пример: Любое эмодзи в строке приводит к возникновению ошибки. Почему юникод вызывает проблемы? Рассмотрим, что такое строки в компьютерной науке и JS. Юникод — это современный стандарт кодировки символов, когда каждому символу присваивается определенное число для обеспечения возможности использования символов в компьютерных системах. Для более глубокого погружения в юникод взгляните на эту статью W3C. Примеры некоторых символов юникода и соответствующих им чисел: Числа, представляющие символы, называются "кодовыми точками" (code points). Они напоминают адреса символов. В эмодзи красного сердца на самом деле две кодовых точки: одна для сердца, вторая для "варианта" цвета — красного. Подробнее о селекторах варианта можно почитать здесь. Юникод может преобразовывать эти кодовые точки в последовательность байтов двумя способами: UTF-8 и UTF-16. В двух словах: Важно: JS всегда обрабатывает строки как UTF-16. Это ломает такие функции как btoa(), которые ожидают, что каждый символ строки будет использовать один байт. Цитата с MDN: Метод btoa() создает закодированную в Base64 строку ASCII из двоичной строки (т.е. строки, каждый символ которой рассматривается как байт двоичных данных). Теперь мы знаем, что символы в JS часто требуют больше одного байта. Но как быть с кодировкой и декодированием таких символов в base64? В ранее упоминавшейся статье на MDN имеется сниппет для решения "проблемы юникода": Вот что делает этот код: Процесс декодирования делает тоже самое, но в обратном порядке. Это работает, поскольку преобразование строки в Uint8Array гарантирует, что хотя строки в JS представлены как UTF-16, т.е. двумя байтами, кодовая точка, которую представляет каждый байт, всегда меньше 128. Это работает в большинстве случаев, но есть один нюанс. Рассмотрим следующий пример: Если мы возьмем последний символ после декодирования строки (�) и проверим его шестнадцатеричное (HEX) значение, то получим \uFFFD вместо оригинального \uDE75. Ошибки не возникло, но символ изменился. Почему? Как упоминалось ранее, JS обрабатывает строки как UTF-16. Но у таких строк есть одно уникальное свойство. Рассмотрим в качестве примера эмодзи сыра. Кодовой точкой юникода этого эмодзи (🧀) является 129472. Но максимальным значение 16-битного числа является 65535! Как представляются большие числа в UTF-16? Для этого используется концепция суррогатной пары (surrogate pair). Об этом можно думать следующим образом: Как вы понимаете, наличие книги без записи — это проблема. В UTF-16 это называется одиноким суррогатом (lone surrogate). Одни интерфейсы JS умеют работать с одинокими суррогатами, другие нет. В нашем случае для декодирования строки используется TextDecoder. В качестве второго опционального параметра конструктор TextDecoder() принимает объект с настройками, одной из которых является fatal — логическое значение, являющееся индикатором того, должен ли метод decode() выбрасывать TypeError при декодировании невалидных данных. По умолчанию эта настройка имеет значение false. Это означает, что по умолчанию искаженные (malformed) данные заменяются символом замены (replacement character). Символ �, представленный HEX-значением \uFFFD является символом замены. В UTF-16 строки с одинокими суррогатами считаются "искаженными" или "плохо сформированными". Существуют разные веб-стандарты (например, 1, 2, 3, 4), определяющие поведение интерфейса при обработке искаженных строк, и TextDecoder является одним из таких интерфейсов. Поэтому проверка правильной формы строки при обработке текста считается хорошей практикой. Для этой цели все современные браузеры предоставляют функцию String.isWellFormed() (поддержка — 79,65%). Того же можно добиться с помощью метода encodeURIComponent(), который выбрасывает URIError, если строка содержит одинокий суррогат. Следующая функция использует isWellFormed(), если она доступна, и encodeURIComponent() в противном случае: Полный сниппет кодировки и декодирования строк JS в base64 с учетом юникода и одиноких суррогатов выглядит следующим образом: JavaScript Developer