Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Кодирование Base64 в JavaScript/Node.js: полное руководство

У вас есть данные, которым нужно стать текстом. Файл, который должен ехать внутри JSON-поля, изображение, которое хочет жить в CSS-файле, секрет, который будет сидеть в переменной окружения, токен, который будет путешествовать по строке запроса. Ответ в JavaScript и Node.js почти всегда один и тот же: Base64. Эта статья - инструкция по упаковке, от первого байта, который у вас в руках, до момента, когда ваша закодированная строка покидает машину.

Главная страница этого сайта подробно объясняет формат, алфавит, математику, заполнение, поэтому здесь это одним предложением: каждые три байта превращаются в четыре печатаемых символа, отсюда и ваш вывод примерно на 33 процента больше входа. Держите это в кармане, потому что в этом причина существования каждого раздела этой статьи, и именно в этих числах рассчитывается ваш счёт за хранение.

Утешительная часть: устанавливать нечего. Каждый современный браузер отдаёт btoa() и поновее Uint8Array.toBase64(), а каждая Node.js-версия, которая имеет значение, несёт класс Buffer с режимом 'base64' и, начиная с версии 15.7.0, режимом 'base64url' первого класса. Искусство - в том, чтобы знать, какая форма входа у вас в руках, какой алфавит требует пункт назначения и какие правила переноса строк старые форматы по-прежнему применяют.

Узнайте свой вход, прежде чем кодировать

Каждый вопрос о кодировании начинается с одного и того же: что именно у вас в руках? Строка JavaScript - это UTF-16-текст, Buffer - это массив байтов, и правильный вызов зависит от того, какой из них у вас есть:

Что у вас в руках Вызывайте это Заметки
Строка только из ASCII (символы до 256) btoa(string) Самый быстрый путь в браузерах и Node.js 16+, но он останавливается на первом символе, который не влезает в байт
Любая Unicode-строка TextEncoder в байты, затем base64-вызов UTF-8-мост; единственный безопасный путь для букв с диакритикой и эмодзи
Buffer или Uint8Array buffer.toString('base64') или bytes.toBase64() Рабочая лошадка Node.js, а также метод ES2026 в современных браузерах и Node.js 25+

Три примера, по одному на строку таблицы:

// Только ASCII-текст: легаси-сокращение (браузеры и Node.js 16+)
console.log(btoa('hello world')); // "aGVsbG8gd29ybGQ="
// Любой текст в Node.js: Buffer читает UTF-8 по умолчанию
const { Buffer } = require('node:buffer');
console.log(Buffer.from('héllo ⛳', 'utf8').toString('base64')); // "aMOpbGxvIOKbsw=="
// Байты, которые у вас уже есть
console.log(Buffer.from([1, 2, 3, 4]).toString('base64')); // "AQIDBA=="
console.log(new Uint8Array([1, 2, 3, 4]).toBase64()); // "AQIDBA==" (ES2026-рантаймы)

Обратите внимание на второй пример: один и тот же текст даёт разную Base64-строку в зависимости от кодировки, в которой вы его кодируете. Это не баг - это и есть вся игра. Слой Base64 кодирует байты, а строка становится байтами только тогда, когда вы выбрали кодировку, поэтому «закодируйте этот текст» всегда молча означает «закодируйте UTF-8-байты этого текста» (или Latin-1-байты, если вы скажете об этом).

Unicode-стена и мосты через неё

btoa() - самое старое API в комнате, и её договор - девяностые: каждый символ входной строки должен влезать в один байт, кодовые точки от 0 до 255. Всё, что выше, - эмодзи, кириллическая буква с диакритикой, китайский иероглиф, - бросает исключение:

try {
  btoa('héllo ⛳');
} catch (error) {
  console.log(error.name); // "InvalidCharacterError"
  console.log(error.message); // «Invalid character» в Node; в браузерах формулировка про Latin1-диапазон
}

Исправление - перестать думать символами и начать думать байтами. TextEncoder (глобальная переменная в каждом браузере и в Node.js) превращает строку в её UTF-8-последовательность байтов; вы поднимаете эти байты в Latin-1 строку, и btoa() получает ровно то, что обещала обрабатывать:

