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

XMLHttpRequest

Представь: ты делаешь загрузку фотографий в облако. Пользователь выбрал файл 50 МБ — нужно показать прогресс-бар с процентом. fetch не умеет отслеживать прогресс отправки. XMLHttpRequest — умеет. Именно поэтому XHR до сих пор используется, несмотря на то что fetch заменил его почти везде.

Что решает этот механизм

XHR — это callback-based API для HTTP-запросов с дополнительными возможностями: прогресс загрузки (xhr.upload.onprogress), таймаут (xhr.timeout), отмена (xhr.abort()). Для загрузки файлов с прогресс-баром XHR до сих пор является стандартным решением.

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

  • fetch — XHR решает те же задачи, но через callback-паттерн; понимание fetch помогает сравнить подходы
  • Promise — обёртка XHR в Promise — классический пример промисификации callback API
  • Основное использование

    const xhr = new XMLHttpRequest()
    
    // 1. Открыть запрос: метод, URL, async (true по умолчанию)
    xhr.open('GET', 'https://api.example.com/products')
    
    // 2. Установить обработчики
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        const data = JSON.parse(xhr.responseText)
        console.log('Данные:', data)
      } else {
        console.log('Ошибка HTTP:', xhr.status)
      }
    }
    
    xhr.onerror = function() {
      console.log('Сетевая ошибка')
    }
    
    // 3. Отправить запрос
    xhr.send()

    readyState — стадии запроса

    // xhr.readyState меняется от 0 до 4:
    // 0 — UNSENT:           xhr создан, open не вызван
    // 1 — OPENED:           open() вызван
    // 2 — HEADERS_RECEIVED: получены заголовки ответа
    // 3 — LOADING:          получение тела ответа
    // 4 — DONE:             запрос завершён
    
    xhr.onreadystatechange = function() {
      console.log('readyState:', xhr.readyState)
      if (xhr.readyState === 4) {
        console.log('Готово! status:', xhr.status)
      }
    }

    Прогресс загрузки файла

    Главное преимущество XHR перед fetch — xhr.upload.onprogress:

    const xhr = new XMLHttpRequest()
    xhr.open('POST', '/api/upload')
    
    // Прогресс отправки файла
    xhr.upload.onprogress = function(event) {
      if (event.lengthComputable) {
        const percent = Math.round((event.loaded / event.total) * 100)
        console.log(`Загружено: ${percent}%`)
        progressBar.style.width = percent + '%'
      }
    }
    
    xhr.upload.onload = function() {
      console.log('Файл успешно отправлен!')
    }
    
    const formData = new FormData()
    formData.append('file', selectedFile)
    xhr.send(formData)

    Отмена запроса

    const xhr = new XMLHttpRequest()
    xhr.open('GET', '/api/slow-endpoint')
    xhr.send()
    
    // Отменить запрос
    setTimeout(() => {
      xhr.abort()         // прерывает запрос
    }, 3000)
    
    xhr.onabort = function() {
      console.log('Запрос отменён')
    }

    Заголовки запроса и ответа

    const xhr = new XMLHttpRequest()
    xhr.open('POST', '/api/data')
    
    // Установить заголовок запроса
    xhr.setRequestHeader('Content-Type', 'application/json')
    xhr.setRequestHeader('Authorization', 'Bearer my-token')
    
    xhr.onload = function() {
      // Прочитать заголовок ответа
      const contentType = xhr.getResponseHeader('Content-Type')
      const allHeaders  = xhr.getAllResponseHeaders()
      console.log(contentType)
    }
    
    xhr.send(JSON.stringify({ name: 'Иван' }))

    XHR vs fetch

    | | XHR | fetch |

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

    | API стиль | Callback-based | Promise-based |

    | Upload прогресс | Да (xhr.upload) | Нет |

    | Прервать запрос | xhr.abort() | AbortController |

    | Таймаут | xhr.timeout | Нет встроенного |

    | Читаемость | Сложно | Чисто |

    | Стриминг ответа | Нет | Да (ReadableStream) |

    Для загрузки файлов с прогрессом — XHR. Для всего остального — fetch.

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

    1. Читать responseText до onload — данные ещё не пришли

    // ПЛОХО — responseText пустой, readyState < 4
    xhr.open('GET', '/api/data')
    xhr.send()
    console.log(xhr.responseText)  // '' — запрос ещё не завершён!
    
    // ХОРОШО — читать только в onload (readyState === 4)
    xhr.onload = function() {
      console.log(xhr.responseText)  // данные готовы
    }

    2. Не проверять xhr.status — onload вызывается даже при HTTP ошибках

    // ПЛОХО — onload срабатывает и для 404, и для 500
    xhr.onload = function() {
      const data = JSON.parse(xhr.responseText)  // может быть текст ошибки!
      processData(data)
    }
    
    // ХОРОШО — проверяй статус
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        processData(JSON.parse(xhr.responseText))
      } else {
        console.error('HTTP ошибка:', xhr.status, xhr.statusText)
      }
    }

    3. Устанавливать заголовки после send()

    // ПЛОХО — заголовки нужно устанавливать ДО send()
    xhr.open('POST', '/api/data')
    xhr.send(body)
    xhr.setRequestHeader('Content-Type', 'application/json')  // слишком поздно!
    
    // ХОРОШО — setRequestHeader между open() и send()
    xhr.open('POST', '/api/data')
    xhr.setRequestHeader('Content-Type', 'application/json')  // здесь
    xhr.send(body)

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

  • Загрузка файлов с прогрессом: Dropbox, Google Drive, ВКонтакте используют XHR для отображения прогресс-баров при загрузке
  • Обёртка в Promise: библиотека axios внутри использует XHR (для браузерной версии) и оборачивает его в Promise
  • Отмена запросов: поиск с автодополнением отменяет предыдущий XHR при каждом новом вводе через xhr.abort()
  • Примеры

    Mock-класс XMLHttpRequest: симуляция open/send/onload, прогресс загрузки файла, обработка ошибок

    // Mock-реализация XMLHttpRequest для демонстрации API
    // Воспроизводит интерфейс реального XHR
    
    function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }
    
    class MockXMLHttpRequest {
      constructor() {
        this.readyState = 0       // UNSENT
        this.status = 0
        this.responseText = ''
        this.timeout = 0
        this._method = ''
        this._url = ''
        this._aborted = false
    
        // Обработчики
        this.onload = null
        this.onerror = null
        this.onabort = null
        this.onreadystatechange = null
        this.ontimeout = null
    
        // upload — отдельный объект для прогресса
        this.upload = {
          onprogress: null,
          onload: null,
        }
      }
    
      open(method, url, async = true) {
        this._method = method
        this._url = url
        this.readyState = 1  // OPENED
        this._fireReadyStateChange()
        console.log(`XHR: open(${method}, ${url})`)
      }
    
      setRequestHeader(name, value) {
        console.log(`XHR: заголовок "${name}: ${value}"`)
      }
    
      abort() {
        this._aborted = true
        console.log('XHR: запрос прерван (abort)')
        if (this.onabort) this.onabort()
      }
    
      send(body = null) {
        if (this._aborted) return
        console.log(`XHR: send() — начинаем запрос к ${this._url}`)
    
        // Симулируем асинхронный запрос
        this._simulateRequest(body)
      }
    
      async _simulateRequest(body) {
        // Имитация загрузки файла с прогрессом
        if (body && this.upload.onprogress) {
          const total = 1024 * 1024  // 1MB
          const steps = 5
    
          for (let i = 1; i <= steps; i++) {
            if (this._aborted) return
            await delay(100)
            const loaded = Math.round((total / steps) * i)
            this.upload.onprogress({ loaded, total, lengthComputable: true })
          }
    
          if (this.upload.onload) this.upload.onload()
        }
    
        // Имитация ответа сервера
        if (!this._aborted) {
          await delay(200)
          this.readyState = 2  // HEADERS_RECEIVED
          this._fireReadyStateChange()
    
          await delay(50)
          this.readyState = 3  // LOADING
          this._fireReadyStateChange()
    
          await delay(50)
    
          if (!this._aborted) {
            // Решаем: успех или ошибка (для демо — всегда успех)
            this.status = 200
            this.responseText = JSON.stringify({ ok: true, url: this._url, method: this._method })
            this.readyState = 4  // DONE
            this._fireReadyStateChange()
            if (this.onload) this.onload()
          }
        }
      }
    
      _fireReadyStateChange() {
        if (this.onreadystatechange) this.onreadystatechange()
      }
    }
    
    // --- Демо 1: Простой GET запрос ---
    console.log('=== GET запрос ===')
    const xhr1 = new MockXMLHttpRequest()
    
    xhr1.onreadystatechange = function() {
      const states = ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE']
      console.log(`readyState: ${xhr1.readyState} (${states[xhr1.readyState]})`)
    }
    
    xhr1.onload = function() {
      console.log('status:', xhr1.status)
      console.log('response:', xhr1.responseText)
    }
    
    xhr1.open('GET', 'https://api.example.com/users')
    xhr1.send()
    
    // --- Демо 2: Загрузка файла с прогрессом ---
    setTimeout(() => {
      console.log('\n=== Загрузка файла с прогрессом ===')
      const xhr2 = new MockXMLHttpRequest()
    
      xhr2.upload.onprogress = function(event) {
        const percent = Math.round((event.loaded / event.total) * 100)
        const bar = '='.repeat(Math.floor(percent / 5)).padEnd(20, ' ')
        console.log(`[${bar}] ${percent}% (${event.loaded}/${event.total} байт)`)
      }
    
      xhr2.upload.onload = function() {
        console.log('Файл отправлен на сервер!')
      }
    
      xhr2.onload = function() {
        console.log('Ответ сервера получен:', xhr2.responseText)
      }
    
      xhr2.open('POST', 'https://api.example.com/upload')
      xhr2.setRequestHeader('Content-Type', 'multipart/form-data')
      xhr2.send({ file: 'document.pdf', size: 1024 * 1024 })
    }, 800)

    XMLHttpRequest

    Представь: ты делаешь загрузку фотографий в облако. Пользователь выбрал файл 50 МБ — нужно показать прогресс-бар с процентом. fetch не умеет отслеживать прогресс отправки. XMLHttpRequest — умеет. Именно поэтому XHR до сих пор используется, несмотря на то что fetch заменил его почти везде.

    Что решает этот механизм

    XHR — это callback-based API для HTTP-запросов с дополнительными возможностями: прогресс загрузки (xhr.upload.onprogress), таймаут (xhr.timeout), отмена (xhr.abort()). Для загрузки файлов с прогресс-баром XHR до сих пор является стандартным решением.

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

  • fetch — XHR решает те же задачи, но через callback-паттерн; понимание fetch помогает сравнить подходы
  • Promise — обёртка XHR в Promise — классический пример промисификации callback API
  • Основное использование

    const xhr = new XMLHttpRequest()
    
    // 1. Открыть запрос: метод, URL, async (true по умолчанию)
    xhr.open('GET', 'https://api.example.com/products')
    
    // 2. Установить обработчики
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        const data = JSON.parse(xhr.responseText)
        console.log('Данные:', data)
      } else {
        console.log('Ошибка HTTP:', xhr.status)
      }
    }
    
    xhr.onerror = function() {
      console.log('Сетевая ошибка')
    }
    
    // 3. Отправить запрос
    xhr.send()

    readyState — стадии запроса

    // xhr.readyState меняется от 0 до 4:
    // 0 — UNSENT:           xhr создан, open не вызван
    // 1 — OPENED:           open() вызван
    // 2 — HEADERS_RECEIVED: получены заголовки ответа
    // 3 — LOADING:          получение тела ответа
    // 4 — DONE:             запрос завершён
    
    xhr.onreadystatechange = function() {
      console.log('readyState:', xhr.readyState)
      if (xhr.readyState === 4) {
        console.log('Готово! status:', xhr.status)
      }
    }

    Прогресс загрузки файла

    Главное преимущество XHR перед fetch — xhr.upload.onprogress:

    const xhr = new XMLHttpRequest()
    xhr.open('POST', '/api/upload')
    
    // Прогресс отправки файла
    xhr.upload.onprogress = function(event) {
      if (event.lengthComputable) {
        const percent = Math.round((event.loaded / event.total) * 100)
        console.log(`Загружено: ${percent}%`)
        progressBar.style.width = percent + '%'
      }
    }
    
    xhr.upload.onload = function() {
      console.log('Файл успешно отправлен!')
    }
    
    const formData = new FormData()
    formData.append('file', selectedFile)
    xhr.send(formData)

    Отмена запроса

    const xhr = new XMLHttpRequest()
    xhr.open('GET', '/api/slow-endpoint')
    xhr.send()
    
    // Отменить запрос
    setTimeout(() => {
      xhr.abort()         // прерывает запрос
    }, 3000)
    
    xhr.onabort = function() {
      console.log('Запрос отменён')
    }

    Заголовки запроса и ответа

    const xhr = new XMLHttpRequest()
    xhr.open('POST', '/api/data')
    
    // Установить заголовок запроса
    xhr.setRequestHeader('Content-Type', 'application/json')
    xhr.setRequestHeader('Authorization', 'Bearer my-token')
    
    xhr.onload = function() {
      // Прочитать заголовок ответа
      const contentType = xhr.getResponseHeader('Content-Type')
      const allHeaders  = xhr.getAllResponseHeaders()
      console.log(contentType)
    }
    
    xhr.send(JSON.stringify({ name: 'Иван' }))

    XHR vs fetch

    | | XHR | fetch |

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

    | API стиль | Callback-based | Promise-based |

    | Upload прогресс | Да (xhr.upload) | Нет |

    | Прервать запрос | xhr.abort() | AbortController |

    | Таймаут | xhr.timeout | Нет встроенного |

    | Читаемость | Сложно | Чисто |

    | Стриминг ответа | Нет | Да (ReadableStream) |

    Для загрузки файлов с прогрессом — XHR. Для всего остального — fetch.

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

    1. Читать responseText до onload — данные ещё не пришли

    // ПЛОХО — responseText пустой, readyState < 4
    xhr.open('GET', '/api/data')
    xhr.send()
    console.log(xhr.responseText)  // '' — запрос ещё не завершён!
    
    // ХОРОШО — читать только в onload (readyState === 4)
    xhr.onload = function() {
      console.log(xhr.responseText)  // данные готовы
    }

    2. Не проверять xhr.status — onload вызывается даже при HTTP ошибках

    // ПЛОХО — onload срабатывает и для 404, и для 500
    xhr.onload = function() {
      const data = JSON.parse(xhr.responseText)  // может быть текст ошибки!
      processData(data)
    }
    
    // ХОРОШО — проверяй статус
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        processData(JSON.parse(xhr.responseText))
      } else {
        console.error('HTTP ошибка:', xhr.status, xhr.statusText)
      }
    }

    3. Устанавливать заголовки после send()

    // ПЛОХО — заголовки нужно устанавливать ДО send()
    xhr.open('POST', '/api/data')
    xhr.send(body)
    xhr.setRequestHeader('Content-Type', 'application/json')  // слишком поздно!
    
    // ХОРОШО — setRequestHeader между open() и send()
    xhr.open('POST', '/api/data')
    xhr.setRequestHeader('Content-Type', 'application/json')  // здесь
    xhr.send(body)

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

  • Загрузка файлов с прогрессом: Dropbox, Google Drive, ВКонтакте используют XHR для отображения прогресс-баров при загрузке
  • Обёртка в Promise: библиотека axios внутри использует XHR (для браузерной версии) и оборачивает его в Promise
  • Отмена запросов: поиск с автодополнением отменяет предыдущий XHR при каждом новом вводе через xhr.abort()
  • Примеры

    Mock-класс XMLHttpRequest: симуляция open/send/onload, прогресс загрузки файла, обработка ошибок

    // Mock-реализация XMLHttpRequest для демонстрации API
    // Воспроизводит интерфейс реального XHR
    
    function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }
    
    class MockXMLHttpRequest {
      constructor() {
        this.readyState = 0       // UNSENT
        this.status = 0
        this.responseText = ''
        this.timeout = 0
        this._method = ''
        this._url = ''
        this._aborted = false
    
        // Обработчики
        this.onload = null
        this.onerror = null
        this.onabort = null
        this.onreadystatechange = null
        this.ontimeout = null
    
        // upload — отдельный объект для прогресса
        this.upload = {
          onprogress: null,
          onload: null,
        }
      }
    
      open(method, url, async = true) {
        this._method = method
        this._url = url
        this.readyState = 1  // OPENED
        this._fireReadyStateChange()
        console.log(`XHR: open(${method}, ${url})`)
      }
    
      setRequestHeader(name, value) {
        console.log(`XHR: заголовок "${name}: ${value}"`)
      }
    
      abort() {
        this._aborted = true
        console.log('XHR: запрос прерван (abort)')
        if (this.onabort) this.onabort()
      }
    
      send(body = null) {
        if (this._aborted) return
        console.log(`XHR: send() — начинаем запрос к ${this._url}`)
    
        // Симулируем асинхронный запрос
        this._simulateRequest(body)
      }
    
      async _simulateRequest(body) {
        // Имитация загрузки файла с прогрессом
        if (body && this.upload.onprogress) {
          const total = 1024 * 1024  // 1MB
          const steps = 5
    
          for (let i = 1; i <= steps; i++) {
            if (this._aborted) return
            await delay(100)
            const loaded = Math.round((total / steps) * i)
            this.upload.onprogress({ loaded, total, lengthComputable: true })
          }
    
          if (this.upload.onload) this.upload.onload()
        }
    
        // Имитация ответа сервера
        if (!this._aborted) {
          await delay(200)
          this.readyState = 2  // HEADERS_RECEIVED
          this._fireReadyStateChange()
    
          await delay(50)
          this.readyState = 3  // LOADING
          this._fireReadyStateChange()
    
          await delay(50)
    
          if (!this._aborted) {
            // Решаем: успех или ошибка (для демо — всегда успех)
            this.status = 200
            this.responseText = JSON.stringify({ ok: true, url: this._url, method: this._method })
            this.readyState = 4  // DONE
            this._fireReadyStateChange()
            if (this.onload) this.onload()
          }
        }
      }
    
      _fireReadyStateChange() {
        if (this.onreadystatechange) this.onreadystatechange()
      }
    }
    
    // --- Демо 1: Простой GET запрос ---
    console.log('=== GET запрос ===')
    const xhr1 = new MockXMLHttpRequest()
    
    xhr1.onreadystatechange = function() {
      const states = ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE']
      console.log(`readyState: ${xhr1.readyState} (${states[xhr1.readyState]})`)
    }
    
    xhr1.onload = function() {
      console.log('status:', xhr1.status)
      console.log('response:', xhr1.responseText)
    }
    
    xhr1.open('GET', 'https://api.example.com/users')
    xhr1.send()
    
    // --- Демо 2: Загрузка файла с прогрессом ---
    setTimeout(() => {
      console.log('\n=== Загрузка файла с прогрессом ===')
      const xhr2 = new MockXMLHttpRequest()
    
      xhr2.upload.onprogress = function(event) {
        const percent = Math.round((event.loaded / event.total) * 100)
        const bar = '='.repeat(Math.floor(percent / 5)).padEnd(20, ' ')
        console.log(`[${bar}] ${percent}% (${event.loaded}/${event.total} байт)`)
      }
    
      xhr2.upload.onload = function() {
        console.log('Файл отправлен на сервер!')
      }
    
      xhr2.onload = function() {
        console.log('Ответ сервера получен:', xhr2.responseText)
      }
    
      xhr2.open('POST', 'https://api.example.com/upload')
      xhr2.setRequestHeader('Content-Type', 'multipart/form-data')
      xhr2.send({ file: 'document.pdf', size: 1024 * 1024 })
    }, 800)

    Задание

    Используя класс MockXMLHttpRequest из примера, реализуй функцию fetchWithRetry(url, maxRetries) которая делает GET-запрос с повторными попытками при ошибке. При каждой неудачной попытке выводи в консоль сообщение с номером попытки. При успехе возвращай responseText, при исчерпании попыток — выбрасывай ошибку.

    Подсказка

    resolve(xhr.responseText) при успехе. attempt < maxRetries — если ещё есть попытки, вызвать makeRequest() рекурсивно. Иначе reject с описанием ошибки.

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