อัปโหลดตัวบ่งชี้ความคืบหน้าสำหรับการดึงข้อมูล?


110

ฉันพยายามที่จะหาเอกสารหรือตัวอย่างของการใช้ตัวบ่งชี้ความคืบหน้าในการอัปโหลดโดยใช้ดึงข้อมูล

นี่เป็นข้อมูลอ้างอิงเดียวที่ฉันพบจนถึงตอนนี้ซึ่งระบุว่า:

เหตุการณ์ความคืบหน้าเป็นคุณลักษณะระดับสูงที่จะไม่มาในการดึงข้อมูลในตอนนี้ คุณสามารถสร้างของคุณเองได้โดยดูที่Content-Lengthส่วนหัวและใช้สตรีมพาสทรูเพื่อตรวจสอบไบต์ที่ได้รับ

ซึ่งหมายความว่าคุณสามารถจัดการอย่างชัดเจนโดยไม่ต้องมีการตอบสนองContent-Lengthที่แตกต่างกัน และแน่นอนว่าถึงแม้ว่าContent-Lengthมันจะเป็นเรื่องโกหกก็ตาม ด้วยสตรีมคุณสามารถจัดการกับคำโกหกเหล่านี้ได้ตามที่คุณต้องการ

ฉันจะเขียน "สตรีมพาสเพื่อตรวจสอบไบต์" ที่ส่งไปได้อย่างไร ถ้ามันทำให้การเรียงลำดับของความแตกต่างใด ๆ ฉันพยายามที่จะทำเช่นนี้เพื่ออัปโหลดภาพพลังงานจากเบราว์เซอร์Cloudinary

หมายเหตุ : ฉันไม่สนใจไลบรารี Cloudinary JSเนื่องจากขึ้นอยู่กับ jQuery และแอปของฉันไม่สนใจ ฉันสนใจเฉพาะในกระแสการประมวลผลที่จำเป็นในการทำเช่นนี้กับจาวาสคริปต์พื้นเมืองและ Github ของfetchpolyfill


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


คำตอบ:


47

สตรีมเริ่มเข้าสู่แพลตฟอร์มเว็บ ( https://jakearchibald.com/2016/streams-ftw/ ) แต่ยังเป็นช่วงแรก ๆ

ในไม่ช้าคุณจะสามารถให้สตรีมเป็นเนื้อหาของคำขอได้ แต่คำถามที่เปิดอยู่คือการใช้สตรีมนั้นเกี่ยวข้องกับไบต์ที่อัปโหลด

การเปลี่ยนเส้นทางโดยเฉพาะอาจส่งผลให้มีการส่งข้อมูลไปยังตำแหน่งใหม่ แต่สตรีมไม่สามารถ "รีสตาร์ท" ได้ เราสามารถแก้ไขได้โดยเปลี่ยนร่างกายเป็นการโทรกลับซึ่งสามารถโทรกลับได้หลายครั้ง แต่เราต้องแน่ใจว่าการเปิดเผยจำนวนการเปลี่ยนเส้นทางไม่ใช่การรั่วไหลของความปลอดภัยเนื่องจากเป็นครั้งแรกบนแพลตฟอร์มที่ JS สามารถทำได้ ตรวจพบว่า

บางคนกำลังตั้งคำถามว่าการเชื่อมโยงการใช้สตรีมกับไบต์ที่อัปโหลดนั้นสมเหตุสมผลหรือไม่

เรื่องสั้นขนาดยาว: ยังไม่สามารถทำได้ แต่ในอนาคตสิ่งนี้จะถูกจัดการโดยสตรีมหรือการโทรกลับระดับสูงบางประเภทที่ส่งผ่านเข้าfetch()มา


7
เลวมาก. ยอมรับในตอนนี้ แต่เมื่อสิ่งนี้กลายเป็นความจริงฉันหวังว่าจะมีคนโพสต์วิธีแก้ไขที่อัปเดต! :)
neezer

