javascript - Indicatori de progres pentru încărcare la fetch?
Îmi este dificil să găsesc documentație sau exemple despre implementarea unui indicator de progres al încărcării folosind fetch.
Aceasta este singura referință pe care am găsit-o până acum, care afirmă:
Evenimentele de progres sunt o funcție de nivel înalt care nu va ajunge în fetch deocamdată. Poți să îți creezi propriul eveniment uitându-te la antetul
Content-Lengthși folosind un flux de trecere pentru a monitoriza byte-ii primiți.Aceasta înseamnă că poți gestiona explicit răspunsurile fără un
Content-Lengthîn mod diferit. Și, desigur, chiar dacăContent-Lengthexistă, poate fi o minciună. Cu stream-uri, poți gestiona aceste minciuni așa cum vrei.
Cum aș scrie „un flux prin care să monitorizez octeții” trimiși? Dacă contează în vreun fel, încerc să fac asta pentru a alimenta încărcările de imagini de la browser către Cloudinary.
NOTĂ: Nu sunt interesat de biblioteca Cloudinary JS, deoarece depinde de jQuery, iar aplicația mea nu îl folosește. Sunt interesat doar de procesarea fluxului necesară pentru a face acest lucru cu JavaScript nativ și polyfill-ul fetch de la Github.
https://fetch.spec.whatwg.org/#fetch-api
Răspuns #1
fetch: doar Chrome
Browserele lucrează la suportarea unui ReadableStream ca body al fetch-ului. Pentru Chrome, aceasta a fost implementată începând cu versiunea v105. Pentru alte browsere, în prezent nu este implementată.
(Rețineți că duplex: "half" este în prezent necesar pentru a folosi un corp de flux cu fetch.)
Un TransformStream personalizat poate fi folosit pentru a urmări progresul. Iată un exemplu funcțional:
avertisment: acest cod nu funcționează în browserele altele decât Chrome
async function main() {
const blob = new Blob([new Uint8Array(10 * 1024 * 1024)]); // any Blob, including a File
const uploadProgress = document.getElementById("upload-progress");
const downloadProgress = document.getElementById("download-progress");
const totalBytes = blob.size;
let bytesUploaded = 0;
// Use a custom TransformStream to track upload progress
const progressTrackingStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk);
bytesUploaded += chunk.byteLength;
console.log("upload progress:", bytesUploaded / totalBytes);
uploadProgress.value = bytesUploaded / totalBytes;
},
flush(controller) {
console.log("completed stream");
},
});
const response = await fetch("https://httpbin.org/put", {
method: "PUT",
headers: {
"Content-Type": "application/octet-stream"
},
body: blob.stream().pipeThrough(progressTrackingStream),
duplex: "half",
});
// After the initial response headers have been received, display download progress for the response body
let success = true;
const totalDownloadBytes = response.headers.get("content-length");
let bytesDownloaded = 0;
const reader = response.body.getReader();
while (true) {
try {
const { value, done } = await reader.read();
if (done) {
break;
}
bytesDownloaded += value.length;
if (totalDownloadBytes != undefined) {
console.log("download progress:", bytesDownloaded / totalDownloadBytes);
downloadProgress.value = bytesDownloaded / totalDownloadBytes;
} else {
console.log("download progress:", bytesDownloaded, ", unknown total");
}
} catch (error) {
console.error("error:", error);
success = false;
break;
}
}
console.log("success:", success);
}
main().catch(console.error);upload: <progress id="upload-progress"></progress><br/>
download: <progress id="download-progress"></progress>soluție de evitare: bunul vechi XMLHttpRequest
În loc de fetch(), este posibil să folosești XMLHttpRequest pentru a urmări progresul încărcării — obiectul xhr.upload emite un eveniment progress.
async function main() {
const blob = new Blob([new Uint8Array(10 * 1024 * 1024)]); // any Blob, including a File
const uploadProgress = document.getElementById("upload-progress");
const downloadProgress = document.getElementById("download-progress");
const xhr = new XMLHttpRequest();
const success = await new Promise((resolve) => {
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
console.log("upload progress:", event.loaded / event.total);
uploadProgress.value = event.loaded / event.total;
}
});
xhr.addEventListener("progress", (event) => {
if (event.lengthComputable) {
console.log("download progress:", event.loaded / event.total);
downloadProgress.value = event.loaded / event.total;
}
});
xhr.addEventListener("loadend", () => {
resolve(xhr.readyState === 4 && xhr.status === 200);
});
xhr.open("PUT", "https://httpbin.org/put", true);
xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.send(blob);
});
console.log("success:", success);
}
main().catch(console.error);upload: <progress id="upload-progress"></progress><br/>
download: <progress id="download-progress"></progress>Răspuns #2
Fluxurile încep să apară pe platforma web (https://jakearchibald.com/2016/streams-ftw/), dar este încă devreme.
În curând vei putea să oferi un flux ca fiind corpul unei cereri, dar întrebarea deschisă este dacă consumul acelui flux este legat de octeții încărcați.
Redirecționările particulare pot duce la reTransmiterea datelor către noua locație, dar fluxurile nu pot „repornire”. Putem rezolva acest lucru transformând corpul într-un callback care poate fi apelat de mai multe ori, dar trebuie să fim siguri că expunerea numărului de redirecționări nu reprezintă o breșă de securitate, deoarece ar fi prima dată pe platformă când JS ar putea detecta acest lucru.
Unii se întreabă dacă are sens să se lege consumul de stream de byte-ii încărcați.
Pe scurt: acest lucru nu este posibil încă, dar în viitor va fi gestionat fie prin fluxuri, fie printr-un fel de callback de nivel mai înalt transmis în fetch().
Răspunsul #3
Soluția mea este să folosesc axios, care suportă acest lucru destul de bine:
axios.request({
method: "post",
url: "/aaa",
data: myData,
onUploadProgress: (p) => {
console.log(p);
//this.setState({
//fileprogress: p.loaded / p.total
//})
}
}).then (data => {
//this.setState({
//fileprogress: 1.0,
//})
})
Am un exemplu pentru folosirea acestuia în React pe GitHub.
Răspunsul #4
Așa cum am explicat deja în celelalte răspunsuri, cu fetch nu este (încă) posibil în mod fiabil în toate browserele, așa că XHR rămâne metoda de urmat pentru moment. Iată soluția mea XHR puțin mai compactă:
const uploadFiles = (url, files, onProgress) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', e => onProgress(e.loaded / e.total));
xhr.addEventListener('load', () => resolve({ status: xhr.status, body: xhr.responseText }));
xhr.addEventListener('error', () => reject(new Error('File upload failed')));
xhr.addEventListener('abort', () => reject(new Error('File upload aborted')));
xhr.open('POST', url, true);
const formData = new FormData();
Array.from(files).forEach((file, index) => formData.append(index.toString(), file));
xhr.send(formData);
});
Funcționează cu un fișier sau cu mai multe fișiere.
Dacă ai un element de input pentru fișiere ca acesta:
<input type="file" multiple id="fileUpload" />
Apelează funcția astfel:
document.getElementById('fileUpload').addEventListener('change', async e => {
const onProgress = progress => console.log(`Progress: ${Math.round(progress * 100)}%`);
const response = await uploadFiles('/api/upload', e.currentTarget.files, onProgress);
if (response.status >= 400) {
throw new Error(`File upload failed - Status code: ${response.status}`);
}
console.log(`Response: ${response.body}`);
}
Funcționează și cu e.dataTransfer.files pe care îl obții dintr-un eveniment drop atunci când construiești o zonă de trimitere a fișierelor.
Răspunsul #5
Actualizare: așa cum spune răspunsul acceptat, acum este imposibil. Dar codul de mai jos ne-a rezolvat problema pentru o perioadă. Ar trebui să adaug că cel puțin a trebuit să trecem la folosirea unei biblioteci bazate pe XMLHttpRequest.
const response = await fetch(url);
const total = Number(response.headers.get('content-length'));
const reader = response.body.getReader();
let bytesReceived = 0;
while (true) {
const result = await reader.read();
if (result.done) {
console.log('Fetch complete');
break;
}
bytesReceived += result.value.length;
console.log('Received', bytesReceived, 'bytes of data so far');
}
mulțumită acestui link: https://jakearchibald.com/2016/streams-ftw/
Răspuns #6
cu fetch: acum posibil cu Chrome >= 105 🎉
Cum să: https://developer.chrome.com/articles/fetch-streaming-requests/
În prezent nu este acceptat de alte browsere (poate va fi cazul când citiți acest lucru, vă rog să editați răspunsul meu în consecință)
Detectarea caracteristicilor (sursă)
const supportsRequestStreams = (() => {
let duplexAccessed = false;
const hasContentType = new Request('', {
body: new ReadableStream(),
method: 'POST',
get duplex() {
duplexAccessed = true;
return 'half';
},
}).headers.has('Content-Type');
return duplexAccessed && !hasContentType;
})();
HTTP >= 2 necesar
Preluarea va fi respinsă dacă conexiunea este HTTP/1.x.
