一个前端,爱跑步、爱吉他、爱做饭、爱生活、爱编程、爱南芳姑娘,爱我所爱。世间最温暖又无价的是阳光、空气与爱,愿它们能带你去更远的地方。

  • 文章
  • 心情
  • 照片墙
  • 留言板
  • 工具
  • 友链
  • biaoblog

    专注web开发技术分享

    new ReadableStream 前端流文件处理以及显示下载进度

    技术 199 2024-07-29 09:22

    首先后端需要支持的内容

    1.返回头中返回“content-length”字段,前端用于计算进度

    2.后端返回的content-type 需要支持以下格式


    在前端使用 ReadableStream 来实时获取进度时,支持流传输(streaming)的 Content-Type 是关键。大多数情况下,以下几种 Content-Type 是常用并且支持流传输的:


    常用的支持流传输的 Content-Type

    1. 图片类型
    • image/jpeg
    • image/png
    • image/gif
    1. 视频类型
    • video/mp4
    • video/webm
    • video/ogg
    1. 音频类型
    • audio/mpeg
    • audio/ogg
    • audio/wav
    1. 文本类型
    • text/plain
    • text/html
    • text/css
    • text/javascript
    1. 二进制数据
    • application/octet-stream
    • application/pdf
    • application/zip
    1. JSON
    • application/json
    1. 其他常见类型
    • application/xml
    • application/x-www-form-urlencoded


    不支持的格式:

    不支持流传输的格式主要是那些需要完整的文件或数据块才能被正确处理的格式。这些格式在传输过程中无法逐步解析或显示,通常是一些压缩格式或需要整体处理的数据。以下是一些常见的不支持流传输的格式:


    不支持流传输的 Content-Type

    1. 压缩文件
    • application/x-tar
    • application/gzip
    • application/x-7z-compressed
    1. ISO文件
    • application/x-iso9660-image
    1. 二进制可执行文件
    • application/octet-stream
    • application/x-msdownload
    1. 文档格式
    • application/vnd.ms-excel (XLS)
    • application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (XLSX)
    • application/msword (DOC)
    • application/vnd.openxmlformats-officedocument.wordprocessingml.document (DOCX)
    1. 特定应用程序数据
    • application/vnd.ms-powerpoint (PPT)
    • application/vnd.openxmlformats-officedocument.presentationml.presentation (PPTX)
    • application/x-matroska (MKV) - 这个格式虽然是视频文件,但它的内部结构使得逐块处理变得复杂。

    理由

    这些格式通常不支持流传输的原因包括:


    • 压缩文件:需要解压缩整个文件才能提取内容,中途解压可能导致文件损坏或内容不可用。
    • ISO文件:需要完整的映像才能被正确加载和使用。
    • 二进制可执行文件:需要整体性才能保证执行的完整性和安全性。
    • 文档格式:需要完整的文件结构才能正确打开和解析内容。



    fetch请求完整示例(下载图片,其它格式同理)

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Download Progress</title>
      </head>
      <body>
        <h1>Download Image with Progress</h1>
        <progress id="progress-bar" value="0" max="100"></progress>
        <span id="progress-percent">0%</span>
        <br />
        <img id="downloaded-image" alt="Downloaded Image" style="display: none" />
    
        <button onclick="downloadFile()">下载</button>
        <script>
          async function downloadFile(url) {
            const response = await fetch(url); // 发起网络请求获取文件
            const contentLength = response.headers.get("content-length"); // 获取文件的总长度
            if (!contentLength) {
              console.error("Content-Length response header unavailable");
              return;
            }
    
            const total = parseInt(contentLength, 10); // 将文件总长度转换为整数
    
            let loaded = 0;
    
            // 获取 ReadableStream 的 reader 对象
            const reader = response.body.getReader();
            // 创建一个新的可读流,用于处理从后端读取的数据
            const stream = new ReadableStream({
              // start 方法在流启动时调用,接受一个 controller 参数用于控制流的行为
              start(controller) {
                // push 函数用于从 reader 中读取数据并处理
                function push() {
                  // 调用 reader.read() 方法读取下一个数据块
                  reader
                    .read()
                    .then(({ done, value }) => {
                      // 如果 done 为 true,表示没有更多数据可读,流结束
                      if (done) {
                        controller.close(); // 关闭流
                        return; // 退出 push 函数
                      }
    
                      // 打印当前读取的数据块(仅用于调试)
                      console.log("value_", value);
    
                      // 累加已读取的数据长度
                      loaded += value.length;
    
                      // 计算下载进度百分比
                      const progress = (loaded / total) * 100;
    
                      // 更新进度条和进度百分比显示
                      document.getElementById("progress-bar").value = progress;
                      document.getElementById(
                        "progress-percent"
                      ).textContent = `${progress.toFixed(2)}%`;
    
                      // 将读取的数据块传给流的消费端
                      controller.enqueue(value);
    
                      // 递归调用 push 函数,继续读取下一个数据块
                      push();
                    })
                    .catch((error) => {
                      // 捕获读取错误并在控制台打印
                      console.error("Read error:", error);
                      controller.error(error); // 报告流的错误
                    });
                }
                // 启动递归读取数据块
                push();
              },
            });
                    // 将流转换为 Blob 对象
                const responseStream = new Response(stream);
                const blob = await responseStream.blob();
                const urlBlob = URL.createObjectURL(blob); // 创建 Blob URL
                    // 获取用于显示下载图片的 img 元素
                const img = document.getElementById("downloaded-image");
                img.src = urlBlob;
                img.style.display = "block";
              }
        
              // 调用 downloadFile 函数下载文件
              // downloadFile("https://biaoblog.cn:5000/uploads/1661914638476.jpg");
            </script>
          </body>
        </html>
    


    axios请求示例:

    async function downloadResources(link) {
      let date = Date.now();
      return new Promise((resolve) => {
        const path = Path.resolve(__dirname, "files", `${date}.mp4`);
        const writer = Fs.createWriteStream(path);
    
        writeLog(`当前写入文件名:${date}.mp4`);
        axios({
          url: link,
          method: "GET",
          responseType: "stream",
          onDownloadProgress: function (progressEvent) {
            let process = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
            let downloadSpeed = filterByte(progressEvent.rate);
            let progressText = `下载进度:${process}%`;
    
            const logText = `文件总大小:${filterByte(
              progressEvent.total
            )},已下载:${filterByte(
              progressEvent.loaded
            )},${progressText},当前下载速度: ${downloadSpeed}/s,预计还需要:${formatterSecond(
              progressEvent.estimated
            )}`;
    
            console.log(logText);
            writeLog(logText);
            if (process == 100) {
              console.log(`当前资源下载完成, 当前写入文件名:${date}.mp4,下载完成`);
              writeLog(`当前资源下载完成, 当前写入文件名:${date}.mp4,下载完成`);
              resolve();
            }
          },
        })
          .then((res) => {
            res.data.pipe(writer);
          })
          .catch((err) => {
            writeLog("downloadResources失败:", err);
            resolve();
          });
      });
    }
    


    拓展1:前端的ReadableStream 和 后端的EventStream有什么区别?

    `ReadableStream` 和 `EventStream`(通常指的是 Server-Sent Events, SSE)是前端和后端处理流数据的不同技术,它们各自有不同的用途和实现方式。下面是它们的主要区别:


    ### 1. **定义和用途**


    - **`ReadableStream`**:

    - **定义**:`ReadableStream` 是浏览器提供的一种流式接口,允许开发者以流式方式处理数据。例如,可以从一个网络请求中逐步读取数据块而不是一次性读取整个数据。

    - **用途**:用于处理从浏览器中发出的请求的响应数据,或用于处理浏览器端的数据流,例如文件上传和下载、实时数据处理等。


    - **`EventStream`** (Server-Sent Events, SSE):

    - **定义**:Server-Sent Events 是一种标准的 HTTP 协议扩展,允许服务器推送更新到浏览器客户端。SSE 通过持久的 HTTP 连接向客户端推送事件数据。

    - **用途**:用于在浏览器中接收从服务器推送的实时事件流,例如实时通知、消息更新等。


    ### 2. **实现方式**


    - **`ReadableStream`**:

    - **浏览器端**:使用 JavaScript 中的 `ReadableStream` 对象来创建和处理流。可以通过 `fetch` API 获取响应流,并逐步读取数据块。

    - **示例**:

        const stream = new ReadableStream({
          start(controller) {
            // 假设 reader 是从 fetch 请求中获取的 ReadableStreamDefaultReader
            function push() {
              reader.read().then(({ done, value }) => {
                if (done) {
                  controller.close();
                  return;
                }
                controller.enqueue(value);
                push();
              });
            }
            push();
          }
        });
    


    - **`EventStream`** (SSE):

    - **服务器端**:服务器发送 `Content-Type: text/event-stream` 的响应,通过长连接向客户端推送事件。

    - **浏览器端**:使用 `EventSource` 对象来接收从服务器推送的事件。

    - **示例**:

    - **服务器端**(例如 Node.js):

          const express = require('express');
          const app = express();
    
          app.get('/events', (req, res) => {
            res.setHeader('Content-Type', 'text/event-stream');
            res.setHeader('Cache-Control', 'no-cache');
            res.setHeader('Connection', 'keep-alive');
    
            // 发送事件
            setInterval(() => {
              res.write(`data: ${new Date().toISOString()}\n\n`);
            }, 1000);
          });
    
          app.listen(3000);
          ```
        - **浏览器端**:
          ```javascript
          const eventSource = new EventSource('http://localhost:3000/events');
    
          eventSource.onmessage = function(event) {
            console.log('New message:', event.data);
          };
    


    ### 3. **数据传输**


    - **`ReadableStream`**:

    - **数据传输**:处理数据块的流式传输。数据是以块的形式逐步读取和处理的。

    - **特性**:允许精细控制数据的读取和处理,可以处理大文件或需要逐步处理的数据流。


    - **`EventStream`** (SSE):

    - **数据传输**:推送事件流。数据以事件的形式从服务器推送到客户端。

    - **特性**:适用于服务器主动推送更新到客户端的场景,如实时通知或数据更新。


    ### 4. **连接管理**


    - **`ReadableStream`**:

    - **连接管理**:依赖于标准的 HTTP 请求-响应模式。每次请求都是独立的,数据流在请求生命周期内进行处理。

    - **`EventStream`** (SSE):

    - **连接管理**:使用持久的 HTTP 连接,服务器可以在连接上不断推送事件,直到客户端断开连接。

    ### 5. **兼容性和支持**


    - **`ReadableStream`**:

    - **浏览器支持**:现代浏览器支持 `ReadableStream`。但对于旧版浏览器,可能需要 polyfill 或替代方案。


    - **`EventStream`** (SSE):

    - **浏览器支持**:大部分现代浏览器支持 SSE,但某些浏览器(如 IE)不支持。


    ### 总结


    - **`ReadableStream`** 主要用于处理流式数据的读取和操作,适用于需要逐步读取和处理数据的场景。

    - **`EventStream`** (SSE) 主要用于服务器向客户端推送实时事件,适用于需要实时更新的应用场景。


    选择使用哪种技术取决于你的具体需求和应用场景。


    拓展2:如果EventSteam和sockets都是长连接的话,那么他们之间又有什么区别?

    EventStream(Server-Sent Events, SSE)和 WebSockets 都用于实时通信,但它们的工作原理、用途和特性有明显不同。以下是它们之间的主要区别:

    1. 定义和用途

    • EventStream (Server-Sent Events, SSE):
    • 定义:一种 HTTP 协议扩展,允许服务器通过持久的 HTTP 连接向客户端推送事件数据。
    • 用途:适用于从服务器向客户端推送实时更新,例如实时通知、消息流、股票价格更新等。
    • WebSocket:
    • 定义:一种全双工通信协议,通过持久的 TCP 连接实现客户端和服务器之间的实时双向通信。
    • 用途:适用于需要高频率、低延迟的双向数据交换的应用场景,例如在线聊天、实时游戏、协作编辑等。

    2. 通信模式

    • EventStream (SSE):
    • 通信模式:单向通信,从服务器到客户端。客户端不能向服务器发送数据(虽然可以通过其他方式,如传统的 HTTP 请求,来实现双向通信)。
    • 连接:基于 HTTP 协议,通过 Content-Type: text/event-stream 来维持持久的连接。
    • WebSocket:
    • 通信模式:双向通信,客户端和服务器可以互相发送消息。
    • 连接:建立持久的 TCP 连接,通过 WebSocket 协议进行通信,通常以 ws://wss:// 开头。

    3. 数据格式和协议

    • EventStream (SSE):
    • 数据格式:文本格式,事件数据以 data: 开头。每个事件用两个换行符分隔。
    • 协议:基于 HTTP 协议,使用长连接来推送事件。
    // 服务器端
    res.write('data: ' + JSON.stringify({ message: 'Hello!' }) + '\n\n');
    
    • WebSocket:
    • 数据格式:可以发送文本(如 JSON、XML)或二进制数据(如 Blob、ArrayBuffer)。
    • 协议:使用 WebSocket 协议,在初始 HTTP 握手后升级为 WebSocket 协议。
    // 服务器端(Node.js 示例)
    const WebSocket = require('ws');
    const wss = new WebSocket.Server({ port: 8080 });
    
    wss.on('connection', ws => {
      ws.on('message', message => {
        console.log(`Received message => ${message}`);
        ws.send('Hello!'); // 发送消息给客户端
      });
    });
    

    4. 支持和兼容性

    • EventStream (SSE):
    • 支持:大部分现代浏览器支持 SSE,但某些旧版本的浏览器(如 Internet Explorer)不支持。
    • 连接:浏览器内置的 EventSource API 处理 SSE。
    • WebSocket:
    • 支持:几乎所有现代浏览器都支持 WebSocket。
    • 连接:通过 WebSocket API 处理 WebSocket 连接。

    5. 性能和资源消耗

    • EventStream (SSE):
    • 性能:适用于较少量的数据推送。由于使用 HTTP 协议,适合于低频率的事件推送。
    • 资源消耗:由于使用持久的 HTTP 连接,相对较低。每个 SSE 连接占用一个 HTTP 连接。
    • WebSocket:
    • 性能:支持高频率、低延迟的双向通信。适合于需要实时互动的场景。
    • 资源消耗:TCP 连接相对较重,但提供了高效的双向数据传输。适合于需要实时反馈和频繁数据交换的应用场景。

    6. 使用场景

    • EventStream (SSE):
    • 场景:新闻推送、股票行情更新、实时通知、日志流。
    • WebSocket:
    • 场景:在线聊天、多人在线游戏、实时协作编辑、实时数据监控。

    总结

    • EventStream (SSE) 适用于从服务器向客户端推送事件的场景,提供单向的、基于 HTTP 的实时数据传输。
    • WebSocket 适用于需要双向、低延迟实时通信的场景,提供持久的 TCP 连接以实现双向通信。

    根据你的应用需求选择合适的技术,可以更好地满足性能要求和用户体验。



    拓展3:那么前端的readableStream和EventStream以及socket都有什么样的使用场景 以及原因

    前端的 ReadableStreamEventStream(Server-Sent Events, SSE)、和 WebSocket 都有不同的使用场景,根据你的需求选择合适的技术可以提高应用的性能和用户体验。下面是每种技术的使用场景及其原因:

    1. ReadableStream

    使用场景

    • 大文件下载或上传:当处理大文件的下载或上传时,使用 ReadableStream 可以逐步处理数据,避免一次性加载整个文件,减少内存占用。
    • 逐步读取数据:例如,处理大文件的内容流,或者从网络流中逐块读取数据进行处理。
    • 实时数据处理:从 HTTP 响应中流式读取数据并处理,比如在处理实时数据流时逐步更新界面。

    原因

    • 节省内存:将数据分块处理,避免一次性将整个数据加载到内存中。
    • 提升用户体验:逐步处理数据可以提供即时反馈或展示部分内容(如进度条、实时日志等)。
    • 灵活性:可以与 Response 对象、fetch API 等一起使用,实现复杂的数据处理需求。
    fetch('large-file-url')
      .then(response => {
        const reader = response.body.getReader();
        const stream = new ReadableStream({
          start(controller) {
            function push() {
              reader.read().then(({ done, value }) => {
                if (done) {
                  controller.close();
                  return;
                }
                // 处理数据块
                controller.enqueue(value);
                push();
              });
            }
            push();
          }
        });
        return new Response(stream);
      })
      .then(response => response.blob())
      .then(blob => {
        // 处理 blob 数据
      });
    

    2. EventStream (SSE)

    使用场景

    • 实时通知:从服务器向客户端推送实时通知或更新,例如社交媒体通知、消息提醒。
    • 动态数据更新:在应用中需要实时更新数据的场景,比如实时股票行情、新闻更新、日志监控。
    • 简单实时数据流:适合于从服务器推送事件数据到客户端的场景,不需要双向通信。

    原因

    • 简单易用:基于标准的 HTTP 协议,易于实现和使用,浏览器内置 EventSource API。
    • 低开销:适合于推送频率较低的场景,因为 SSE 使用的是持久的 HTTP 连接。
    • 文本数据:主要用于传输文本格式的数据,适合简单的事件通知和数据更新。
    const eventSource = new EventSource('http://example.com/events');
    
    eventSource.onmessage = function(event) {
      console.log('New message:', event.data);
    };
    
    eventSource.onerror = function(error) {
      console.error('EventSource error:', error);
    };
    

    3. WebSocket

    使用场景

    • 实时聊天应用:需要低延迟的双向通信,支持即时消息交换。
    • 在线游戏:需要实时交互,低延迟的数据交换。
    • 协作工具:多人同时编辑文档或表单,实时同步数据。
    • 实时监控:监控系统需要实时更新数据(如股票价格、系统状态)。

    原因

    • 双向通信:支持客户端和服务器之间的双向数据传输,适合需要即时反馈的应用。
    • 低延迟:提供更低的延迟和高效的数据传输,适合需要高频率交互的场景。
    • 灵活性:支持文本和二进制数据,适用于各种数据传输需求。
    const socket = new WebSocket('ws://example.com/socket');
    
    socket.onopen = function() {
      console.log('WebSocket connection established');
      socket.send('Hello Server!');
    };
    
    socket.onmessage = function(event) {
      console.log('Message from server:', event.data);
    };
    
    socket.onerror = function(error) {
      console.error('WebSocket error:', error);
    };
    

    总结

    • ReadableStream:用于处理逐块读取的数据流,适合大文件下载/上传、实时数据处理。
    • EventStream (SSE):用于从服务器向客户端推送实时事件数据,适合实时通知和动态数据更新。
    • WebSocket:用于双向实时通信,适合在线聊天、实时游戏、协作工具等需要低延迟和高频率交互的应用。

    选择合适的技术要根据你的应用需求、数据传输模式以及对实时性的要求来决定。



    拓展4:fetch在请求到结果之后 没有格式化(json(),text(),blob()) 它到底是什么格式的?

    fetch 在请求到结果之后,默认返回的是一个 Response 对象。这个 Response 对象包含了 HTTP 响应的所有元数据,包括状态码、头部信息等,并且有一个 body 属性表示响应体的数据流。

    在没有格式化的情况下,响应体的数据流 (response.body) 是一个 ReadableStream。你需要手动读取这个流并进行处理。这个流是原始的字节流,并没有具体的格式,具体的内容格式取决于服务器返回的内容。

    为了更清楚地理解,下面是一个 fetch 请求返回的默认 Response 对象结构的示例:

    fetch('https://api.example.com/data')
      .then(response => {
        // 默认返回的 Response 对象
        console.log(response);
        
        // Response 对象的结构
        /*
        Response {
          body: ReadableStream,           // 原始的字节流
          bodyUsed: false,                // 表示 body 是否已经被使用
          headers: Headers,               // 响应的头部信息
          ok: true,                       // 请求是否成功
          redirected: false,              // 请求是否重定向
          status: 200,                    // HTTP 状态码
          statusText: "OK",               // HTTP 状态文本
          type: "cors",                   // 响应类型 (basic, cors, error, opaque, opaqueredirect)
          url: "https://api.example.com/data"  // 请求的 URL
        }
        */
        
        // 示例:获取响应的头部信息
        console.log(response.headers.get('content-type'));
        
        // 示例:获取响应体的 ReadableStream
        const reader = response.body.getReader();
        
        // 手动读取数据流
        reader.read().then(({ done, value }) => {
          if (!done) {
            console.log('Received chunk:', value);
          }
        });
      })
      .catch(error => {
        console.error('Fetch error:', error);
      });
    

    在这个示例中,response.body 是一个 ReadableStream 对象,表示响应体的原始字节流。在没有进行格式化之前,它没有具体的数据格式,你可以使用各种方法(如 json(), text(), blob(), arrayBuffer(), formData())来解析这个流的内容。

    所以,总结来说:

    • fetch 请求到结果之后默认返回的是一个 Response 对象。
    • Response 对象包含响应的元数据和一个 ReadableStreamresponse.body)表示响应体的原始字节流。
    • 你需要手动调用相应的方法(如 json(), text(), blob() 等)来解析响应体的内容。


    结论就是如果要使用readableStream来处理流,在fetch之后不需要格式化,而是需要处理它原始的response,获取其中的body 即:原始的字节流



    参考文档:https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream/ReadableStream

    文章评论

    评论列表(0