Przesyłanie wskaźników postępu pobierania?

105

Trudno mi znaleźć dokumentację lub przykłady implementacji wskaźnika postępu przesyłania za pomocą funkcji pobierania .

To jedyne odniesienie, jakie do tej pory znalazłem , które stwierdza:

Zdarzenia postępu to funkcja wysokiego poziomu, która na razie nie zostanie pobrana. Możesz utworzyć własny, patrząc na Content-Lengthnagłówek i używając strumienia przekazującego do monitorowania odebranych bajtów.

Oznacza to, że możesz jawnie obsługiwać odpowiedzi bez Content-Lengthinaczej. I oczywiście, nawet jeśli Content-Lengthistnieje, może to być kłamstwo. Dzięki strumieniom możesz obsługiwać te kłamstwa, jak chcesz.

Jak napisać „przekazujący strumień do monitorowania wysłanych bajtów”? Jeśli to robi jakąkolwiek różnicę, próbuję to zrobić, aby zasilić przesyłanie obrazów z przeglądarki do Cloudinary .

UWAGA : Ja nie interesuje się biblioteki Cloudinary JS , ponieważ zależy to od jQuery i moja aplikacja nie robi. Interesuje mnie tylko przetwarzanie strumienia niezbędne do zrobienia tego z natywnym javascriptem i fetchpolyfillem Githuba.


https://fetch.spec.whatwg.org/#fetch-api

neezer
źródło

Odpowiedzi:

46

Strumienie zaczynają lądować na platformie internetowej ( https://jakearchibald.com/2016/streams-ftw/ ), ale to dopiero początek.

Wkrótce będziesz w stanie dostarczyć strumień jako treść żądania, ale otwarte pytanie dotyczy tego, czy zużycie tego strumienia dotyczy przesłanych bajtów.

Poszczególne przekierowania mogą powodować retransmisję danych do nowej lokalizacji, ale strumienie nie mogą się ponownie „restartować”. Możemy to naprawić, zmieniając treść w wywołanie zwrotne, które można wywołać wiele razy, ale musimy mieć pewność, że ujawnienie liczby przekierowań nie jest wyciekiem bezpieczeństwa, ponieważ byłby to pierwszy raz na platformie, gdy JS mógłby wykryć to.

Niektórzy zastanawiają się, czy w ogóle sensowne jest powiązanie zużycia strumienia z przesłanymi bajtami.

Krótko mówiąc: nie jest to jeszcze możliwe, ale w przyszłości będzie to obsługiwane przez strumienie lub jakiś rodzaj wywołania zwrotnego wyższego poziomu przekazanego do fetch().

JaffaTheCake
źródło
7
Szkoda. Akceptuję to na razie, ale kiedy stanie się to rzeczywistością, mam nadzieję, że ktoś inny opublikuje zaktualizowane rozwiązanie! :)
neezer
1
Aktualizacja - pokazująca postęp w pobieraniu API za pomocą strumieni - twitter.com/umaar/status/917789464658890753/photo/1
Eitan Peer
2
@EitanPeer Nice. Czy podobna rzecz zadziała przy wgrywaniu, np. POST?
Michael
4
@EitanPeer Ale pytanie dotyczy postępu w przesyłaniu, a nie w pobieraniu
John Balvin Arias
1
teraz jest rok 2020, dlaczego nadal nie ma sposobu, aby to zrobić :(
MHA15
25

Moim rozwiązaniem jest użycie axios , które całkiem dobrze to obsługuje:

      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,
        //})
      })

Mam przykład wykorzystania tego w reakcji na githubie.

dwjohnston
źródło
2
To też było moje rozwiązanie. Axios wydaje się naprawdę dobrze pasować do formy.
Jason Rice
1
Czy axiosużywa fetchlub XMLHttpRequestpod maską?
Dai
3
XMLHttpRequest. Jeśli używasz tego do reagowania natywnego, uważaj, że XMLHttpRequest wydaje się BARDZO BARDZO wolno analizować duże odpowiedzi json w porównaniu z pobieraniem (około 10 razy wolniej i zawiesza cały wątek interfejsu użytkownika).
Cristiano Coelho
29
Nie odpowiada na pytanie! Jeśli pytanie brzmi „jak zrobić x w y?” powiedzenie „zrób x zamiast z” nie jest akceptowalną odpowiedzią.
Derek Henderson
4
To nie odpowiada na pytanie, zwłaszcza że axiosnie używa się go fetchpod maską i nie ma takiego wsparcia. Dosłownie tworzę to teraz dla nich, więc.
sgammon
7

Nie sądzę, żeby to było możliwe. Projekt stanowi:

obecnie brakuje [ w porównaniu do XHR ], jeśli chodzi o progresję żądań


(stara odpowiedź):
Pierwszy przykład w rozdziale Fetch API daje wgląd w to, jak:

Jeśli chcesz otrzymywać dane ciała stopniowo:

function consume(reader) {
  var total = 0
  return new Promise((resolve, reject) => {
    function pump() {
      reader.read().then(({done, value}) => {
        if (done) {
          resolve()
          return
        }
        total += value.byteLength
        log(`received ${value.byteLength} bytes (${total} bytes in total)`)
        pump()
      }).catch(reject)
    }
    pump()
  })
}

fetch("/music/pk/altes-kamuffel.flac")
  .then(res => consume(res.body.getReader()))
  .then(() => log("consumed the entire body without keeping the whole thing in memory!"))
  .catch(e => log("something went wrong: " + e))

Oprócz tego, że używają Promisekonstruktora antipattern , możesz zobaczyć, że response.bodyjest to strumień, z którego możesz czytać bajt po bajcie za pomocą czytnika, i możesz uruchomić zdarzenie lub zrobić, co chcesz (np. Zarejestrować postęp) dla każdego z nich.

Jednak specyfikacja strumieni nie wydaje się być całkowicie ukończona i nie mam pojęcia, czy to już działa w jakiejkolwiek implementacji pobierania.

Bergi
źródło
13
Jeśli jednak dobrze przeczytam ten przykład, oznaczałoby to pobranie pliku przez fetch. Interesują mnie wskaźniki postępu przesyłania pliku.
neezer
Ups, ten cytat mówi o odbieraniu bajtów, co mnie zdezorientowało.
Bergi
1
@Bergi Uwaga, Promisekonstruktor nie jest konieczny. Response.body.getReader()zwraca a Promise. Zobacz, jak rozwiązać błąd Uncaught Range podczas pobierania pliku
JSON w
3
@ guest271314 tak, już naprawiłem to u źródła cytatu. I nie, getReadernie zwraca obietnicy. Nie mam pojęcia, co to ma wspólnego z postem, do którego utworzyłeś łącze.
Bergi
@Bergi Tak, masz rację .getReader(), .read()metoda zwraca a Promise. To właśnie próbowałem przekazać. Link ma nawiązywać do założenia, że ​​jeśli postęp można sprawdzić pod kątem pobierania, postęp można sprawdzić pod kątem przesyłania. Stwórz wzór, który zwraca oczekiwany rezultat w znacznym stopniu; to jest postęp w fetch()przesyłaniu. Nie znaleźli sposób na lub obiekt w jsfiddle, prawdopodobnie brakuje czegoś prostego. Testowanie przy wysyłaniu pliku bardzo szybko, bez naśladowania warunków sieciowych; choć właśnie zapamiętany . echoBlobFilelocalhostNetwork throttling
gość271314
7

Aktualizacja: zgodnie z przyjętą odpowiedzią jest to teraz niemożliwe. ale poniższy kod rozwiązał nasz problem przez jakiś czas. Powinienem dodać, że przynajmniej musieliśmy przejść do korzystania z biblioteki opartej na 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');
}

dzięki temu linkowi: https://jakearchibald.com/2016/streams-ftw/

Hosseinmp76
źródło
3
Świetnie, ale czy dotyczy to również przesyłania?
kernel
@kernel Próbowałem się dowiedzieć, ale nie byłem w stanie tego zrobić. i lubię znaleźć sposób, aby to zrobić również w przypadku przesyłania.
Hosseinmp76,
2
content-length! == długość ciała. Gdy używana jest kompresja http (typowa dla dużych pobrań), długość zawartości to rozmiar po kompresji http, a długość to rozmiar po wyodrębnieniu pliku.
Ferrybig,
1
W Twoim kodzie założono, że długość nagłówka zawartości określa liczbę bajtów, które ma zostać pobrane. Nie zawsze jest to prawdą, więc twój kod nie może pokazać postępów użytkownikowi, ponieważ bytesReceivedstaje się większy niżtotal
Ferrybig
1
Co więcej, nawet przeglądarka nie zna wcześniej rzeczywistej długości treści. Wszystko, co otrzymasz, to wskaźnik postępu po kompresji. Na przykład, jeśli pobierasz plik zip z nierównomiernie rozłożonym współczynnikiem kompresji (niektóre pliki są losowe, inne mają niską entropię), zauważysz, że wskaźnik postępu jest mocno wypaczony.
elslooo
4

Ponieważ żadna z odpowiedzi nie rozwiązuje problemu.

Ze względu na implementację możesz wykryć prędkość wysyłania za pomocą niewielkiej początkowej porcji o znanym rozmiarze, a czas przesyłania można obliczyć za pomocą długości treści / szybkości wysyłania. Możesz wykorzystać ten czas jako oszacowanie.

Shishir Arora
źródło
3
Bardzo sprytna, fajna sztuczka do użycia, podczas gdy czekamy na rozwiązanie w czasie rzeczywistym :)
Magix
16
To dla mnie zbyt ryzykowne. Nie chciałbym skończyć jak pasek postępu kopiowania plików w systemie Windows
Jack Giffin
2

