Представь: ты делаешь загрузку фотографий в облако. Пользователь выбрал файл 50 МБ — нужно показать прогресс-бар с процентом. fetch не умеет отслеживать прогресс отправки. XMLHttpRequest — умеет. Именно поэтому XHR до сих пор используется, несмотря на то что fetch заменил его почти везде.
XHR — это callback-based API для HTTP-запросов с дополнительными возможностями: прогресс загрузки (xhr.upload.onprogress), таймаут (xhr.timeout), отмена (xhr.abort()). Для загрузки файлов с прогресс-баром XHR до сих пор является стандартным решением.
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()// 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 | 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)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)Представь: ты делаешь загрузку фотографий в облако. Пользователь выбрал файл 50 МБ — нужно показать прогресс-бар с процентом. fetch не умеет отслеживать прогресс отправки. XMLHttpRequest — умеет. Именно поэтому XHR до сих пор используется, несмотря на то что fetch заменил его почти везде.
XHR — это callback-based API для HTTP-запросов с дополнительными возможностями: прогресс загрузки (xhr.upload.onprogress), таймаут (xhr.timeout), отмена (xhr.abort()). Для загрузки файлов с прогресс-баром XHR до сих пор является стандартным решением.
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()// 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 | 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)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 с описанием ошибки.