前端文件流、切片下载和上传:优化文件传输效率与用户体验

    |     2024年3月14日   |   web前端技术   |     0 条评论   |    334

大文件直接整包传,容易卡死、超时、断了重来。前端可用 文件流(Blob / ArrayBuffer / FileReader)处理二进制,再用 切片上传 / 切片下载 提高吞吐和断点续传体验。

文件传输是常见需求。大文件用传统方式会碰到性能和体验问题。下面按文件流、切片下载、大文件上传与断点续传、应用场景展开,示例沿用原文 React 代码。

一、前端文件流操作

文件流是用数据流去 读取、写入、展示 文件。数据流是连续数据序列;文件处理包括读内容、写数据、删除和重命名。

Blob 与 ArrayBuffer

Blob(Binary Large Object)表示大块二进制,可用构造函数或 FormData 拿到的文件生成。ArrayBuffer 是定长二进制缓冲区,用来操作文件字节。

import React, { useState } from 'react';

function FileInput() {
  const [fileContent, setFileContent] = useState('');

  // 读取文件内容到ArrayBuffer
  function readFileToArrayBuffer(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      // 注册文件读取完成后的回调函数
      reader.onload = function(event) {
        const arrayBuffer = event.target.result;
        resolve(arrayBuffer);
      };

      // 读取文件内容到ArrayBuffer
      reader.readAsArrayBuffer(file);
    });
  }

  // 将ArrayBuffer转为十六进制字符串
  function arrayBufferToHexString(arrayBuffer) {
    const uint8Array = new Uint8Array(arrayBuffer);
    let hexString = '';
    for (let i = 0; i < uint8Array.length; i++) {
      const hex = uint8Array[i].toString(16).padStart(2, '0');
      hexString += hex;
    }
    return hexString;
  }

  // 处理文件选择事件
  function handleFileChange(event) {
    const file = event.target.files[0];  // 获取选中的文件

    if (file) {
      readFileToArrayBuffer(file)
        .then(arrayBuffer => {
          const hexString = arrayBufferToHexString(arrayBuffer);
          setFileContent(hexString);
        })
        .catch(error => {
          console.error('文件读取失败:', error);
        });
    } else {
      setFileContent('请选择一个文件');
    }
  }

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <div>
        <h4>文件内容:</h4>
        <pre>{fileContent}</pre>
      </div>
    </div>
  );
}

export default FileInput;

FileInput 里用文件选择框和 <pre> 展示内容。选中后 FileReader.readAsArrayBuffer,再转十六进制字符串显示。

FileReader

FileReader 异步读文件,转成文本或二进制。常用 readAsText()、readAsArrayBuffer()。

把文件流展示到页面

文本可放文本框;图片用 <img>;音视频用 <video> / <audio>,实现在线预览。

小结:大文件常用流式处理。Blob / ArrayBuffer 管二进制,FileReader 负责读入。

二、文件切片下载

先看整体流程:

传统下载的问题

切片下载把大文件切成小段,多请求并发拉,加快整体速度。整包下发会碰到:

  1. 等待久:传完才能用
  2. 占满带宽,别人更慢
  3. 断点难:网络故障或用户中断只能整文件重来

切片如何提速

每片通常几百 KB 到几 MB,客户端并发拉:

  1. 快速启动:先下第一片就能开始
  2. 并发吃满带宽
  3. 断点续传:只补中断的那几片

切片上传代码示例:

const [selectedFile, setSelectedFile] = useState(null); 
const [progress, setProgress] = useState(0);
 // 处理文件选择事件
 function handleFileChange(event) {
   setSelectedFile(event.target.files[0]);
 }

 // 处理文件上传事件
 function handleFileUpload() {
   if (selectedFile) {
     // 计算切片数量和每个切片的大小
     const fileSize = selectedFile.size;
     const chunkSize = 1024 * 1024; // 设置切片大小为1MB
     const totalChunks = Math.ceil(fileSize / chunkSize);

     // 创建FormData对象,并添加文件信息
     const formData = new FormData();
     formData.append('file', selectedFile);
     formData.append('totalChunks', totalChunks);

     // 循环上传切片
     for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
       const start = chunkNumber * chunkSize;
       const end = Math.min(start + chunkSize, fileSize);
       const chunk = selectedFile.slice(start, end);
       formData.append(`chunk-${chunkNumber}`, chunk, selectedFile.name);
     }

     // 发起文件上传请求
     axios.post('/upload', formData, {
       onUploadProgress: progressEvent => {
         const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
         setProgress(progress);
       }
     })
       .then(response => {
         console.log('文件上传成功:', response.data);
       })
       .catch(error => {
         console.error('文件上传失败:', error);
       });
   }
 }