Możliwym obejściem byłoby użycie new Request()konstruktora, a następnie sprawdzenia Request.bodyUsed Booleanatrybutu

Funkcja bodyUsedpobierająca atrybut musi zwracać wartość true if disturbed, aw przeciwnym razie wartość false.

aby określić, czy strumień jest distributed

BodyMówi się, że obiekt implementujący mixin jest disturbedif bodyjest różny od null i tak streamjest disturbed.

Zwraca połączenie fetch() Promisez wewnątrz .then()do rekurencyjnego .read()wywołania ReadableStreamkiedy Request.bodyUsedjest równe true.

Uwaga: podejście nie odczytuje bajtów, Request.bodyponieważ bajty są przesyłane strumieniowo do punktu końcowego. Ponadto przesyłanie może zakończyć się na długo przed zwróceniem pełnej odpowiedzi do przeglądarki.

const [input, progress, label] = [
  document.querySelector("input")
  , document.querySelector("progress")
  , document.querySelector("label")
];

const url = "/path/to/server/";

input.onmousedown = () => {
  label.innerHTML = "";
  progress.value = "0"
};

input.onchange = (event) => {

  const file = event.target.files[0];
  const filename = file.name;
  progress.max = file.size;

  const request = new Request(url, {
    method: "POST",
    body: file,
    cache: "no-store"
  });

  const upload = settings => fetch(settings);

  const uploadProgress = new ReadableStream({
    start(controller) {
        console.log("starting upload, request.bodyUsed:", request.bodyUsed);
        controller.enqueue(request.bodyUsed);
    },
    pull(controller) {
      if (request.bodyUsed) {
        controller.close();
      }
      controller.enqueue(request.bodyUsed);
      console.log("pull, request.bodyUsed:", request.bodyUsed);
    },
    cancel(reason) {
      console.log(reason);
    }
  });

  const [fileUpload, reader] = [
    upload(request)
    .catch(e => {
      reader.cancel();
      throw e
    })
    , uploadProgress.getReader()
  ];

  const processUploadRequest = ({value, done}) => {
    if (value || done) {
      console.log("upload complete, request.bodyUsed:", request.bodyUsed);
      // set `progress.value` to `progress.max` here 
      // if not awaiting server response
      // progress.value = progress.max;
      return reader.closed.then(() => fileUpload);
    }
    console.log("upload progress:", value);
    progress.value = +progress.value + 1;
    return reader.read().then(result => processUploadRequest(result));
  };

  reader.read().then(({value, done}) => processUploadRequest({value,done}))
  .then(response => response.text())
  .then(text => {
    console.log("response:", text);
    progress.value = progress.max;
    input.value = "";
  })
  .catch(err => console.log("upload error:", err));

}
gość271314
źródło
-2
const req = await fetch('./foo.json');
const total = Number(req.headers.get('content-length'));
let loaded = 0;
for await(const {length} of req.body.getReader()) {
  loaded = += length;
  const progress = ((loaded / total) * 100).toFixed(2); // toFixed(2) means two digits after floating point
  console.log(`${progress}%`); // or yourDiv.textContent = `${progress}%`;
}
Leon Gilyadov
źródło
Chcę podziękować Benjaminowi Gruenbaumowi za całą odpowiedź. Ponieważ nauczyłem się tego z jego wykładu.
Leon Gilyadov
@LeonGilyadov Czy wykład jest dostępny w dowolnym miejscu online? Link do źródła byłby miły.
Mark Amery,
@MarkAmery Oto jest: youtube.com/watch?v=Ja8GKkxahCo (wykład został wygłoszony w języku hebrajskim)
Leon Gilyadov
13
Pytanie dotyczy przesyłania, a nie pobierania.
sarneeh
problem z postępem pobierania jest kiedy chcesz wgrać (nie ma problemu z pobraniem)
Kamil Kiełczewski
-6

Kluczową częścią jest ReadableStreamobj_response .body≫.

Próba:

let parse=_/*result*/=>{
  console.log(_)
  //...
  return /*cont?*/_.value?true:false
}

fetch('').
then(_=>( a/*!*/=_.body.getReader(), b/*!*/=z=>a.read().then(parse).then(_=>(_?b:z=>z)()), b() ))

Możesz przetestować uruchomienie go na ogromnej stronie, np. Https://html.spec.whatwg.org/ i https://html.spec.whatwg.org/print.pdf . CtrlShiftJ i załaduj kod w.

(Testowane w Chrome).

Pacerier
źródło
1
Ta odpowiedź ma minus punkty, ale nikt nie wyjaśnia, dlaczego daje minus punkt - więc daję +1
Kamil Kiełczewski
5
Dostaje ode mnie -1, ponieważ nie ma znaczenia przy przesyłaniu .
Brad
Wydaje mi się, że dostał -1, ponieważ wydaje się, że pisze javascript w
zminimalizowanej