function encodeUnicode (text) {
  const bytes = new TextEncoder().encode(text);
  let binary = '';
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  return btoa(binary);
}
console.log(encodeUnicode('héllo ⛳')); // "aMOpbGxvIOKbsw=="
console.log(encodeUnicode('héllo ⛳') === Buffer.from('héllo ⛳', 'utf8').toString('base64')); // true, те же байты

В кодовых базах вы также встретите более старый идиом, и он работает тем же самым подкапотным способом: btoa(unescape(encodeURIComponent(text))). Вызов encodeURIComponent производит процентно-кодированные UTF-8-байты, а unescape превращает процентные экранировки обратно в сырые символы. И escape, и unescape - легаси-функции, поэтому новый код должен предпочитать мост TextEncoder, но когда вы наследуете старую форму, теперь вы точно знаете, что она делает, вместо того чтобы пожимать плечами.

В Node.js стена в основном не событие, потому что Buffer.from(text) предполагает UTF-8 и делает побайтовое преобразование за вас в том же вызове. Мост важнее всего в браузере, где btoa() - легаси-опция, а шаг UTF-8 - ваш, и его нужно сделать явно.

Байты на входе, буквы на выходе: Buffer, заполнение и разновидности

Как только у вас есть байты, кодирующая сторона Node.js - это один метод: toString('base64'). Он справляется с групповой математикой, с заполнением, со всем, и всегда производит канонический вывод в смысле RFC 4648, то есть неиспользуемые биты заполнения последней группы - нули:

const { Buffer } = require('node:buffer');
const fox = Buffer.from('The quick brown fox jumps over the lazy dog');
console.log(fox.length); // 43 байта
console.log(fox.toString('base64')); // "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=="
console.log(fox.toString('base64').length); // 60 символов, 33-процентный налог в действии

Заполнение в конце делает реальную работу, а не служит украшением. Последняя группа с одним оставшимся байтом становится двумя Base64-символами плюс двумя =, а группа с двумя оставшимися байтами - тремя символами плюс одним =. Несёт ли ваш вывод это заполнение, зависит от пункта назначения, и именно в этом различие между двумя разновидностями Base64, которыми вы будете пользоваться каждый день:

const one = new Uint8Array([72]);
console.log(one.toBase64()); // "SA==" (ES2026, заполнитель включён)
console.log(one.toBase64({ omitPadding: true })); // "SA"
console.log(Buffer.from([72]).toString('base64url')); // «SA», Node отбрасывает заполнитель в режиме base64url

Помните пропорцию, когда прикидываете размеры: три байта на входе, четыре символа на выходе, так что 1 МБ данных превращается примерно в 1.33 МБ текста, а если вы ещё и заворачиваете текст в строки для почты или PEM, переводы строк добавляют сверху несколько процентов.

Отправляем байты в эфир

Самая частая проблема провода в JavaScript - то, что в JSON нет байтов. В нём есть строки, и строка, которая может безопасно проехать через любой JSON-парсер, любой HTTP-прокси и любую лог-систему, - это Base64-строка. Паттерн один и тот же на обоих концах соединения: кодируйте на границе, декодируйте на границе, держите байты посередине:

const fs = require('node:fs');
const photo = fs.readFileSync('./photo.jpg', 'base64');
const payload = JSON.stringify({
  name: 'photo.jpg',
  contentType: 'image/jpeg',
  data: photo
});
console.log(payload.startsWith('{"name":"photo.jpg"')); // true, файл теперь едет внутри обычного JSON

Знайте, когда стоит поспорить с этим паттерном. Если ваш транспорт уже поддерживает бинарные данные - пользуйтесь: загрузка multipart/form-data отправляет сырой файл без налога за размер, WebSocket-фрейм несёт сырые байты, а колонка Postgres bytea хранит их нативно. Base64 там, где были разрешены сырые байты, - чистый оверхед: 33-процентный налог без какой-либо выгоды взамен. Base64 оправдывает себя, когда канал только текстовый: JSON API, тела писем, переменные окружения, строки запроса URL и множество мостов (мобильные SDK, десктопные приложения, чат-системы), которые пускают только текст.

Data URL: картинки, живущие в тексте

Data URL - строка data:image/png;base64,... - это Base64 в MIME-ярлыке, и именно поэтому вы можете уместить целое изображение в одном HTML-атрибуте. RFC 1998 года, определивший эту схему, прямо говорит, что она «полезна только для коротких значений», потому что в раннем HTML на значения атрибутов действовал лимит в 1024 символа. Современные браузеры смеются над этим лимитом и с удовольствием отрисовывают data URL размером в мегабайты, - а это и суперсила, и ловушка.

