Уявіть, що ви завантажуєте відеофайл розміром 2 ГБ. На 99% інтернет зникає на секунду.
Зі стандартним fetch або submit форми вам доведеться починати все спочатку. Це поганий UX, особливо на мобільних мережах.
Вбудованого "resume" у браузерах немає. Але ми можемо реалізувати його самі, використовуючи простий алгоритм: розрізати файл і домовлятися з сервером.
Ідея проста: "Запитай сервер, що він має, і дошли решту".
Нам потрібен унікальний ідентифікатор файлу, щоб сервер розумів, про який файл йде мова при наступному запиті.
Надійна комбінація: name + size + lastModified.
Перед відправкою (або після помилки) питаємо сервер: "Скільки байт цього файлу ти вже маєш?".
Використовуємо file.slice(startByte) щоб відрізати вже завантажену частину і відправити тільки те, що залишилось.
Ми створимо клас Uploader, який капсулює цю логіку. Ми використаємо XMLHttpRequest для progress bar, оскільки fetch поки не вміє відстежувати upload progress.
class Uploader {
constructor(file, options) {
this.file = file
this.options = options || {}
// Унікальний ID файлу
this.fileId = file.name + '-' + file.size + '-' + file.lastModified
}
// Запитуємо у сервера, скільки байт вже завантажено
async getUploadedBytes() {
const response = await fetch('/status', {
headers: {
'X-File-Id': this.fileId,
},
})
if (response.status !== 200) {
throw new Error("Can't get uploaded bytes: " + response.statusText)
}
const text = await response.text()
return +text // Перетворюємо рядок "1024" в число 1024
}
// Головний метод завантаження
async upload() {
// 1. Скільки ми вже завантажили?
this.startByte = await this.getUploadedBytes()
console.log(`Відновлюємо з ${this.startByte} байт`)
if (this.startByte >= this.file.size) {
console.log('Файл вже повністю завантажено!')
return
}
// 2. Створюємо запит
const xhr = new XMLHttpRequest()
this.xhr = xhr
xhr.open('POST', '/upload', true)
// IDs для сервера
xhr.setRequestHeader('X-File-Id', this.fileId)
xhr.setRequestHeader('X-Start-Byte', this.startByte)
// 3. Відстежуємо прогрес (Upload Events)
xhr.upload.onprogress = (e) => {
// e.loaded - скільки відправлено в ЦЬОМУ запиті
// this.startByte - скільки вже БУЛО на сервері
// totalLoaded - реальний прогрес
const totalLoaded = this.startByte + e.loaded
const totalSize = this.file.size // Важливо використовувати original file size
const percent = Math.round((totalLoaded / totalSize) * 100)
console.log(`Прогрес: ${percent}%`)
// Викликаємо колбек, якщо передали
if (this.options.onProgress) {
this.options.onProgress(percent, totalLoaded, totalSize)
}
}
// 4. Відправляємо тільки залишок!
// Метод file.slice() працює миттєво і не копіює дані в пам'ять
const chunk = this.file.slice(this.startByte)
return new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.status === 200) {
resolve('Success')
} else {
reject(new Error('Upload failed: ' + xhr.statusText))
}
}
xhr.onerror = () => reject(new Error('Network Error'))
xhr.onabort = () => resolve('Aborted')
xhr.send(chunk)
})
}
stop() {
if (this.xhr) {
this.xhr.abort()
}
}
}
Ось як це виглядає в інтерфейсі:
<input type="file" id="fileInput" />
<button id="uploadBtn">Upload</button>
<button id="stopBtn">Stop</button>
<div id="progress">0%</div>
<script>
let uploader
document.getElementById('uploadBtn').onclick = async () => {
const file = document.getElementById('fileInput').files[0]
if (!file) return
uploader = new Uploader(file, {
onProgress: (percent) => {
document.getElementById('progress').innerText = percent + '%'
},
})
try {
await uploader.upload()
console.log('Завантаження завершено!')
} catch (err) {
console.error('Помилка:', err)
// Тут можна автоматично спробувати ще раз через 3 сек
// setTimeout(() => uploader.upload(), 3000);
}
}
document.getElementById('stopBtn').onclick = () => {
if (uploader) uploader.stop()
}
</script>
Звісно, це працюватиме тільки якщо сервер підтримує таку логіку. Сервер повинен:
X-File-Id та X-Start-Byte.X-Start-Byte > 0, відкривати файл у режимі append (дописування).startByte === 0), створювати його.proxy_request_buffering off; у Nginx).File Slice
blob.slice() — це магія, що дозволяє нам відправляти файл шматками без завантаження всього файлу в оперативну пам'ять.Resumable
Server Side
XMLHttpRequest - AJAX та низькорівневі запити
Повний посібник з XMLHttpRequest (AJAX). Історія, життєвий цикл, синхронні/асинхронні запити, прогрес завантаження, робота з бінарними даними та порівняння з Fetch API.
Cookies, document.cookie та світ після "Cookiepocalypse"
Енциклопедія про HTTP Cookies. Від `document.cookie` до атрибутів безпеки (SameSite, Secure), боротьби з CSRF/XSS та майбутнього без сторонніх cookies.