切片上传/下载一般是前端文件 API + 后端配合。上面这段在讲如何切片上传:

  • handleFileChange 把选中文件存进 selectedFile
  • 点上传走 handleFileUpload
  • 按 1MB 切片,用 FormData 带上文件信息和各片数据

客户端切片下载方案

  1. 服务端切分并为每片生成唯一标识
  2. 客户端拿切片列表,同时开始下第一片
  3. 按列表并发拉其余片并拼接
  4. 全部完成后合并成完整文件
function downloadFile() {
  // 发起文件下载请求
  fetch('/download', {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
  })
    .then(response => response.json())
    .then(data => {
      const totalSize = data.totalSize;
      const totalChunks = data.totalChunks;

      let downloadedChunks = 0;
      let chunks = [];

      // 下载每个切片
      for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
        fetch(`/download/${chunkNumber}`, {
          method: 'GET',
        })
          .then(response => response.blob())
          .then(chunk => {
            downloadedChunks++;
            chunks.push(chunk);

            // 当所有切片都下载完成时
            if (downloadedChunks === totalChunks) {
              // 合并切片
              const mergedBlob = new Blob(chunks);

              // 创建对象 URL,生成下载链接
              const downloadUrl = window.URL.createObjectURL(mergedBlob);

              // 创建 <a> 元素并设置属性
              const link = document.createElement('a');
              link.href = downloadUrl;
              link.setAttribute('download', 'file.txt');

              // 模拟点击下载
              link.click();

              // 释放资源
              window.URL.revokeObjectURL(downloadUrl);
            }
          });
      }
    })
    .catch(error => {
      console.error('文件下载失败:', error);
    });
}

用 Blob 生成对象 URL,动态 <a> 设 href 和 download,再模拟点击。

进度和完成态

  1. 听每片进度,汇总后更新进度条
  2. 全部完成后显示完成图标或文案

可接在切片上传示例后面:

  // 处理文件下载事件
  function handleFileDownload() {
    axios.get('/download', {
      responseType: 'blob',
      onDownloadProgress: progressEvent => {
        const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
        setProgress(progress);
      }
    })
      .then(response => {
        // 创建一个临时的URL对象用于下载
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', 'file.txt');
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      })
      .catch(error => {
        console.error('文件下载失败:', error);
      });
  }
  
  
  <button onClick={handleFileDownload}>下载文件</button>
  <div>进度:{progress}%</div>
  • 点击下载走 handleFileDownload
  • axios 设 responseType: ‘blob’ 拿二进制
  • onDownloadProgress 更新进度
  • 完成后 createObjectURL + <a download> 触发保存

三、大文件上传:问题与方案

传统上传的问题

  • 耗时长,容易超时
  • 占服务器和带宽,影响别人
  • 中断就要整文件重传
  • 进度不好展示和控制

切片上传的优势

  • 切小片多次传,更稳更快
  • 可监控进度
  • 吃浏览器并发,减轻单次压力
  • 断点续传,避免重复传已完成部分

实现方法

  • File API 拿文件,Blob.prototype.slice() 切片
  • FormData + AJAX / Fetch 发到服务器
  • 后端收片进临时存储,稍后合并
  • 听上传进度事件,更新进度条
const [file, setFile] = useState(null);  //用来存放我本地上传的文件

const chunkSize = 1024 * 1024; // 1MB 切片大小

  const upload = () => {
    if (!file) {
      alert("请选择要上传的文件!");
      return;
    }

    const chunkSize = 1024 * 1024; // 1MB

    let start = 0;
    let end = Math.min(chunkSize, file.size);

    while (start < file.size) {
      const chunk = file.slice(start, end);
      
      // 创建FormData对象
      const formData = new FormData();
      formData.append('file', chunk);

      // 发送切片到服务器
      fetch('上传接口xxxx', {
        method: 'POST',
        body: formData
      })
      .then(response => response.json())
      .then(data => {
        console.log(data);
        // 处理响应结果
      })
      .catch(error => {
        console.error(error);
        // 处理错误
      });

      start = end;
      end = Math.min(start + chunkSize, file.size);
    }
  };
  
 return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <button onClick={upload}>上传</button>
    </div>
  );
}