1
อัปเดต - แสดงความคืบหน้าด้วย fetch API โดยใช้สตรีม - twitter.com/umaar/status/917789464658890753/photo/1
Eitan Peer

2
@EitanPeer นีซ. สิ่งที่คล้ายกันจะใช้ได้กับการอัปโหลดเช่น POST หรือไม่
Michael

6
@EitanPeer แต่คำถามเกี่ยวกับความคืบหน้าในการอัปโหลดไม่ใช่ในการดาวน์โหลด
John Balvin Arias

4
ตอนนี้เป็นปี 2020 แล้วทำไมยังไม่มีวิธีทำ :(
MHA15

26

ทางออกของฉันคือใช้axiosซึ่งรองรับสิ่งนี้ได้ดี:

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

ฉันมีตัวอย่างสำหรับการใช้สิ่งนี้ในการตอบสนองบนgithub


2
นั่นคือทางออกของฉันเช่นกัน Axios ดูเหมือนจะเข้ากับแม่พิมพ์ได้ดีจริงๆ
Jason Rice

1
ไม่axiosใช้fetchหรือXMLHttpRequestภายใต้ฝากระโปรง?
ได

4
XMLHttpRequest หากคุณใช้สิ่งนี้เพื่อตอบสนองเนทีฟโปรดระวังว่า XMLHttpRequest ดูเหมือนว่าจะช้ามากในการแยกวิเคราะห์การตอบสนอง json ขนาดใหญ่เมื่อเทียบกับการดึงข้อมูล (ช้ากว่าประมาณ 10 เท่าและจะหยุดเธรด UI ทั้งหมด)
Cristiano Coelho

35
ไม่ตอบคำถาม! หากคำถามคือ "คุณทำ x ใน y ได้อย่างไร" การพูดว่า "ทำ x ใน z แทน" ไม่ใช่คำตอบที่ยอมรับได้
Derek Henderson

5
สิ่งนี้ไม่ตอบคำถามโดยเฉพาะอย่างยิ่งเนื่องจากaxiosไม่ได้ใช้fetchใต้ฝากระโปรงและไม่มีการสนับสนุนดังกล่าว ตอนนี้ฉันกำลังเขียนมันเพื่อพวกเขาอย่างแท้จริง
sgammon

9

อัปเดต: ตามคำตอบที่ยอมรับบอกว่าตอนนี้เป็นไปไม่ได้ แต่โค้ดด้านล่างนี้ช่วยจัดการปัญหาของเราได้ในบางครั้ง ฉันควรเพิ่มว่าอย่างน้อยเราต้องเปลี่ยนไปใช้ไลบรารีที่ใช้ 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');
}

ขอบคุณลิงค์นี้: https://jakearchibald.com/2016/streams-ftw/


4
ดี แต่ใช้กับการอัปโหลดด้วยหรือไม่
kernel

@kernel ฉันพยายามค้นหา แต่ไม่สามารถทำได้ และฉันก็ชอบที่จะหาวิธีอัปโหลดด้วยเช่นกัน
Hosseinmp76

3
content-length! == ความยาวของลำตัว เมื่อใช้การบีบอัด http (โดยทั่วไปสำหรับการดาวน์โหลดขนาดใหญ่) ความยาวของเนื้อหาจะเป็นขนาดหลังการบีบอัด http ในขณะที่ความยาวคือขนาดหลังจากแตกไฟล์แล้ว
Ferrybig

2
โค้ดของคุณจะถือว่าความยาวส่วนหัวของเนื้อหาระบุจำนวนไบต์ที่กำลังจะดาวน์โหลด นี้ไม่ได้เป็นจริงเสมอดังนั้นรหัสของคุณไม่สามารถแสดงความคืบหน้าให้กับผู้ใช้ในขณะที่bytesReceivedจะกลายเป็นใหญ่กว่าtotal
Ferrybig