В браузере canvas API делает всю работу за вас: пиксели на входе, data URL на выходе:

const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const context = canvas.getContext('2d');
context.fillStyle = '#ff0000';
context.fillRect(0, 0, 1, 1);
const dataUrl = canvas.toDataURL('image/png'); // "data:image/png;base64,iVBOR..."
console.log(dataUrl.slice(0, 24)); // "data:image/png;base64,iV"

А в любом рантайме, включая Node.js, сборка одного - это просто конкатенация строк с метаданными на правильном месте: префикс data:, медиатип, необязательный маркер ;base64, запятая и пелод. Без маркера ;base64 пелод ожидается как процентно-кодированный текст, - и в этом причина, по которой маркер существует:

const { Buffer } = require('node:buffer');
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB', 'base64');
const dataUrl = 'data:image/png;base64,' + png.toString('base64');
console.log(dataUrl.startsWith('data:image/png;base64,')); // true

Честные издержки: data URL - часть документа, поэтому его нельзя кэшировать как отдельный ресурс, он учитывается в размере HTML или CSS, в котором он сидит, и DOM должен разобрать его и удержать в памяти. Для иконки в 20 килобайт это выгодная сделка. Для 4-мегабайтного изображения-героя отправляйте файл по HTTP, где работают и кэширование, и сжатие, и держите data URL для мелочей.

Файлы, которые едут строками

Файл в Base64 - это двухшаговый танец, который оба рантайма сжимают в один вызов. В Node.js файловая система принимает 'base64' как кодировку чтения, и сторона записи тоже:

const fs = require('node:fs');
const { Buffer } = require('node:buffer');
const base64 = fs.readFileSync('./report.pdf', 'base64');
console.log(base64.length); // файл, примерно на 33 процентов тяжелее
fs.writeFileSync('./report.pdf.b64', base64, 'utf8');
const copy = Buffer.from(base64, 'base64');
fs.writeFileSync('./report.copy.pdf', copy);

В браузере FileReader делает ту же работу, с одним поворотом: его режим чтения данных выдаёт вам data URL, поэтому вы срезаете префикс, чтобы получить голый Base64-пелод:

const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', () => {
  const reader = new FileReader();
  reader.onload = () => {
    const dataUrl = reader.result; // "data:application/pdf;base64,..."
    const payload = {
      name: fileInput.files[0].name,
      data: dataUrl.slice(dataUrl.indexOf(',') + 1)
    };
    console.log(payload.data.length); // файл, готовый к JSON-запросу
  };
  reader.readAsDataURL(fileInput.files[0]);
});

Если в браузере вам нужен сырой Base64 без префикса data URL, file.arrayBuffer() с последующим Uint8Array.toBase64() (на рантаймах, где он есть) пропускает префикс целиком и является более чистым путём для загрузочных конвейеров.

base64url: алфавит, который выживает в URL

Классический Base64 несёт два символа, у которых аллергия на URL. + становится пробелом всякий раз, когда строку запроса декодируют по правилам формы, / - это разделитель путей, а заполнение = выглядит как присваивание. Вариант из раздела 5 RFC 4648, безопасный для URL и имён файлов, - base64url, - заменяет два спецсимвола на - и _ и отбрасывает заполнитель, когда длина известна по контексту. Это алфавит JWT, OAuth-токенов и глубоких ссылок, и он заслуживает отдельного места в вашем ментальном наборе инструментов.

Buffer в Node говорит на этом диалекте с версии 15.7.0, и кодирующая сторона - один аргумент:

const { Buffer } = require('node:buffer');
const classic = 'k+XS/B4=';
console.log(Buffer.from(classic, 'base64').toString('base64url')); // "k-XS_B4"

Две вещи заметить. + стал -, / стал _, а заполнение исчезло, потому что режим base64url по замыслу его пропускает. И IETF прямо говорит, что это другое кодирование, а не то же самое в костюме, так что когда спецификация говорит «base64url», вы должны производить base64url, а не классический Base64, переправленный поиском и заменой. Метод ES2026 превращает те же выборы в явные опции, и его флаг omitPadding возвращает заполнитель, когда контекст его требует:

console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url', omitPadding: true })); // "q_8"
console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url' })); // «q_8=», двум байтам нужен один символ заполнения

