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

File и FileReader

Пользователь перетаскивает CSV-файл с заказами в браузерное приложение. Приложение читает его, парсит, показывает предпросмотр данных — без единого запроса к серверу. Или: загрузка аватара с предпросмотром до отправки, проверка типа и размера файла перед загрузкой. Всё это — File и FileReader.

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

Браузер даёт доступ к файлам пользователя через специальный API. File — это Blob с метаданными (имя, дата). FileReader — асинхронный инструмент для чтения содержимого. Современный способ — использовать методы промисов прямо на File-объекте.

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

  • Blob: File наследует от Blob — все методы Blob доступны напрямую
  • TextDecoder: альтернатива FileReader для чтения текста
  • Promise / async-await: .text(), .arrayBuffer() — промис-based API
  • Объект File

    Файл приходит из <input type="file"> или Drag & Drop. Можно создать программно:

    const file = new File(['Содержимое файла'], 'data.txt', {
      type: 'text/plain',
      lastModified: Date.now(),
    })
    
    console.log(file.name)          // 'data.txt'
    console.log(file.size)          // число байт
    console.log(file.type)          // 'text/plain'
    console.log(file.lastModified)  // timestamp Unix
    console.log(file instanceof Blob)  // true — наследует от Blob!

    Современный API: Promise-based

    Прямые методы на File/Blob — самый простой способ:

    // Читать как текст UTF-8
    const text = await file.text()
    
    // Читать как ArrayBuffer (бинарные данные)
    const buf  = await file.arrayBuffer()
    const view = new Uint8Array(buf)

    FileReader — событийный API

    Старый способ. Нужен когда требуется отслеживание прогресса или readAsDataURL:

    function readAsPromise(file, method = 'readAsText') {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
    
        reader.onload  = e => resolve(e.target.result)
        reader.onerror = () => reject(new Error(reader.error.message))
        reader.onprogress = e => {
          if (e.lengthComputable) {
            const pct = Math.round(e.loaded / e.total * 100)
            console.log(pct + '%')
          }
        }
    
        reader[method](file, 'utf-8')  // readAsText, readAsDataURL, readAsArrayBuffer
      })
    }

    readAsDataURL — предпросмотр изображений

    // Создаёт base64-строку: 'data:image/jpeg;base64,/9j/4AA...'
    const dataUrl = await new Promise((resolve) => {
      const reader = new FileReader()
      reader.onload = e => resolve(e.target.result)
      reader.readAsDataURL(imageFile)
    })
    
    img.src = dataUrl  // Показываем предпросмотр сразу

    Альтернатива без FileReader (быстрее и проще):

    const url = URL.createObjectURL(imageFile)
    img.src = url
    img.onload = () => URL.revokeObjectURL(url)

    Сравнение: FileReader vs современный API

    | | FileReader | file.text() / .arrayBuffer() |

    |---|---|---|

    | API | События (onload) | Promise / async-await |

    | Прогресс | Есть (onprogress) | Нет |

    | readAsDataURL | Есть | Нет (нужен FileReader или URL.createObjectURL) |

    | Отмена | reader.abort() | AbortController |

    | Поддержка | Все браузеры | Chrome 76+, Firefox 69+ |

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

    Ошибка 1: Чтение файла до выбора пользователем

    // НЕВЕРНО — input.files пуст до события change
    const file = input.files[0]  // undefined до выбора файла
    await file.text()  // TypeError!
    
    // ВЕРНО
    input.addEventListener('change', async (e) => {
      const file = e.target.files[0]
      if (!file) return
      const text = await file.text()
    })

    Ошибка 2: Не проверять тип и размер

    // НЕВЕРНО — принимаем любой файл
    const text = await file.text()
    
    // ВЕРНО — валидируем перед чтением
    function validateFile(file, maxSizeMB = 5) {
      if (!file.type.startsWith('text/') && !file.name.endsWith('.csv')) {
        throw new Error('Только текстовые файлы и CSV')
      }
      if (file.size > maxSizeMB * 1024 * 1024) {
        throw new Error(`Файл больше ${maxSizeMB} МБ`)
      }
    }

    Ошибка 3: Повторное использование FileReader одновременно

    // НЕВЕРНО — один reader не может читать два файла параллельно
    const reader = new FileReader()
    reader.readAsText(file1)
    reader.readAsText(file2)  // Прерывает первое чтение!
    
    // ВЕРНО — создаём отдельный reader для каждого файла
    const [text1, text2] = await Promise.all([file1.text(), file2.text()])

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

  • Импорт данных: CSV с транзакциями, контактами, товарами
  • Аватар/изображения: предпросмотр до загрузки на сервер
  • Drag & Drop загрузка: FileReader/File в zone.addEventListener('drop', ...)
  • Валидация: проверка типа файла по magic bytes (не только расширению)
  • Chunk upload: file.slice() + FileReader для загрузки по частям
  • Примеры

    Импорт CSV файлов: парсинг, валидация, предпросмотр и обработка ошибок

    // ===== File объект и FileReader =====
    
    // Вспомогательная функция: читает файл через FileReader (старый API)
    function readFileWithReader(file, method = 'readAsText') {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        const started = Date.now()
    
        reader.onload = (e) => {
          const elapsed = Date.now() - started
          console.log(`  Загружено за ~${elapsed}ms, байт: ${e.loaded}`)
          resolve(e.target.result)
        }
        reader.onerror = () => reject(new Error(`Ошибка: ${reader.error?.message ?? 'неизвестно'}`))
        reader.onprogress = (e) => {
          if (e.lengthComputable && e.total > 0) {
            console.log(`  Прогресс: ${Math.round(e.loaded / e.total * 100)}%`)
          }
        }
    
        if (method === 'readAsText') reader.readAsText(file, 'utf-8')
        else if (method === 'readAsDataURL') reader.readAsDataURL(file)
        else if (method === 'readAsArrayBuffer') reader.readAsArrayBuffer(file)
      })
    }
    
    // Валидация файла до чтения
    function validateCSVFile(file) {
      const errors = []
      const MAX_SIZE = 5 * 1024 * 1024  // 5 MB
    
      if (!file.type.includes('csv') && !file.name.endsWith('.csv')) {
        errors.push('Ожидается CSV файл')
      }
      if (file.size === 0) {
        errors.push('Файл пустой')
      }
      if (file.size > MAX_SIZE) {
        errors.push(`Файл слишком большой: ${(file.size / 1024 / 1024).toFixed(1)} МБ (макс. 5 МБ)`)
      }
      return { valid: errors.length === 0, errors }
    }
    
    // Парсер CSV
    function parseCSV(text) {
      const lines   = text.split('\n').filter(l => l.trim() !== '')
      if (lines.length === 0) return { headers: [], records: [], count: 0 }
    
      const headers = lines[0].split(',').map(h => h.trim())
      const records = lines.slice(1).map(line => {
        const values = line.split(',').map(v => v.trim())
        return Object.fromEntries(headers.map((h, i) => [h, values[i] ?? '']))
      })
    
      return { headers, records, count: records.length }
    }
    
    // ===== Тесты =====
    
    async function runDemo() {
      // --- 1. Метаданные файла ---
      console.log('=== File метаданные ===')
      const csvContent = [
        'id,name,email,amount',
        '1,Иван Петров,ivan@example.com,1500.00',
        '2,Мария Сидорова,maria@example.com,2300.50',
        '3,Алексей Козлов,alexey@example.com,850.00',
        '4,Ольга Смирнова,olga@example.com,4200.00',
      ].join('\n')
    
      const csvFile = new File([csvContent], 'payments.csv', {
        type: 'text/csv',
        lastModified: Date.now() - 86400000,  // вчера
      })
    
      console.log('name:', csvFile.name)
      console.log('size:', csvFile.size, 'байт')
      console.log('type:', csvFile.type)
      console.log('lastModified:', new Date(csvFile.lastModified).toLocaleDateString('ru'))
      console.log('instanceof Blob:', csvFile instanceof Blob)  // true
    
      // --- 2. Валидация ---
      console.log('\n=== Валидация файла ===')
      const { valid, errors } = validateCSVFile(csvFile)
      console.log('Валидный:', valid)   // true
      console.log('Ошибки:', errors)   // []
    
      const badFile = new File([''], 'test.jpg', { type: 'image/jpeg' })
      const { valid: badValid, errors: badErrors } = validateCSVFile(badFile)
      console.log('\nbadFile валидный:', badValid)    // false
      console.log('Ошибки:', badErrors)  // ['Ожидается CSV файл', 'Файл пустой']
    
      // --- 3. Современный API: file.text() ---
      console.log('\n=== Современный API: file.text() ===')
      const text    = await csvFile.text()
      const result  = parseCSV(text)
      console.log('Заголовки:', result.headers)
      console.log('Записей:', result.count)
      console.log('Первый:', result.records[0].name, '—', result.records[0].amount)
    
      // --- 4. FileReader: читаем как текст ---
      console.log('\n=== FileReader (старый API) ===')
      const frText = await readFileWithReader(csvFile, 'readAsText')
      console.log('Результат идентичен file.text():', frText === text)  // true
    
      // --- 5. readAsDataURL: base64 ---
      console.log('\n=== FileReader: readAsDataURL ===')
      // Симулируем GIF (magic bytes: 47 49 46 38 39 61)
      const gifFile = new File(
        [new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61])],
        'test.gif',
        { type: 'image/gif' }
      )
      const dataUrl = await readFileWithReader(gifFile, 'readAsDataURL')
      console.log('DataURL:', dataUrl.substring(0, 40) + '...')
      // data:image/gif;base64,R0lGODlh...
      console.log('Prefix:', dataUrl.split(';')[0])  // data:image/gif
      console.log('Encoding:', dataUrl.split(';')[1].split(',')[0])  // base64
    
      // --- 6. Параллельное чтение нескольких файлов ---
      console.log('\n=== Параллельное чтение файлов ===')
      const files = [
        new File(['файл 1 данные'], 'f1.txt', { type: 'text/plain' }),
        new File(['файл 2 данные'], 'f2.txt', { type: 'text/plain' }),
        new File(['файл 3 данные'], 'f3.txt', { type: 'text/plain' }),
      ]
    
      // Promise.all читает все файлы параллельно
      const contents = await Promise.all(files.map(f => f.text()))
      contents.forEach((content, i) => {
        console.log(`files[${i}] (${files[i].name}): "${content}"`)
      })
    
      // --- 7. Анализ CSV: итоговая сумма ---
      console.log('\n=== Анализ платежей ===')
      const total = result.records.reduce((sum, r) => sum + parseFloat(r.amount || 0), 0)
      const max   = result.records.reduce((m, r) => Math.max(m, parseFloat(r.amount || 0)), 0)
      console.log('Итого платежей:', total.toFixed(2), 'руб.')
      console.log('Максимальный платёж:', max.toFixed(2), 'руб.')
    }
    
    runDemo()

    File и FileReader

    Пользователь перетаскивает CSV-файл с заказами в браузерное приложение. Приложение читает его, парсит, показывает предпросмотр данных — без единого запроса к серверу. Или: загрузка аватара с предпросмотром до отправки, проверка типа и размера файла перед загрузкой. Всё это — File и FileReader.

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

    Браузер даёт доступ к файлам пользователя через специальный API. File — это Blob с метаданными (имя, дата). FileReader — асинхронный инструмент для чтения содержимого. Современный способ — использовать методы промисов прямо на File-объекте.

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

  • Blob: File наследует от Blob — все методы Blob доступны напрямую
  • TextDecoder: альтернатива FileReader для чтения текста
  • Promise / async-await: .text(), .arrayBuffer() — промис-based API
  • Объект File

    Файл приходит из <input type="file"> или Drag & Drop. Можно создать программно:

    const file = new File(['Содержимое файла'], 'data.txt', {
      type: 'text/plain',
      lastModified: Date.now(),
    })
    
    console.log(file.name)          // 'data.txt'
    console.log(file.size)          // число байт
    console.log(file.type)          // 'text/plain'
    console.log(file.lastModified)  // timestamp Unix
    console.log(file instanceof Blob)  // true — наследует от Blob!

    Современный API: Promise-based

    Прямые методы на File/Blob — самый простой способ:

    // Читать как текст UTF-8
    const text = await file.text()
    
    // Читать как ArrayBuffer (бинарные данные)
    const buf  = await file.arrayBuffer()
    const view = new Uint8Array(buf)

    FileReader — событийный API

    Старый способ. Нужен когда требуется отслеживание прогресса или readAsDataURL:

    function readAsPromise(file, method = 'readAsText') {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
    
        reader.onload  = e => resolve(e.target.result)
        reader.onerror = () => reject(new Error(reader.error.message))
        reader.onprogress = e => {
          if (e.lengthComputable) {
            const pct = Math.round(e.loaded / e.total * 100)
            console.log(pct + '%')
          }
        }
    
        reader[method](file, 'utf-8')  // readAsText, readAsDataURL, readAsArrayBuffer
      })
    }

    readAsDataURL — предпросмотр изображений

    // Создаёт base64-строку: 'data:image/jpeg;base64,/9j/4AA...'
    const dataUrl = await new Promise((resolve) => {
      const reader = new FileReader()
      reader.onload = e => resolve(e.target.result)
      reader.readAsDataURL(imageFile)
    })
    
    img.src = dataUrl  // Показываем предпросмотр сразу

    Альтернатива без FileReader (быстрее и проще):

    const url = URL.createObjectURL(imageFile)
    img.src = url
    img.onload = () => URL.revokeObjectURL(url)

    Сравнение: FileReader vs современный API

    | | FileReader | file.text() / .arrayBuffer() |

    |---|---|---|

    | API | События (onload) | Promise / async-await |

    | Прогресс | Есть (onprogress) | Нет |

    | readAsDataURL | Есть | Нет (нужен FileReader или URL.createObjectURL) |

    | Отмена | reader.abort() | AbortController |

    | Поддержка | Все браузеры | Chrome 76+, Firefox 69+ |

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

    Ошибка 1: Чтение файла до выбора пользователем

    // НЕВЕРНО — input.files пуст до события change
    const file = input.files[0]  // undefined до выбора файла
    await file.text()  // TypeError!
    
    // ВЕРНО
    input.addEventListener('change', async (e) => {
      const file = e.target.files[0]
      if (!file) return
      const text = await file.text()
    })

    Ошибка 2: Не проверять тип и размер

    // НЕВЕРНО — принимаем любой файл
    const text = await file.text()
    
    // ВЕРНО — валидируем перед чтением
    function validateFile(file, maxSizeMB = 5) {
      if (!file.type.startsWith('text/') && !file.name.endsWith('.csv')) {
        throw new Error('Только текстовые файлы и CSV')
      }
      if (file.size > maxSizeMB * 1024 * 1024) {
        throw new Error(`Файл больше ${maxSizeMB} МБ`)
      }
    }

    Ошибка 3: Повторное использование FileReader одновременно

    // НЕВЕРНО — один reader не может читать два файла параллельно
    const reader = new FileReader()
    reader.readAsText(file1)
    reader.readAsText(file2)  // Прерывает первое чтение!
    
    // ВЕРНО — создаём отдельный reader для каждого файла
    const [text1, text2] = await Promise.all([file1.text(), file2.text()])

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

  • Импорт данных: CSV с транзакциями, контактами, товарами
  • Аватар/изображения: предпросмотр до загрузки на сервер
  • Drag & Drop загрузка: FileReader/File в zone.addEventListener('drop', ...)
  • Валидация: проверка типа файла по magic bytes (не только расширению)
  • Chunk upload: file.slice() + FileReader для загрузки по частям
  • Примеры

    Импорт CSV файлов: парсинг, валидация, предпросмотр и обработка ошибок

    // ===== File объект и FileReader =====
    
    // Вспомогательная функция: читает файл через FileReader (старый API)
    function readFileWithReader(file, method = 'readAsText') {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        const started = Date.now()
    
        reader.onload = (e) => {
          const elapsed = Date.now() - started
          console.log(`  Загружено за ~${elapsed}ms, байт: ${e.loaded}`)
          resolve(e.target.result)
        }
        reader.onerror = () => reject(new Error(`Ошибка: ${reader.error?.message ?? 'неизвестно'}`))
        reader.onprogress = (e) => {
          if (e.lengthComputable && e.total > 0) {
            console.log(`  Прогресс: ${Math.round(e.loaded / e.total * 100)}%`)
          }
        }
    
        if (method === 'readAsText') reader.readAsText(file, 'utf-8')
        else if (method === 'readAsDataURL') reader.readAsDataURL(file)
        else if (method === 'readAsArrayBuffer') reader.readAsArrayBuffer(file)
      })
    }
    
    // Валидация файла до чтения
    function validateCSVFile(file) {
      const errors = []
      const MAX_SIZE = 5 * 1024 * 1024  // 5 MB
    
      if (!file.type.includes('csv') && !file.name.endsWith('.csv')) {
        errors.push('Ожидается CSV файл')
      }
      if (file.size === 0) {
        errors.push('Файл пустой')
      }
      if (file.size > MAX_SIZE) {
        errors.push(`Файл слишком большой: ${(file.size / 1024 / 1024).toFixed(1)} МБ (макс. 5 МБ)`)
      }
      return { valid: errors.length === 0, errors }
    }
    
    // Парсер CSV
    function parseCSV(text) {
      const lines   = text.split('\n').filter(l => l.trim() !== '')
      if (lines.length === 0) return { headers: [], records: [], count: 0 }
    
      const headers = lines[0].split(',').map(h => h.trim())
      const records = lines.slice(1).map(line => {
        const values = line.split(',').map(v => v.trim())
        return Object.fromEntries(headers.map((h, i) => [h, values[i] ?? '']))
      })
    
      return { headers, records, count: records.length }
    }
    
    // ===== Тесты =====
    
    async function runDemo() {
      // --- 1. Метаданные файла ---
      console.log('=== File метаданные ===')
      const csvContent = [
        'id,name,email,amount',
        '1,Иван Петров,ivan@example.com,1500.00',
        '2,Мария Сидорова,maria@example.com,2300.50',
        '3,Алексей Козлов,alexey@example.com,850.00',
        '4,Ольга Смирнова,olga@example.com,4200.00',
      ].join('\n')
    
      const csvFile = new File([csvContent], 'payments.csv', {
        type: 'text/csv',
        lastModified: Date.now() - 86400000,  // вчера
      })
    
      console.log('name:', csvFile.name)
      console.log('size:', csvFile.size, 'байт')
      console.log('type:', csvFile.type)
      console.log('lastModified:', new Date(csvFile.lastModified).toLocaleDateString('ru'))
      console.log('instanceof Blob:', csvFile instanceof Blob)  // true
    
      // --- 2. Валидация ---
      console.log('\n=== Валидация файла ===')
      const { valid, errors } = validateCSVFile(csvFile)
      console.log('Валидный:', valid)   // true
      console.log('Ошибки:', errors)   // []
    
      const badFile = new File([''], 'test.jpg', { type: 'image/jpeg' })
      const { valid: badValid, errors: badErrors } = validateCSVFile(badFile)
      console.log('\nbadFile валидный:', badValid)    // false
      console.log('Ошибки:', badErrors)  // ['Ожидается CSV файл', 'Файл пустой']
    
      // --- 3. Современный API: file.text() ---
      console.log('\n=== Современный API: file.text() ===')
      const text    = await csvFile.text()
      const result  = parseCSV(text)
      console.log('Заголовки:', result.headers)
      console.log('Записей:', result.count)
      console.log('Первый:', result.records[0].name, '—', result.records[0].amount)
    
      // --- 4. FileReader: читаем как текст ---
      console.log('\n=== FileReader (старый API) ===')
      const frText = await readFileWithReader(csvFile, 'readAsText')
      console.log('Результат идентичен file.text():', frText === text)  // true
    
      // --- 5. readAsDataURL: base64 ---
      console.log('\n=== FileReader: readAsDataURL ===')
      // Симулируем GIF (magic bytes: 47 49 46 38 39 61)
      const gifFile = new File(
        [new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61])],
        'test.gif',
        { type: 'image/gif' }
      )
      const dataUrl = await readFileWithReader(gifFile, 'readAsDataURL')
      console.log('DataURL:', dataUrl.substring(0, 40) + '...')
      // data:image/gif;base64,R0lGODlh...
      console.log('Prefix:', dataUrl.split(';')[0])  // data:image/gif
      console.log('Encoding:', dataUrl.split(';')[1].split(',')[0])  // base64
    
      // --- 6. Параллельное чтение нескольких файлов ---
      console.log('\n=== Параллельное чтение файлов ===')
      const files = [
        new File(['файл 1 данные'], 'f1.txt', { type: 'text/plain' }),
        new File(['файл 2 данные'], 'f2.txt', { type: 'text/plain' }),
        new File(['файл 3 данные'], 'f3.txt', { type: 'text/plain' }),
      ]
    
      // Promise.all читает все файлы параллельно
      const contents = await Promise.all(files.map(f => f.text()))
      contents.forEach((content, i) => {
        console.log(`files[${i}] (${files[i].name}): "${content}"`)
      })
    
      // --- 7. Анализ CSV: итоговая сумма ---
      console.log('\n=== Анализ платежей ===')
      const total = result.records.reduce((sum, r) => sum + parseFloat(r.amount || 0), 0)
      const max   = result.records.reduce((m, r) => Math.max(m, parseFloat(r.amount || 0)), 0)
      console.log('Итого платежей:', total.toFixed(2), 'руб.')
      console.log('Максимальный платёж:', max.toFixed(2), 'руб.')
    }
    
    runDemo()

    Задание

    Ты разрабатываешь модуль импорта данных. Нужно реализовать несколько утилит для работы с файлами. Реализуй: - `getFileInfo(file)` — возвращает `{ name, size, type, sizeKB }` (`sizeKB` — размер в КБ, округлённый до 2 знаков) - `processCSV(file)` — читает File через `file.text()`, парсит CSV (первая строка — заголовки, разделитель — запятая), возвращает Promise с массивом объектов - `detectFileType(file)` — читает первые 4 байта через `file.slice(0, 4).arrayBuffer()` и определяет тип: `'PNG'`, `'JPEG'`, `'PDF'` или `'UNKNOWN'` по magic bytes

    Подсказка

    getFileInfo: { name: file.name, size: file.size, type: file.type, sizeKB: Math.round(file.size/1024*100)/100 }. processCSV: await file.text(), split("\n").filter(), slice(1).map(). detectFileType: file.slice(0,4).arrayBuffer(), new Uint8Array(buf), проверяй байты

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