前端大文件分片下载解决方案,没用你来砍我

慈云数据 2024-03-19 技术支持 119 0

大文件下载,支持断点续传

  • 前言
  • 一、背景
  • 二、代码实现
    • 1.后端
    • 2.前端
      • 创建工具类 download.js
      • localforage 相关方法
      • 3.效果展示
        • localforage数据展示
        • 下载进度展示
        • 三、总结

          前言

          好记性不如烂笔头,站在岸上学不会游泳。这次分享一是为了记录下忙碌几天的成果,方便以后查阅;二是避免别人走弯路,给别人方便就是给自己方便。

          一、背景

          客户下载超大文件(1G以上)时,下载异常断开,分析了下有以下原因:

          1. nginx配置允许下载大小超限
          2. 网络带宽限制,导致龟速下载;
          3. 下载长时间连接可能会导致超时

          针对上述问题,找到一个合理的解决方案 分片下载,既可不超限亦不需要保持长连接,如果出现网络波动断开连接,亦可断点续传。多的不说,直接上干货。

          二、代码实现

          1.后端

          创建下载工具类DownloadUtil.java

          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
          import java.io.*;
          import java.net.URLEncoder;
          public class DownloadUtil {
              /**
               * 每个分片大小 50M
               */
              private final static int BUFFER_SIZE = 1024 * 1024 * 50;
              public static void downloadByBlock(File file, HttpServletRequest request, HttpServletResponse response) throws IOException {
                  // 获取文件名
                  String fileName = file.getName();
                  // 获取文件大小
                  long fileSize = file.length();
                  // 设置返回头
                  response.setContentType("application/octect-stream;charset=UTF-8");
                  response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
                  response.setHeader("Accept-Ranges", "bytes");
                  // 判断是否包含Range
                  String rangeHeader = request.getHeader("Range");
                  if (rangeHeader == null) {
                      // 返回文件大小信息
                      response.setHeader("Content-Length", String.valueOf(fileSize));
                      response.setHeader("Buffer-Size", String.valueOf(BUFFER_SIZE));
                      response.setHeader("Access-Control-Expose-Headers", "Buffer-Size");
                  } else {
                      // 分片下载
                      long start = 0;
                      long end = fileSize - 1;
                      String[] range = rangeHeader.split("=")[1].split("-");
                      if (range.length == 1) {
                          start = Long.parseLong(range[0]);
                          end = fileSize - 1;
                      } else {
                          start = Long.parseLong(range[0]);
                          end = Long.parseLong(range[1]);
                      }
                      long contentLength = end - start + 1;
                      // 返回头里存放每次读取的开始和结束字节
                      response.setHeader("Content-Length", String.valueOf(contentLength));
                      response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
                      InputStream in = new FileInputStream(file);
                      OutputStream out = response.getOutputStream();
                      // 跳到第start字节
                      in.skip(start);
                      byte[] buffer = new byte[BUFFER_SIZE];
                      int bytesRead = -1;
                      long bytesWritten = 0;
                      while ((bytesRead = in.read(buffer)) != -1) {
                          if (bytesWritten + bytesRead > contentLength) {
                              out.write(buffer, 0, (int) (contentLength - bytesWritten));
                              break;
                          } else {
                              out.write(buffer, 0, bytesRead);
                              bytesWritten += bytesRead;
                          }
                      }
                      in.close();
                      out.close();
                  }
              }
          }
          

          后端代码借鉴其他博友 来源

          2.前端

          前端使用vue脚手架项目作为实例,也可用其他方式实现;

          当前实例需要用到的技术:

          localforage:localForage 是一个 JavaScript 库,通过简单类似 ​​localStorage​​ API 的异步存储来改进你的 Web 应用程序的离线体验,最最重要的一点是通过它可以将数据持久化至本地磁盘

          streamsaver:用于实现在Web浏览器中直接将大文件流式传输到用户设备的功能

          创建工具类 download.js

          import axios from 'axios';
          import localForage from 'localforage'
          import streamSaver from 'streamsaver'
          import store from "@/store/index"
          // 基础数据库 存放其他文件的数据库实例名
          const baseDataBaseName = "localforageInstance";
          // 数据库进度数据主键
          const progressKey = "progress";
          /**
           * @description 分片下载
           * @param fileName 文件名
           * @param url 下载地址
           * @param dataBaseName 数据库名,用于存放分片数据 每个文件唯一
           * @param progress 下载进度 type:continue 续传 again重新下载 cancel取消
           */
          export async function downloadByBlock(fileName, url, dataBaseName, progress) {
          	if(progress.type){
          		await controlFile(fileName, progress.type, dataBaseName)
          	}
          	// 创建基础数据库 用于保存文件信息
          	const baseDataBase = createInstance(baseDataBaseName);
          	// 判断数据库中是否已存在该文件的下载任务
          	let isError = false;
          	await getData(dataBaseName, baseDataBase, async(res) => {
          		if(res && !progress.type){
          			alert("已存在下载进度!");
          			isError = true;
          		}
          	})
          	if(isError){
          		return;
          	}
          	// 创建数据库 用于存储每个文件的分片数据
          	const dataBase = createInstance(dataBaseName);
          	
          	// 获取文件大小
          	const response = await axios.head(url);
          	// 文件大小 注意转为数字
          	const fileSize = +response.headers['content-length'];
          	// 分片大小 注意转为数字
          	const chunkSize = +response.headers['buffer-size'];
          	// 开始节点
          	let start = 0;
          	// 结束节点
          	let end = chunkSize -1;
          	if(fileSize  
          

          localforage 相关方法

          /**
           * @description 创建数据库实例 每个实例对象都有独立的数据库 不会影响其他实例
           * @param dataBasse 数据库名
           */
          function createInstance(dataBase){
          	return localForage.createInstance({
          		name: dataBase
          	});
          }
          /**
           * @description 保存数据
           * @param name 键名
           * @param value 数据
           * @param storeName 仓库/数据库实例
           */
          async function saveData(name, value, storeName){
          	await storeName.setItem(name,value).then(()=>{
          		// success
          	}).catch(err=>{
          		// err
          	})
          }
          /**
           * @description 获取数据
           * @param name 键名
           * @param storeName 仓库/数据库实例
           * @param callback 回调函数
           */
          async function getData(name, storeName, callback){
          	await storeName.getItem(name).then((val)=>{
          		// success
          		callback(val);
          	}).catch(err=>{
          		// err
          		callback(false);
          	})
          }
          /**
           * @description 移除数据
           * @param name 键名
           * @param storeName 仓库/数据库实例
           */
          async function removeData(name, storeName){
          	await storeName.removeItem(name).then(()=>{
          		// success
          	}).catch(err=>{
          		// err
          	})
          }
          /**
           * @description 删除数据库实例
           * @param dataBasse 数据库名
           */
          async function dropDataBase(dataBase){
          	await localForage.dropInstance({
          		name: dataBase
          	}).then(()=>{
          		// success
          	}).catch(err=>{
          		// err
          	})
          }
          

          3.效果展示

          localforage数据展示

          F12打开管理器,application -> indexedDB

          分片数据存储

          下载进度展示

          下载站列表

          三、总结

          以上就是所有核心代码,当前功能涉及到下载、暂停、续传、取消,也可动态展示下载进度,因为是纯手工敲的,所以可能会有个别错误,见谅。其他细枝末节的代码就不在这陈述,再就是功能代码之前赶得比较急,也没怎么仔细整理,有些地方可能有更优的解法,欢迎大家评论区讨论

微信扫一扫加客服

微信扫一扫加客服

点击启动AI问答
Draggable Icon