На рантаймах без обоих, преобразование - это замена двух символов плюс срезка заполнителя, и это один из самых копируемых кусочков в мире JavaScript:

const toUrlSafe = (value) => value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
console.log(toUrlSafe('k+XS/B4=')); // "k-XS_B4"

Используйте base64url для всего, что будет жить в URL, в строке запроса, в имени файла или в стандарте токенов. Используйте классический Base64 для MIME-тел, data URL и всего, что никогда не встретит процентный декодер. Смешивать их - самая частая ошибка совместимости во всём этом формате.

Упаковываем учётные данные: Basic-аутентификация, JWT и PKCE

Три уголка аутентификации веба построены на Base64, и все три дёшево построить руками один раз, - а это хорошо, потому что знание того, что происходит под библиотекой, и сохраняет вам спокойствие, когда библиотека вас удивляет.

Во-первых, HTTP-аутентификация Basic (RFC 7617): клиент отправляет слово схемы Basic плюс Base64 от user-id:password. Одна строка, и к ней прилагается одно серьёзное предупреждение:

const { Buffer } = require('node:buffer');
console.log('Basic ' + Buffer.from('octo:cat').toString('base64')); // "Basic b2N0bzpjYXQ="

Base64 здесь - обфускация, а не безопасность. Кто угодно, кто может прочитать заголовок, может прочитать пароль, поэтому схема приемлема только поверх HTTPS, и даже тогда это легаси-паттерн: предпочитайте токены. Во-вторых, JWT: первые две части, разделённые точками, - это base64url от обычного JSON, а третья - подпись. Собрать токен HMAC-SHA256 руками - это горсть строк встроенного модуля crypto:

const crypto = require('node:crypto');
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const payload = Buffer.from(JSON.stringify({ sub: 'octocat', exp: 1893456000 })).toString('base64url');
const signature = crypto.createHmac('sha256', 'topsecret').update(header + '.' + payload).digest('base64url');
const token = header + '.' + payload + '.' + signature;
console.log(token.split('.').length); // 3 части, base64url без заполнителя по всему ходу

Обратите внимание на детали, которые решают судьбу токена: заполнителя нет нигде (RFC 7515 его опускает), подпись вычисляется по буквальному значению строки header + '.' + payload, а не по разобранным объектам, и всё вместе столь же секретно, насколько секретен ключ. В продакшене вы будете использовать библиотеку, jose (ноль зависимостей, браузер и Node.js) или jsonwebtoken (Node.js), но под капотом они прогоняют именно эти вызовы. В-третьих, PKCE (RFC 7636), расширение, которое позволяет публичным клиентам вроде SPA и мобильных приложений входить в систему безопасно: клиент генерирует code_verifier с высокой энтропией, публикует BASE64URL(SHA256(verifier)) в качестве challenge и доказывает владение verifier'ом при обмене на токен. Случайность важна, поэтому verifier берётся из криптографического модуля, никогда не из Math.random():

const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
console.log(verifier.length, challenge.length); // 43 43, оба в разрешённом диапазоне 43-128

Старая почта требует обёртки: MIME и PEM-броня

Два из старейших в мире форматов Base64 до сих пор настаивают на длине строк, и обоим около 30 лет. MIME, стандарт электронной почты из RFC 2045, заворачивает свой Base64 по 76 символов в строку и требует, чтобы строки заканчивались CRLF, - реликт дней 8-битно-чистого SMTP, когда очень длинные строки ломали настоящие почтовые серверы. RFC 7468, записывающий правила PEM для сертификатов и ключей, ещё строже: генераторы обязаны заворачивать ровно по 64 символа в строку, последняя строка короче, а в качестве рамки - броневые строки -----BEGIN и -----END, называющие содержимое.

Само обёртывание - это одна строка кода, а броня - шаблон:

const wrap = (base64, width) => base64.match(new RegExp('.{1,' + width + '}', 'g')).join('\r\n');
const certBase64 = Buffer.from('x'.repeat(150), 'utf8').toString('base64'); // 200 символов
console.log(wrap(certBase64, 76).split('\r\n').map((line) => line.length).join(', ')); // "76, 76, 48"
console.log(wrap(certBase64, 64).split('\r\n').map((line) => line.length).join(', ')); // "64, 64, 64, 8"
const armor = (label, body) => '-----BEGIN ' + label + '-----\r\n' + wrap(body, 64) + '\r\n-----END ' + label + '-----\r\n';
console.log(armor('CERTIFICATE', 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo='));
// -----BEGIN CERTIFICATE-----
// QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=
// -----END CERTIFICATE-----

Две практические заметки. Когда вы производите MIME или PEM, заворачивайте, потому что строгие потребители (почтовые шлюзы, инструменты эпохи OpenSSL, Java-хранилища ключей) отклонят однострочный Base64-блоб в 4000 символов. Когда вы потребляете, обычно заворачивать не нужно, потому что декодер Node пропускает пробельные символы, и Buffer.from справляется с переводами строк за вас, - но сами броневые строки - это не Base64, поэтому перед декодированием отбрасывайте строки -----BEGIN/-----END (или совпадайте только с телом): Buffer.from(pem.replace(/-----[A-Z ]+-----/g, ''), 'base64'). Эта асимметрия - подарок, но она не означает, что можно пропустить снятие брони, когда Base64 едет туда, где ничего не пропускают, - например, в DER-парсер.

Где живёт закодированные данные: переменные окружения, конфиги и базы данных

Base64 - ещё и формат хранения, что одновременно удобно и опасно просто потому, что его легко принять за безопасность. Переменные окружения - классический дом: несколько менеджеров секретов и CI-системы вручают вам закодированные в Base64 значения, и декодирование - одна строка:

const { Buffer } = require('node:buffer');
const stored = process.env.API_KEY_B64; // "c3VwZXItc2VjcmV0"
console.log(Buffer.from(stored, 'base64').toString('utf8')); // "super-secret"

Скажите важную фразу вслух: кодирование - это не шифрование. Base64-«секрет» в переменной окружения, в файле .env или в секрете Kubernetes (k8s хранит свои секреты в Base64 и в API, и в etcd, и документация постоянно это повторяет) - читаем для любого, кто может прочитать окружение процесса, файл или кластер. Используйте там Base64, потому что транспорт (командная оболочка, YAML, JSON) только текстовый, и никогда не потому, что вы верите, что он что-то прячет.

В базах данных Base64 - стандартный мост для бинарных данных внутри JSON-документных хранилищ, потому что у колонки jsonb или документа MongoDB нет собственного байтового типа:

const document = {
  name: 'logo',
  mime: 'image/png',
  data: Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64')
};
console.log(JSON.stringify(document)); // {"name":"logo","mime":"image/png","data":"iVBORw=="}

Храните медиатип рядом с пелодом, как это делает пример, и через год вы скажете себе спасибо, когда кто-то спросит, что это за байты. Если в вашей базе есть нативный бинарный тип (Postgres bytea - эталонный пример), предпочитайте его: байты ничего не стоят дополнительно, и вы навсегда обходите 33-процентный налог.

Кодирование потоков без разрыва групп

Base64 работает группами по три байта, поэтому кодировщик, принимающий произвольные чанки, должен переносить остаток: один или два байта, которые пока не могут составить группу, должны дождаться следующего чанка, прежде чем их можно закодировать. Сделайте математику на каждый чанк и выпускайте только полные группы, и вывод будет побайтово идентичен кодированию всего потока за один раз:

const { Transform } = require('node:stream');
const { Buffer } = require('node:buffer');
function base64Encoder () {
  let pending = Buffer.alloc(0);
  return new Transform({
    transform (chunk, _encoding, done) {
      pending = Buffer.concat([pending, chunk]);
      const whole = Math.floor(pending.length / 3) * 3;
      this.push(pending.subarray(0, whole).toString('base64'));
      pending = pending.subarray(whole);
      done();
    },
    flush (done) {
      if (pending.length > 0) {
        this.push(pending.toString('base64'));
      }
      done();
    }
  });
}
let output = '';
const encoder = base64Encoder();
encoder.on('data', (part) => { output += part; });
encoder.on('end', () => {
  console.log(output); // «aGVsbG8gd29ybGQsIHRoaXMgaXMgYSBzdHJlYW0h»; идентично одному большому toString('base64')
});
encoder.end(Buffer.from('hello world, this is a stream!'));

Колбэк flush - это деталь, которую все забывают: последние один или два байта, которые так и не нашли пару в обычном чанке, получают своё заполнение и выталкиваются в конце. Та же логика переноса, которую вы будете повторять на стороне декодирования, за исключением того, что там API ES2026 даёт её вам бесплатно: setFromBase64() с "stop-before-partial" останавливается ровно на границах групп и говорит, сколько символов потребило.

Большие файлы и счёт за память

Base64 щедр в отношении места, поэтому большим файлам нужна стратегия. Файл в 1 ГБ превращается примерно в 1.33 ГБ Base64-текста, а строка JavaScript хранит UTF-16, два байта кучи на символ, так что только этот текст требует примерно 2.7 ГБ памяти, ещё до появления вашего Buffer. Потолок в Node прописан явно: buffer.constants.MAX_STRING_LENGTH - это 536870888 символов, чуть меньше 512 МиБ текста, что декодируется примерно в 400 МБ байтов. Дальше одна строка - не вариант, и потоковая обработка - единственная игра в городе:

const fs = require('node:fs');
const { Buffer } = require('node:buffer');
let carried = Buffer.alloc(0);
const source = fs.createReadStream('./video.mp4', { highWaterMark: 64 * 1024 });
source.on('data', (chunk) => {
  const joined = Buffer.concat([carried, chunk]);
  const whole = Math.floor(joined.length / 3) * 3;
  process.stdout.write(joined.subarray(0, whole).toString('base64'));
  carried = joined.subarray(whole);
});
source.on('end', () => {
  if (carried.length > 0) {
    process.stdout.write(carried.toString('base64'));
  }
  process.stdout.write('\n');
});

Паттерн - это поточный кодировщик из предыдущего раздела, сплюснутый: читайте чанками по 64 КиБ, переносите остаток в 1-2 байта, выпускайте полные группы, проливайте хвост. След памяти держится примерно в одном чанке плюс один остаток, какой бы файл ни весил. А если принимающая сторона способна принять бинарные данные, спросите себя, зачем вы вообще платите этот налог.

Однострочники для терминала

Node по совместительству - командно-строковый Base64-кодировщик, и это удобно, когда вы упаковываете значение конфигурации, отлаживаете API или переносите маленький файл между машинами через сообщение в чате:

# Кодируем файл в классический Base64 в stdout
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64"))' notes.txt
# Вариант, безопасный для URL, заполнение отброшено
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64url"))' notes.txt
# Читаем из stdin, для этого и нужны конвейеры
echo -n "hello world" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>process.stdout.write(Buffer.from(d,"utf8").toString("base64")))'

Ни один из трёх не добавляет собственный завершающий перевод строки, и это держит вывод чистым для копирования и для подстановки $(...) в скриптах командной оболочки. Если вы хотите красиво напечатанный файл с обёрнутыми строками, прогоните результат через свой любимый редактор или добавьте \n в конец однострочника.

Ловушки, которые обходятся разработчикам в часы

Каждая из них стоила реального послеполудня в реальной кодовой базе:

  • Unicode-стена: btoa('héllo ⛳') бросает InvalidCharacterError, потому что флаг для гольфа не влезает в байт. Исправление - UTF-8-мост: сначала TextEncoder в байты, затем кодирование. В Node.js вы полностью обходите проблему с Buffer.from(text), который предполагает UTF-8.
  • Легаси-идиом: старый код, полный btoa(unescape(encodeURIComponent(x))), работает, но escape и unescape - отменённые легаси-функции. Когда рефакторите этот код, замените его на мост TextEncoder, и поведение останется идентичным.
  • Пропущенный аргумент кодировки, но в обратную сторону: ловушка на стороне декодирования - Buffer.from(str) без режима 'base64'; её двойник на стороне кодирования - предположение, что Buffer.from(someString) делает с Base64 что-то особенное. Не делает. Без явной кодировки он строит Buffer из UTF-8-байтов строки, и ваш «закодированный» вывод - это Base64 от буквенных байтов строки, что почти никогда не было тем, что хотели. Будьте явны в обоих направлениях.
  • Несовпадение заполнения: JWT и большинство стандартов токенов хотят base64url без заполнения, MIME хочет классический Base64 с заполнением, и легко перепутать одно с другим. = с заполнителем внутри части JWT ломает строгие верификаторы; отсутствующий заполнитель там, где длина неизвестна, ломает ленивые декодеры. Следуйте стандарту, а не привычке.
  • Неканонический хвост: RFC 4648 требует, чтобы неиспользуемые биты заполнения последней группы были нулями. Все встроенные кодировщики производят канонический вывод, но самописный кодировщик, который сдвигает биты вручную, может оставить в этих битах мусор, и строгий декодер отклонит вашу пелод без видимой причины. Если пишете свой кодировщик, тестируйте на тестовых векторах RFC 4648, а не только на своих данных.
  • Забытый перенос строк: MIME хочет строки по 76 символов, PEM - по 64, и строгие потребители (почтовые шлюзы, Java-инструменты для ключей) отклонят однострочный блоб. Обратное случается реже, но бывает: некоторые парсеры ориентированы на строки, и отсутствующий CRLF в конце PEM-файла ломал больше сборок, чем любой баг в самом Base64.
  • Иллюзия безопасности: Base64 в переменной окружения, в файле .env или в секрете Kubernetes - не шифрование. Он декодируется одной строкой кода, на любом языке, любым, кто может прочитать файл или кластер. Относитесь к нему как к транспортному костюму, и пусть настоящие средства контроля (права доступа, TLS, ротация ключей) занимаются защитой.
  • JSON-разбухание: Base64 внутри JSON стоит 33 процента плюс экранирование, и загрузка в 5 МБ становится строкой в 6.7 МБ, которую ваш JSON-парсер должен скопировать в память. Для всего, что по размеру файла, по HTTP, multipart/form-data или сырое бинарное тело - лучший транспорт, а Base64 - для случаев, когда канал только текстовый.
  • Счёт за кучу: закодированная строка - UTF-16 в куче JavaScript, два байта на символ, и декодированный или исходный Buffer - вторая копия данных. Файл в 100 МБ на короткое время означает примерно 270 МБ строки плюс 100 МБ Buffer. Перекачивайте большие, и держите закодированную форму в ссылках как можно короче, насколько позволяет код.
  • Легаси-глобалы в Node: собственная документация Node помечает btoa() и atob() как Stability 3, Legacy, и советует использовать Buffer. В браузере btoa() - вполне годный инструмент для ASCII-текста; в Node.js берите Buffer и оставьте глобалы полифилл-образному коду, которому они нужны.

Как JavaScript научился упаковывать байты

Браузерная сторона - долгая, тихая история. btoa() была описана в черновике HTML5 в начале 2011 года, и с середины 2000-х сидит во всех крупных браузерах, без изменений в поведении, со своим договором «один байт на символ» и всегда выравниваемым выводом. Тот договор старше типизированных массивов - бинарные строки были единственным способом нести байты до 2009 года, - поэтому btoa() до сих пор думает «бинарными строками». Современная половина истории очень свежая: предложение TC39, добавившее нативный Base64 в типизированные массивы (вместе с hex), стало стандартом как часть ES2026, и приземлилось в Firefox 133 и Safari 18.2 в 2024 году, в Chrome 140 2 сентября 2025 года, после чего было объявлено Baseline Newly available. Bun поставил те же методы в версии 1.1.22 в августе 2024 года.

Node.js упаковывал байты по другим часам. Класс Buffer стал глобальным в версии 0.1.103, летом 2010 года, почти за пять лет до Node 1.0, и toString('base64') был кодировщиком выбора более десяти лет, с алфавитными причудами той эпохи (при декодировании он уже принимал URL-безопасные символы, - двуязычная привычка, о которой спецификация никогда не просила). Версия 15.7.0 в январе 2021 года добавила режим 'base64url' как имя кодирования первого класса, Node 16 в том же году добавил браузерные глобалы btoa()/atob() (немедленно помеченные Legacy), а Node 22 в 2024 году выкатил дальнейшую работу по производительности V8 и base64. Потом Node 25, вышедший 15 октября 2025 года, обновил V8 до 14.1 и принёс в рантайм методы ES2026, toBase64() с его опцией omitPadding и setFromBase64() для обратного направления. Для рантаймов, которые не поспевают, core-js поставляет полифиллы (features/typed-array/to-base64 / from-base64), а крошечный пакет base64-js (три функции, ноль зависимостей) годами тихо нёс экосистему в качестве транзитивной зависимости.

Формат, которому они служат, старше всего этого. Алфавит впервые стандартизировали для Privacy-Enhanced Mail в 1987 году (RFC 989), пересмотр 1993 года (RFC 1421) сохранил его, а MIME принял его спустя несколько месяцев тем же годом, со своим 76-символьным переносом; RFC 3548 в 2003 году собрал семейство Base-N и добавил URL-безопасный вариант, который RFC 4648 переиздал в 2006-м. Десять лет спустя RFC 7515 и 7519 сделали base64url без заполнителя опорой каждого JWT, а RFC 7636 поставил его в PKCE-поток OAuth. Кодировщики из этой статьи - последний километр формата, которому тридцать лет и который всё ещё набирает пассажиров.

Стоит знать на вечеринке

  • btoa('GIF89a') возвращает "R0lGODlh" - весь магический заголовок GIF в восьми символах. Это самое маленькое «привет», которое бинарный файл может сказать на Base64, и именно поэтому это первый пример Web API в статье Wikipedia.
  • У toBase64() есть опция omitPadding, которой btoa() никогда не могло быть, потому что договор Web API заполняет без условий. Два десятилетия одного алфавита, и новое API может сделать то, чего старому никогда не разрешали.
  • Один алфавит, две официальные длины строк: MIME заворачивает на 76, PEM - на 64. Те же 64 символа, то же заполнение, два разных тридцатилетних мнения о том, какой шириной может быть строка текста.
  • Число 33 процента - точное: четыре символа на три байта - это соотношение 4/3, и почта RFC-эпохи добавляла примерно ещё 3.5 процента за переводы строк. Ваша «маленькая» строка конфигурации на 37 процентов толще - просто так.
  • Крошечный пакет base64-js набирает больше 100 миллионов загрузок в неделю на npm, почти все скрыты внутри деревьев зависимостей других пакетов. Base64 - самый контрабандный код в экосистеме JavaScript.
  • Маленькие Buffer не выделяются по одному: Node вырезает их из общего пула в 65536 байт (Buffer.poolSize), поэтому создание Buffer быстрое, и поэтому существуют «небезопасные» варианты выделения - на случаи, когда данные предыдущего жильца не важны.
  • RFC, определивший data URL в 1998 году, предупреждает, что они «полезны только для коротких значений», ссылаясь на лимит HTML-атрибутов в 1024 символа. Современные браузеры вставляют изображения размером в мегабайты в те же атрибуты как data URL, - это либо прогресс, либо нахальство, в зависимости от вашего изображения-героя.
  • Хэши Unix-паролей используют собственные Base64-алфавиты, без заполнителя, и, запутанно, порядок различается по схемам: классические хэши crypt(3) используют ./0-9A-Za-z, тогда как строки bcrypt $2b$, которые JavaScript-проекты хранят для паролей пользователей, перетасовывают те же 64 символа в ./A-Za-z0-9. Хорошее напоминание: «Base64» в контексте безопасности - это семья, а не единственный формат.
  • Декодер Node принимает -, _, + и / и в режиме 'base64', и в режиме 'base64url' - четыре символа, одна таблица. Кодировщик, конечно, говорит только на том диалекте, который вы запросили.

Половина кругового пути

Кодирование Base64 в JavaScript и Node.js сводится к трём решениям: какие байты у вас в руках (строке нужна кодировка, Buffer - никакая), какой алфавит требует пункт назначения (классический для MIME и data URL, base64url для токенов и URL, заполнение - по контексту) и какие строковые правила формат по-прежнему исполняет (76 для почты, 64 для PEM, никаких для JSON). Ответьте на это, и встроенные функции сделают остальное: Buffer.toString() в Node, btoa() плюс UTF-8-мост в браузере и Uint8Array.toBase64() в современных рантаймах, у которых оно наконец-то появилось.

И каждый пакет, который вы запечатываете здесь, кто-то однажды вскроет. На стороне декодирования есть свой набор ловушек: снисходительный декодер, который глотает мусор без звука, костюм бинарной строки, который вам подаёт atob(), решения о кодировке, которые принимаются на стороне читателя, и поточная логика, повторяющая паттерн переноса, который вы только что выучили. Эта история, с примерами кода на каждый шаг, подробно разобрана в сопутствующей статье о декодировании Base64 на нашем сайте-близнеце. Читайте её следующей, потому что ловушки на той стороне алфавита тише, а тише - ровно так они и выигрывают.

Последнее обновление: 2026-09-08

Связанная статья: Декодирование Base64 в JavaScript/Node.js: полное руководство