2
ยิ่งไปกว่านั้นแม้แต่เบราว์เซอร์ก็ไม่ทราบความยาวของเนื้อหาที่แท้จริงล่วงหน้า สิ่งที่คุณจะได้รับคือตัวบ่งชี้ความคืบหน้าหลังการบีบอัด ตัวอย่างเช่นหากคุณกำลังดาวน์โหลดไฟล์ zip ที่มีอัตราส่วนการบีบอัดที่กระจายไม่สม่ำเสมอ (บางไฟล์เป็นแบบสุ่มบางไฟล์มีเอนโทรปีต่ำ) คุณจะสังเกตเห็นว่าตัวบ่งชี้ความคืบหน้าบิดเบี้ยวอย่างรุนแรง
elslooo

7

ฉันไม่คิดว่ามันจะเป็นไปได้ ร่างรัฐ:

ขณะนี้ยังขาด [ เมื่อเทียบกับ XHR ] ในการร้องขอความคืบหน้า


(คำตอบเก่า):
ตัวอย่างแรกในบท Fetch APIจะให้ข้อมูลเชิงลึกเกี่ยวกับวิธีการ:

หากคุณต้องการรับข้อมูลร่างกายอย่างต่อเนื่อง:

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))

นอกเหนือจากการใช้ตัวPromiseสร้างแอนตี้แพตเทิร์นแล้วคุณจะเห็นว่าresponse.bodyเป็นสตรีมที่คุณสามารถอ่านไบต์ทีละไบต์โดยใช้เครื่องอ่านและคุณสามารถเริ่มเหตุการณ์หรือทำอะไรก็ได้ที่คุณต้องการ (เช่นบันทึกความคืบหน้า) สำหรับทุกคน

อย่างไรก็ตามข้อมูลจำเพาะของสตรีมดูเหมือนจะไม่เสร็จสมบูรณ์และฉันไม่รู้ว่าสิ่งนี้ใช้ได้กับการดึงข้อมูลหรือไม่


14
หากฉันอ่านตัวอย่างนั้นอย่างถูกต้องนี่จะเป็นการดาวน์โหลดไฟล์ผ่านfetch. ฉันสนใจตัวบ่งชี้ความคืบหน้าสำหรับการอัปโหลดไฟล์
neezer

อ๊ะคำพูดนั้นพูดถึงการรับไบต์ซึ่งทำให้ฉันสับสน
Bergi

1
@Bergi หมายเหตุตัวPromiseสร้างไม่จำเป็น Response.body.getReader()ส่งคืน a Promise. ดูวิธีแก้ Uncaught RangeError เมื่อดาวน์โหลด json ขนาดใหญ่
guest271314

3
@ guest271314 ใช่ฉันแก้ไขที่แหล่งที่มาของคำพูดแล้ว และgetReaderไม่ไม่คืนคำสัญญา ไม่รู้ว่าสิ่งนี้เกี่ยวข้องกับโพสต์ที่คุณลิงก์ไว้อย่างไร
Bergi

@Bergi ใช่คุณถูกต้อง.getReader()ของวิธีการส่งกลับ.read() Promiseนั่นคือสิ่งที่พยายามจะสื่อ ลิงก์นี้มีไว้เพื่ออ้างถึงหลักฐานที่ว่าหากสามารถตรวจสอบความคืบหน้าสำหรับการดาวน์โหลดได้คุณสามารถตรวจสอบความคืบหน้าในการอัปโหลดได้ รวบรวมรูปแบบที่ส่งคืนผลลัพธ์ที่คาดหวังในระดับที่เห็นได้ชัด นั่นคือความคืบหน้าสำหรับการfetch()อัปโหลด ยังไม่พบวิธีechoการBlobหรือFileวัตถุที่ jsfiddle อาจจะขาดอะไรง่ายๆ การทดสอบที่localhostไฟล์อัพโหลดอย่างรวดเร็วโดยไม่เลียนแบบเงื่อนไขเครือข่าย แม้ว่าจะเพิ่งจำNetwork throttlingได้
แขก 271314

