← JavaScript/Blob#159 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Blob

В дашборде аналитики пользователь жмёт «Экспорт в CSV» — и через секунду файл скачивается. Никакого сервера. Данные уже в браузере, Blob превращает их в файл. В другом сценарии: пользователь загружает аватар — ты создаёшь Blob URL для предпросмотра до отправки на сервер. Без Blob это невозможно.

Какую проблему решает

Файловые операции в браузере требуют объекта, который ведёт себя как файл: имеет размер, MIME-тип, поддерживает чтение по частям. Blob (Binary Large Object) — именно такой объект. Он иммутабельный и высокоуровневый, в отличие от ArrayBuffer.

На основе предыдущих уроков

  • ArrayBuffer: Blob может быть создан из ArrayBuffer/TypedArray и конвертирован обратно через .arrayBuffer()
  • TextEncoder: Blob может содержать строки, которые он кодирует в UTF-8
  • Promise: методы .text(), .arrayBuffer() возвращают Promise
  • Создание Blob

    // Из строк
    const textBlob = new Blob(['Привет!'], { type: 'text/plain; charset=utf-8' })
    console.log(textBlob.size)  // 13 байт (кириллица UTF-8)
    console.log(textBlob.type)  // 'text/plain; charset=utf-8'
    
    // Из нескольких частей (конкатенация)
    const html = new Blob(
      ['<!DOCTYPE html>', '<html>', '<body>Привет</body>', '</html>'],
      { type: 'text/html' }
    )
    
    // Из ArrayBuffer / TypedArray
    const bytes = new Blob([new Uint8Array([137, 80, 78, 71])])  // PNG magic bytes

    Методы чтения

    const blob = new Blob(['Hello, Blob!'])
    
    // Современный API — возвращают Promise
    const text = await blob.text()           // 'Hello, Blob!'
    const buf  = await blob.arrayBuffer()    // ArrayBuffer
    // blob.stream()                         // ReadableStream (потоковое чтение)
    
    // Размер и тип
    console.log(blob.size)  // 11 байт
    console.log(blob.type)  // '' (не указан)

    slice — чтение по частям

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

    const big  = new Blob(['ABCDEFGHIJ'])
    const part = big.slice(2, 6)        // новый Blob: байты 2..5
    const text = await part.text()
    console.log(text)  // 'CDEF'
    
    // Читаем только заголовок большого файла (первые 512 байт)
    const header = file.slice(0, 512)
    const magic  = await header.arrayBuffer()

    URL.createObjectURL — скачивание файлов

    Важнейший паттерн: генерация файла прямо в браузере и скачивание без сервера:

    function downloadFile(content, filename, mimeType) {
      const blob = new Blob([content], { type: mimeType })
      const url  = URL.createObjectURL(blob)  // 'blob:https://...'
    
      const link = document.createElement('a')
      link.href     = url
      link.download = filename
      link.click()
    
      URL.revokeObjectURL(url)  // Освобождаем память!
    }

    В sandbox-среде без DOM создаём Blob и URL, просто не переходим по ним.

    Типичные ошибки

    Ошибка 1: Забыть revokeObjectURL

    // НЕВЕРНО — утечка памяти: blob-URL живёт до закрытия вкладки
    const url = URL.createObjectURL(blob)
    link.href = url
    link.click()
    // Забыли URL.revokeObjectURL(url)!
    
    // ВЕРНО — освобождаем сразу после использования
    link.addEventListener('click', () => {
      setTimeout(() => URL.revokeObjectURL(url), 1000)
    })

    Ошибка 2: Неправильный MIME-тип

    // CSV без кодировки — Excel откроет с кракозябрами
    const bad = new Blob([csv], { type: 'text/csv' })
    
    // ВЕРНО — указываем кодировку
    const good = new Blob(['' + csv], { type: 'text/csv; charset=utf-8' })
    // '' — BOM-маркер, помогает Excel понять UTF-8

    Ошибка 3: Передача Blob туда, где нужен ArrayBuffer

    // НЕВЕРНО — Blob не является ArrayBuffer
    const blob = new Blob([data])
    new DataView(blob)  // TypeError!
    
    // ВЕРНО — конвертировать явно
    const buf  = await blob.arrayBuffer()
    const view = new DataView(buf)

    В реальных проектах

  • Экспорт данных: CSV, JSON, Excel-файлы прямо из браузера без сервера
  • Предпросмотр изображений: Blob URL для <img src> до загрузки на сервер
  • Загрузка через FormData: formData.append('file', blob, 'data.json')
  • Service Worker: кэширование медиа-файлов как Blob в IndexedDB
  • Chunk upload: slice() для загрузки больших файлов по частям
  • Примеры

    Генератор отчётов: экспорт CSV и JSON, предпросмотр через Blob URL, загрузка по частям

    // ===== Генератор отчётов с Blob =====
    
    // Функция генерации CSV с BOM для корректного отображения в Excel
    function generateCSV(headers, rows) {
      const BOM      = ''  // UTF-8 BOM для Excel
      const headerRow = headers.join(',')
      const dataRows  = rows.map(row =>
        headers.map(h => {
          const val = String(row[h] ?? '')
          // Экранируем запятые и кавычки
          return val.includes(',') || val.includes('"')
            ? '"' + val.replace(/"/g, '""') + '"'
            : val
        }).join(',')
      )
      return BOM + [headerRow, ...dataRows].join('\n')
    }
    
    // ===== Тестовые данные =====
    const sales = [
      { product: 'Ноутбук',  qty: 15, price: 85000, total: 1275000 },
      { product: 'Мышь',     qty: 80, price: 1200,  total: 96000 },
      { product: 'Клавиатура', qty: 50, price: 3500, total: 175000 },
      { product: 'Монитор',  qty: 20, price: 25000, total: 500000 },
    ]
    
    // CSV экспорт
    console.log('=== CSV экспорт ===')
    const csvContent = generateCSV(['product', 'qty', 'price', 'total'], sales)
    const csvBlob    = new Blob([csvContent], { type: 'text/csv; charset=utf-8' })
    console.log('CSV размер:', csvBlob.size, 'байт')
    console.log('CSV тип:', csvBlob.type)
    
    const csvUrl = URL.createObjectURL(csvBlob)
    console.log('URL для скачивания:', csvUrl.substring(0, 20) + '...')
    URL.revokeObjectURL(csvUrl)
    console.log('URL освобождён')
    
    // JSON экспорт с метаданными
    console.log('\n=== JSON экспорт ===')
    async function exportJSON(data, meta = {}) {
      const payload = {
        exportedAt: new Date().toISOString(),
        version: '1.0',
        ...meta,
        data,
      }
      const json    = JSON.stringify(payload, null, 2)
      const blob    = new Blob([json], { type: 'application/json' })
    
      // Читаем обратно для проверки
      const text   = await blob.text()
      const parsed = JSON.parse(text)
    
      console.log('JSON blob размер:', blob.size, 'байт')
      console.log('Записей в данных:', parsed.data.length)
      console.log('Первый товар:', parsed.data[0].product)
      console.log('Экспортирован:', parsed.exportedAt.substring(0, 10))
    
      return blob
    }
    
    exportJSON(sales, { source: 'sales-dashboard' }).then(blob => {
      console.log('Итоговый размер JSON:', blob.size, 'байт')
    })
    
    // ===== slice: чтение по частям =====
    console.log('\n=== Blob.slice — чтение по частям ===')
    
    async function readInChunks(blob, chunkSize) {
      const chunks = []
      let offset = 0
      while (offset < blob.size) {
        const chunk     = blob.slice(offset, offset + chunkSize)
        const chunkText = await chunk.text()
        chunks.push(chunkText)
        offset += chunkSize
      }
      return chunks.join('')
    }
    
    async function sliceDemo() {
      const text = 'Строка 1\nСтрока 2\nСтрока 3\nСтрока 4\nСтрока 5'
      const blob = new Blob([text])
    
      console.log('Размер blob:', blob.size, 'байт')
    
      // Читаем заголовок (первые 10 байт)
      const header = await blob.slice(0, 10).text()
      console.log('Первые 10 байт:', JSON.stringify(header))
    
      // Читаем по 15-байтовым чанкам
      const assembled = await readInChunks(blob, 15)
      console.log('Собрано чанков:', Math.ceil(blob.size / 15))
      console.log('Текст восстановлен:', assembled === text)
    }
    
    sliceDemo()
    
    // ===== Blob → ArrayBuffer → Blob =====
    console.log('\n=== Конвертация Blob ↔ ArrayBuffer ===')
    
    async function convertDemo() {
      // Создаём Blob с произвольными бинарными данными
      const original = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
      // Это PNG magic bytes!
      const blob = new Blob([original], { type: 'image/png' })
    
      // Blob → ArrayBuffer
      const buf   = await blob.arrayBuffer()
      const bytes = new Uint8Array(buf)
    
      const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(' ')
      console.log('PNG magic bytes:', hex)
      // 89 50 4e 47 0d 0a 1a 0a
    
      // Проверяем: это PNG?
      const isPNG = bytes[0] === 0x89 && bytes[1] === 0x50 &&
                    bytes[2] === 0x4E && bytes[3] === 0x47
      console.log('Это PNG?', isPNG)  // true
    
      // ArrayBuffer → новый Blob
      const restored = new Blob([buf], { type: blob.type })
      console.log('Восстановленный тип:', restored.type)    // image/png
      console.log('Размер совпадает:', restored.size === blob.size)  // true
    }
    
    convertDemo()

    Blob

    В дашборде аналитики пользователь жмёт «Экспорт в CSV» — и через секунду файл скачивается. Никакого сервера. Данные уже в браузере, Blob превращает их в файл. В другом сценарии: пользователь загружает аватар — ты создаёшь Blob URL для предпросмотра до отправки на сервер. Без Blob это невозможно.

    Какую проблему решает

    Файловые операции в браузере требуют объекта, который ведёт себя как файл: имеет размер, MIME-тип, поддерживает чтение по частям. Blob (Binary Large Object) — именно такой объект. Он иммутабельный и высокоуровневый, в отличие от ArrayBuffer.

    На основе предыдущих уроков

  • ArrayBuffer: Blob может быть создан из ArrayBuffer/TypedArray и конвертирован обратно через .arrayBuffer()
  • TextEncoder: Blob может содержать строки, которые он кодирует в UTF-8
  • Promise: методы .text(), .arrayBuffer() возвращают Promise
  • Создание Blob

    // Из строк
    const textBlob = new Blob(['Привет!'], { type: 'text/plain; charset=utf-8' })
    console.log(textBlob.size)  // 13 байт (кириллица UTF-8)
    console.log(textBlob.type)  // 'text/plain; charset=utf-8'
    
    // Из нескольких частей (конкатенация)
    const html = new Blob(
      ['<!DOCTYPE html>', '<html>', '<body>Привет</body>', '</html>'],
      { type: 'text/html' }
    )
    
    // Из ArrayBuffer / TypedArray
    const bytes = new Blob([new Uint8Array([137, 80, 78, 71])])  // PNG magic bytes

    Методы чтения

    const blob = new Blob(['Hello, Blob!'])
    
    // Современный API — возвращают Promise
    const text = await blob.text()           // 'Hello, Blob!'
    const buf  = await blob.arrayBuffer()    // ArrayBuffer
    // blob.stream()                         // ReadableStream (потоковое чтение)
    
    // Размер и тип
    console.log(blob.size)  // 11 байт
    console.log(blob.type)  // '' (не указан)

    slice — чтение по частям

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

    const big  = new Blob(['ABCDEFGHIJ'])
    const part = big.slice(2, 6)        // новый Blob: байты 2..5
    const text = await part.text()
    console.log(text)  // 'CDEF'
    
    // Читаем только заголовок большого файла (первые 512 байт)
    const header = file.slice(0, 512)
    const magic  = await header.arrayBuffer()

    URL.createObjectURL — скачивание файлов

    Важнейший паттерн: генерация файла прямо в браузере и скачивание без сервера:

    function downloadFile(content, filename, mimeType) {
      const blob = new Blob([content], { type: mimeType })
      const url  = URL.createObjectURL(blob)  // 'blob:https://...'
    
      const link = document.createElement('a')
      link.href     = url
      link.download = filename
      link.click()
    
      URL.revokeObjectURL(url)  // Освобождаем память!
    }

    В sandbox-среде без DOM создаём Blob и URL, просто не переходим по ним.

    Типичные ошибки

    Ошибка 1: Забыть revokeObjectURL

    // НЕВЕРНО — утечка памяти: blob-URL живёт до закрытия вкладки
    const url = URL.createObjectURL(blob)
    link.href = url
    link.click()
    // Забыли URL.revokeObjectURL(url)!
    
    // ВЕРНО — освобождаем сразу после использования
    link.addEventListener('click', () => {
      setTimeout(() => URL.revokeObjectURL(url), 1000)
    })

    Ошибка 2: Неправильный MIME-тип

    // CSV без кодировки — Excel откроет с кракозябрами
    const bad = new Blob([csv], { type: 'text/csv' })
    
    // ВЕРНО — указываем кодировку
    const good = new Blob(['' + csv], { type: 'text/csv; charset=utf-8' })
    // '' — BOM-маркер, помогает Excel понять UTF-8

    Ошибка 3: Передача Blob туда, где нужен ArrayBuffer

    // НЕВЕРНО — Blob не является ArrayBuffer
    const blob = new Blob([data])
    new DataView(blob)  // TypeError!
    
    // ВЕРНО — конвертировать явно
    const buf  = await blob.arrayBuffer()
    const view = new DataView(buf)

    В реальных проектах

  • Экспорт данных: CSV, JSON, Excel-файлы прямо из браузера без сервера
  • Предпросмотр изображений: Blob URL для <img src> до загрузки на сервер
  • Загрузка через FormData: formData.append('file', blob, 'data.json')
  • Service Worker: кэширование медиа-файлов как Blob в IndexedDB
  • Chunk upload: slice() для загрузки больших файлов по частям
  • Примеры

    Генератор отчётов: экспорт CSV и JSON, предпросмотр через Blob URL, загрузка по частям

    // ===== Генератор отчётов с Blob =====
    
    // Функция генерации CSV с BOM для корректного отображения в Excel
    function generateCSV(headers, rows) {
      const BOM      = ''  // UTF-8 BOM для Excel
      const headerRow = headers.join(',')
      const dataRows  = rows.map(row =>
        headers.map(h => {
          const val = String(row[h] ?? '')
          // Экранируем запятые и кавычки
          return val.includes(',') || val.includes('"')
            ? '"' + val.replace(/"/g, '""') + '"'
            : val
        }).join(',')
      )
      return BOM + [headerRow, ...dataRows].join('\n')
    }
    
    // ===== Тестовые данные =====
    const sales = [
      { product: 'Ноутбук',  qty: 15, price: 85000, total: 1275000 },
      { product: 'Мышь',     qty: 80, price: 1200,  total: 96000 },
      { product: 'Клавиатура', qty: 50, price: 3500, total: 175000 },
      { product: 'Монитор',  qty: 20, price: 25000, total: 500000 },
    ]
    
    // CSV экспорт
    console.log('=== CSV экспорт ===')
    const csvContent = generateCSV(['product', 'qty', 'price', 'total'], sales)
    const csvBlob    = new Blob([csvContent], { type: 'text/csv; charset=utf-8' })
    console.log('CSV размер:', csvBlob.size, 'байт')
    console.log('CSV тип:', csvBlob.type)
    
    const csvUrl = URL.createObjectURL(csvBlob)
    console.log('URL для скачивания:', csvUrl.substring(0, 20) + '...')
    URL.revokeObjectURL(csvUrl)
    console.log('URL освобождён')
    
    // JSON экспорт с метаданными
    console.log('\n=== JSON экспорт ===')
    async function exportJSON(data, meta = {}) {
      const payload = {
        exportedAt: new Date().toISOString(),
        version: '1.0',
        ...meta,
        data,
      }
      const json    = JSON.stringify(payload, null, 2)
      const blob    = new Blob([json], { type: 'application/json' })
    
      // Читаем обратно для проверки
      const text   = await blob.text()
      const parsed = JSON.parse(text)
    
      console.log('JSON blob размер:', blob.size, 'байт')
      console.log('Записей в данных:', parsed.data.length)
      console.log('Первый товар:', parsed.data[0].product)
      console.log('Экспортирован:', parsed.exportedAt.substring(0, 10))
    
      return blob
    }
    
    exportJSON(sales, { source: 'sales-dashboard' }).then(blob => {
      console.log('Итоговый размер JSON:', blob.size, 'байт')
    })
    
    // ===== slice: чтение по частям =====
    console.log('\n=== Blob.slice — чтение по частям ===')
    
    async function readInChunks(blob, chunkSize) {
      const chunks = []
      let offset = 0
      while (offset < blob.size) {
        const chunk     = blob.slice(offset, offset + chunkSize)
        const chunkText = await chunk.text()
        chunks.push(chunkText)
        offset += chunkSize
      }
      return chunks.join('')
    }
    
    async function sliceDemo() {
      const text = 'Строка 1\nСтрока 2\nСтрока 3\nСтрока 4\nСтрока 5'
      const blob = new Blob([text])
    
      console.log('Размер blob:', blob.size, 'байт')
    
      // Читаем заголовок (первые 10 байт)
      const header = await blob.slice(0, 10).text()
      console.log('Первые 10 байт:', JSON.stringify(header))
    
      // Читаем по 15-байтовым чанкам
      const assembled = await readInChunks(blob, 15)
      console.log('Собрано чанков:', Math.ceil(blob.size / 15))
      console.log('Текст восстановлен:', assembled === text)
    }
    
    sliceDemo()
    
    // ===== Blob → ArrayBuffer → Blob =====
    console.log('\n=== Конвертация Blob ↔ ArrayBuffer ===')
    
    async function convertDemo() {
      // Создаём Blob с произвольными бинарными данными
      const original = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
      // Это PNG magic bytes!
      const blob = new Blob([original], { type: 'image/png' })
    
      // Blob → ArrayBuffer
      const buf   = await blob.arrayBuffer()
      const bytes = new Uint8Array(buf)
    
      const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(' ')
      console.log('PNG magic bytes:', hex)
      // 89 50 4e 47 0d 0a 1a 0a
    
      // Проверяем: это PNG?
      const isPNG = bytes[0] === 0x89 && bytes[1] === 0x50 &&
                    bytes[2] === 0x4E && bytes[3] === 0x47
      console.log('Это PNG?', isPNG)  // true
    
      // ArrayBuffer → новый Blob
      const restored = new Blob([buf], { type: blob.type })
      console.log('Восстановленный тип:', restored.type)    // image/png
      console.log('Размер совпадает:', restored.size === blob.size)  // true
    }
    
    convertDemo()

    Задание

    В дашборде аналитики нужно реализовать экспорт данных в разных форматах. Реализуй: - `downloadCSV(data, headers, filename)` — создаёт CSV-Blob из массива объектов (с BOM `\uFEFF` для Excel), логирует параметры скачивания, освобождает URL, возвращает размер файла в байтах - `createReport(title, lines)` — принимает заголовок и массив строк, создаёт Blob `text/plain`, возвращает `{ blob, url, size }` - `blobToBase64(blob)` — конвертирует Blob в base64-строку через ArrayBuffer

    Подсказка

    downloadCSV: new Blob([csv], {type: "text/csv; charset=utf-8"}), URL.createObjectURL(blob), URL.revokeObjectURL(url), return blob.size. createReport: join строк через \n. blobToBase64: await blob.arrayBuffer(), Uint8Array, fromCharCode, btoa

    Загружаем среду выполнения...
    Загружаем AI-помощника...