上传进度指示器以获取?

Posted

技术标签:

【中文标题】上传进度指示器以获取?【英文标题】:Upload progress indicators for fetch? 【发布时间】:2016-06-13 05:03:20 【问题描述】:

我正在努力寻找使用fetch 实现上传进度指示器的文档或示例。

This is the only reference I've found so far,其中声明:

进度事件是一项高级功能,目前无法获取。您可以通过查看 Content-Length 标头并使用传递流来监控接收到的字节来创建自己的。

这意味着您可以以不同方式显式处理没有Content-Length 的响应。当然,即使有Content-Length 也可能是谎言。通过流,您可以随心所欲地处理这些谎言。

如何编写发送的“用于监控字节的传递流”?如果它有任何不同,我正在尝试这样做以将图像从浏览器上传到Cloudinary。

注意:我对Cloudinary JS library 感兴趣,因为它依赖于 jQuery 而我的应用程序不。我只对使用原生 javascript 和 Github 的 fetch polyfill 执行此操作所需的流处理感兴趣。


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

【问题讨论】:

@Magix 见Aborting a fetch: The Next Generation #447 @guest271314 上面的链接再次用于在 HTTP 响应中使用流,而不是请求。 非常失望地看到 4 年后仍然没有使用 fetch API 的解决方案:fetch.spec.whatwg.org/#fetch-api it is currently lacking when it comes to request progression (not response progression) 现代浏览器,无 IE:developer.mozilla.org/en-US/docs/Web/API/ReadableStream 【参考方案1】:

获取:还不可能

听起来上传进度最终一旦支持ReadableStream 作为body 就可以使用 fetch。这是currently not implemented,但正在进行中。我认为代码看起来像这样:

警告:此代码尚未生效,仍在等待浏览器支持它

async function main() 
  const blob = new Blob([new Uint8Array(10 * 1024 * 1024)]); // any Blob, including a File
  const progressBar = document.getElementById("progress");

  const totalBytes = blob.size;
  let bytesUploaded = 0;

  const blobReader = blob.stream().getReader();
  const progressTrackingStream = new ReadableStream(
    async pull(controller) 
      const result = await blobReader.read();
      if (result.done) 
        console.log("completed stream");
        controller.close();
        return;
      
      controller.enqueue(result.value);
      bytesUploaded += result.value.byteLength;
      console.log("upload progress:", bytesUploaded / totalBytes);
      progressBar.value = bytesUploaded / totalBytes;
    ,
  );
  const response = await fetch("https://httpbin.org/put", 
    method: "PUT",
    headers: 
      "Content-Type": "application/octet-stream"
    ,
    body: progressTrackingStream,
  );
  console.log("success:", response.ok);

main().catch(console.error);
upload: <progress id="progress" />

解决方法:好的 ol' XMLHttpRequest

可以使用XMLHttpRequest 代替fetch() 来跟踪上传进度——xhr.upload 对象发出progress event。

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>

【讨论】:

developer.mozilla.org/en-US/docs/Web/API/ReadableStream 可能是请求正文或响应正文,或两者兼而有之? 如果您运行上面的 XHR 示例代码,您会看到它适用于请求和响应正文进度。这些是 XMLHttpRequest 上的独立事件侦听器。对于fetch()response.body 是一个可用于跟踪下载进度的流。【参考方案2】:

我的解决方案是使用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.做出反应的例子

【讨论】:

这也是我的解决方案。 Axios 似乎非常适合这种模式。 axios 是否在后台使用 fetchXMLHttpRequest XMLHttpRequest。如果您将其用于本机反应,请注意 XMLHttpRequest 与 fetch 相比,解析大型 json 响应似乎非常慢(大约慢 10 倍,并且冻结了整个 ui 线程)。 不回答问题!如果问题是“你如何在 y 中做 x?”说“do x in z”不是一个可以接受的答案。 这并不能回答问题,特别是因为axios 在后台不使用fetch,也没有这样的支持。我现在真的是在为他们创作它。【参考方案3】:

更新:正如公认的答案所说,现在不可能。但下面的代码处理了我们的问题一段时间。我应该补充一点,至少我们必须改用基于 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/

【讨论】:

很好,但它也适用于上传吗? @kernel 我试图找出答案,但我无法做到。我也想找到一种上传方式。 content-length !== 正文长度。使用http压缩时(大下载常见),content-length为http压缩后的大小,length为文件解压后的大小。 您的代码假定内容标头长度指定了获取将下载的字节数。这并不总是正确的,因此您的代码无法向用户显示进度,因为 bytesReceived 变得大于 total 此外,甚至浏览器也不事先知道实际的内容长度。您将得到的只是一个压缩后进度指示器。例如,如果您正在下载压缩比分布不均匀的 zip 文件(有些文件是随机的,有些是低熵的),您会注意到进度指示器严重倾斜。【参考方案4】:
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。因为我是从他的讲座中学到的。 @LeonGilyadov 讲座可以在任何地方在线获得吗?一个指向源的链接会很好。 @MarkAmery 在这里:youtube.com/watch?v=Ja8GKkxahCo(讲座以希伯来语进行) 问题是关于上传,而不是下载。 获取进度的问题是你想上传的时候(下载没有问题)【参考方案5】:

关键部分是ReadableStream&Lt;obj_response.body&Gt;.

示例:

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,因为它与上传无关。 我相信它得到了-1,因为它似乎正在编写缩小版 javascript【参考方案6】:

因为没有一个答案能解决问题。

为了实现,你可以检测上传速度with some small initial chunk of known size,上传时间可以用content-length/upload-speed计算。您可以将此时间用作估算值。

【讨论】:

非常聪明,在我们等待实时解决方案时使用的好技巧 :) 对我来说太冒险了。不想end up like the windows copy file progress bar 不可靠、复杂并且会显示不正确的值。【参考方案7】:

一种可能的解决方法是使用new Request() 构造函数,然后检查Request.bodyUsed Boolean 属性

bodyUsed 属性的 getter 必须在 disturbed 时返回 true,并且 否则为假。

判断流是否为distributed

如果实现Body mixin 的对象被称为disturbed body 不为空,其streamdisturbed

Request.bodyUsed 等于true 时,从.then() 中返回.then() 中的.read() 递归调用ReadableStream 中的fetch() Promise

注意,该方法不会读取 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));


【讨论】:

这完全没有结果。它只是一个非常复杂的语法,用于显示进度/微调器并在请求完成时隐藏它。【参考方案8】:

流开始登陆网络平台 (https://jakearchibald.com/2016/streams-ftw/),但仍处于早期阶段。

很快您就可以提供一个流作为请求的主体,但悬而未决的问题是该流的消耗是否与上传的字节有关。

特定的重定向可能导致数据被重新传输到新位置,但流不能“重新启动”。我们可以通过将主体转换为可以多次调用的回调来解决这个问题,但我们需要确保暴露重定向的数量不是安全漏洞,因为这将是平台上的第一次 JS 可以检测到。

有些人质疑将流消耗与上传的字节相关联是否有意义。

长话短说:这还不可能,但将来这将由流处理,或者传递给fetch() 的某种更高级别的回调。

【讨论】:

太糟糕了。现在接受这个,但是当这成为现实时,我希望其他人会发布更新的解决方案! :) @EitanPeer 不错。类似的东西是否适用于上传,例如发布? @EitanPeer 但是,问题是关于上传进度,而不是下载进度 现在是2020年了,怎么还没有办法呢:( 2021 年了,还是什么都没有?【参考方案9】:

我认为这是不可能的。草案规定:

在请求进度方面,它目前缺乏 [与 XHR 相比]


(旧答案):Fetch API chapter 中的第一个示例提供了一些关于如何:

如果要逐步接收正文数据:

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 constructor antipattern,您还可以看到response.body 是一个Stream,您可以使用Reader 逐字节读取它,您可以触发一个事件或做任何您喜欢的事情(例如记录进步)为他们每个人。

但是,Streams spec 似乎还没有完成,我不知道这是否已经在任何 fetch 实现中有效。

【讨论】:

如果我没看错的话,这将是通过fetch下载一个文件。我对上传文件的进度指示器感兴趣。 哎呀,那句话说的是接收字节,这让我很困惑。 @Bergi 注意,Promise 构造函数不是必需的。 Response.body.getReader() 返回 Promise。见How to solve Uncaught RangeError when download large size json @guest271314 是的,I've fixed it at the source 已经引用了。不,getReader 不返回承诺。不知道这与您链接的帖子有什么关系。 @Bergi 是的,你是正确的.getReader().read() 方法返回Promise。这就是试图传达的。该链接暗示了一个前提,即如果可以检查进度以进行下载,则可以检查进度以进行上传。将可在一定程度上返回预期结果的模式组合在一起;这是fetch() 上传的进度。在 jsfiddle 上还没有找到 echo BlobFile 对象的方法,可能缺少一些简单的东西。在localhost 进行的测试可以非常快速地上传文件,而不会模仿网络条件;虽然只记得Network throttling

以上是关于上传进度指示器以获取?的主要内容,如果未能解决你的问题,请参考以下文章

进度指示器的状态没有变化[重复]

如何在给定的分钟内以编程方式更新圆形百分比指示器的进度颜色?

Flutter Spinkit 进度指示器

在 UIActivityItemProvider 活动期间显示进度指示器

如何使百分比指示器以编程方式在颤动中改变颜色?

在 iCarousel 中添加独立的进度视图以指示下载