4

เนื่องจากไม่มีคำตอบใดที่ช่วยแก้ปัญหาได้

เพื่อประโยชน์ในการนำไปใช้งานคุณสามารถตรวจจับความเร็วในการอัปโหลดด้วยขนาดเริ่มต้นเล็ก ๆ ที่ทราบและเวลาในการอัปโหลดสามารถคำนวณได้ด้วยความยาวของเนื้อหา / ความเร็วในการอัปโหลด คุณสามารถใช้เวลานี้เป็นการประมาณได้


3
ใช้เคล็ดลับที่ชาญฉลาดและดีมากในขณะที่เรารอวิธีแก้ปัญหาแบบเรียลไทม์ :)
Magix

16

2

วิธีแก้ปัญหาที่เป็นไปได้คือการใช้ตัวnew Request()สร้างจากนั้นตรวจสอบRequest.bodyUsed Booleanแอตทริบิวต์

bodyUsedทะเยอทะยานแอตทริบิวต์ต้องกลับจริงถ้าdisturbed, และเท็จอย่างอื่น

เพื่อตรวจสอบว่าสตรีมเป็น distributed

วัตถุที่ดำเนินการBodymixin กล่าวจะเป็นdisturbedถ้า bodyไม่เป็นโมฆะและมันมีstreamdisturbed

กลับfetch() Promiseจากภายใน.then()ล่ามโซ่ไว้กับ recursive .read()โทรของReadableStreamเมื่อมีค่าเท่ากับRequest.bodyUsedtrue

หมายเหตุวิธีการนี้ไม่อ่านไบต์ของไบต์Request.bodyเนื่องจากไบต์ถูกสตรีมไปยังปลายทาง นอกจากนี้การอัปโหลดยังทำได้ดีก่อนที่จะตอบกลับไปยังเบราว์เซอร์ทั้งหมด

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));

}

-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}%`;
}

ฉันต้องการให้เครดิตกับ Benjamin Gruenbaum สำหรับคำตอบทั้งหมด เพราะฉันเรียนรู้จากการบรรยายของเขา
Leon Gilyadov

@LeonGilyadov บรรยายออนไลน์ได้ทุกที่หรือไม่? ลิงค์ไปยังแหล่งข้อมูลน่าจะดี
Mark Amery

@MarkAmery นี่คือ: youtube.com/watch?v=Ja8GKkxahCo (บรรยายเป็นภาษาฮิบรู)
Leon Gilyadov

13
คำถามเกี่ยวกับการอัปโหลดไม่ใช่การดาวน์โหลด
sarneeh

ปัญหาเกี่ยวกับความคืบหน้าในการดึงข้อมูลคือเมื่อคุณต้องการอัปโหลด (ไม่มีปัญหากับการดาวน์โหลด)
Kamil Kiełczewski

-7

ส่วนหนึ่งที่สำคัญคือReadableStream « obj_response .body»

ตัวอย่าง:

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() ))

คุณสามารถทดสอบการทำงานบนหน้าขนาดใหญ่เช่นhttps://html.spec.whatwg.org/และhttps://html.spec.whatwg.org/print.pdf CtrlShiftJ และโหลดโค้ดใน.

(ทดสอบบน Chrome)


1
คำตอบนี้ได้รับคะแนนลบ แต่ไม่มีใครอธิบายว่าทำไมให้คะแนนลบ - ดังนั้นฉันจึงให้ +1
Kamil Kiełczewski

6
จะได้รับ -1 จากฉันเพราะมันไม่เกี่ยวข้องกับการอัปโหลด
แบรด

ฉันเชื่อว่ามันได้ -1 เพราะดูเหมือนว่าจะเขียน javascript เวอร์ชัน
ย่อ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.