Upload 用 useState 管选中文件;onChange 里更新 file。点上传后按固定大小切片,FormData + fetch 逐片发送。

断点续传:记下并恢复状态

  • 前端用 localStorage / sessionStorage 存已传切片索引、大小等
  • 下次先读本地,从断点继续
  • 后端用临时目录或库记录已收切片
  • 完成前持久化状态,中断后可恢复
import React, { useState, useRef, useEffect } from 'react';

function Upload() {
  const [file, setFile] = useState(null);
  const [uploadedChunks, setUploadedChunks] = useState([]);
  const [uploading, setUploading] = useState(false);
  const uploadRequestRef = useRef();

  const handleFileChange = (event) => {
    const selectedFile = event.target.files[0];
    setFile(selectedFile);
  };

  const uploadChunk = (chunk) => {
    // 创建FormData对象
    const formData = new FormData();
    formData.append('file', chunk);

    // 发送切片到服务器
    return fetch('your-upload-url', {
      method: 'POST',
      body: formData
    })
    .then(response => response.json())
    .then(data => {
      console.log(data);
      // 处理响应结果
      return data;
    });
  };

  const upload = async () => {
    if (!file) {
      alert("请选择要上传的文件!");
      return;
    }

    const chunkSize = 1024 * 1024; // 1MB
    const totalChunks = Math.ceil(file.size / chunkSize);

    let start = 0;
    let end = Math.min(chunkSize, file.size);

    setUploading(true);

    for (let i = 0; i < totalChunks; i++) {
      const chunk = file.slice(start, end);
      const uploadedChunkIndex = uploadedChunks.indexOf(i);

      if (uploadedChunkIndex === -1) {
        try {
          const response = await uploadChunk(chunk);
          setUploadedChunks((prevChunks) => [...prevChunks, i]);

          // 保存已上传的切片信息到本地存储
          localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks));
        } catch (error) {
          console.error(error);
          // 处理错误
        }
      }

      start = end;
      end = Math.min(start + chunkSize, file.size);
    }

    setUploading(false);

    // 上传完毕,清除本地存储的切片信息
    localStorage.removeItem('uploadedChunks');
  };

  useEffect(() => {
    const storedUploadedChunks = localStorage.getItem('uploadedChunks');

    if (storedUploadedChunks) {
      setUploadedChunks(JSON.parse(storedUploadedChunks));
    }
  }, []);

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <button onClick={upload} disabled={uploading}>
        {uploading ? '上传中...' : '上传'}
      </button>
    </div>
  );
}

uploadedChunks 存已传索引;useRef 的 uploadRequestRef 预留给当前请求。

handleFileChange 更新文件;uploadChunk 发一片并返回 Promise。

upload 里算总片数,uploading=true 禁用按钮;循环里若索引不在 uploadedChunks 才传。

传完把索引写入数组和 localStorage;全部结束后清掉本地记录,并把 uploading 设回 false。

落地时要考虑服务端处理能力和磁盘,以及安全。为避免并发传同一文件把续传弄乱,用文件唯一 ID 或会话 ID 区分。

四、应用场景

后台管理系统

  • 下载报表、日志、库备份:切片更快更稳,可中断后续传
  • 上传导入包、备份:分批传并显示进度

图片 / 视频

  • 多图切片上传,完成后立刻预览
  • 大视频切片上传更可靠;下发也可用切片,不必等整文件下完再播

云存储 / 云盘

  • 分块上传:可中断再续
  • 分块下载:加速并支持恢复
  • 切片预览和在线编辑:不必整文件先下完
环节 关键 API
读文件 FileReader / ArrayBuffer / Blob
切片 file.slice(start, end),常见 1MB
上传 FormData + fetch/axios,记 localStorage 做续传
下载 按片 GET,Blob 合并后 createObjectURL 触发保存

一句话总结:大文件不要整包硬传:先用 FileReader/Blob 处理流,再按片并发上下行,并把已完成切片记下来做断点续传。

原文

转载请注明来源:前端文件流、切片下载和上传:优化文件传输效率与用户体验
本文链接地址:https://ai.zhousir.top/?p=3461
回复 取消