基于springboot+vue的分块下载文件解决思路

目录

  1. 基于fastdfs文件服务
  2. nginx+fastdfs 启用slice,range进行下载。
  3. 基于springboot的应用服务器转发nginx服务的文件请求服务。
  4. vue使用asyn和await进行请求和合并文件
  5. 添加进度条

一、fastdfs文件服务

启动fastdfs服务命令:

  1. /usr/bin/fdfs_storaged /etc/fdfs/storage.conf start
  2. /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf start

二、配置Nginx 

手动安装nginx,因为要编译模块slice模块;

要求版本必须大于等于1.9.8。

① 下载nginx

# wget -c https://nginx.org/download/nginx-1.12.1.tar.gz

② 解压

# tar -zxvf nginx-1.12.1.tar.gz

# cd nginx-1.12.1

③ 使用默认配置  (指定安装目录:./configure --prefix=/usr/local/nginx)

# ./configure --with-http_slice_module

④ 编译、安装

# make

# make install

⑤ 启动nginx

# cd /usr/local/nginx/sbin/

# ./nginx

⑥ 配置nginx.conf

 文件目录:/usr/local/nginx/conf/nginx.conf

gzip  on;
       proxy_cache_path /tmp/nginx/cache levels=1:2 keys_zone=cache:100m;
server {
          ........
location /group1/M00 {
            alias /home/fdfs_storage/data;
            slice 1m;
            proxy_cache cache;
            proxy_cache_key $uri$is_args$args$slice_range;
            proxy_set_header Range $slice_range;
            proxy_cache_valid 200 206 1h;
            proxy_set_header Range $http_range;
            #proxy_pass http://192.168.41.171:80;
           }
            ........
}

三、Springboot的转发服务

写一个转发服务,如下:

参考:

    private String targetAddr = "http://192.168.41.171";

/**
 *  定义接口
**/

	@RequestMapping(value = "/proxy", method = { RequestMethod.HEAD,RequestMethod.GET})
	public void splitDownload(@RequestParam String deptCode,HttpServletRequest request,
							  HttpServletResponse response);

//实现类,请根据你的要求修改,deptCode是项目所需要的,你可以删除这个参数
public void splitDownload(String deptCode,HttpServletRequest request, HttpServletResponse response) {
        // String url = URLDecoder.decode(request.getRequestURL().toString(), "UTF-8");
        try {
            URI uri = new URI(request.getRequestURI());
            String path = uri.getPath();
            String query = request.getQueryString();
            String target = targetAddr + path.replace("/proxy", "");
            if (query != null && !query.equals("") && !query.equals("null")) {
                target = target + "?" + query;
            }
            target = "http://192.168.41.171/group1/M00/00/13/wKgpq2ZZKp2ABhbMDZ6Pj1m7yF0717.zip";
            URI newUri = new URI(target);
            // 执行代理查询
            String methodName = request.getMethod();

            HttpMethod httpMethod = HttpMethod.resolve(methodName);
            if (httpMethod == null) {
                return;
            }
            InputStream stream = null;
            String contentType = request.getContentType();

            // 兼容文件上传的请求
            if (contentType != null && contentType.startsWith("multipart/form-data")) {
                MultipartHttpServletRequest mulReq = (MultipartHttpServletRequest) request;
                Map<String, MultipartFile> map = mulReq.getFileMap();
                List<MultipartFile> valueList = new ArrayList<MultipartFile>(map.values());
                MultiValueMap<String, Object> params = new LinkedMultiValueMap();
                for (MultipartFile file : valueList) {
                    File newFile = File.createTempFile("temp", file.getOriginalFilename());
                    FileUtils.copyInputStreamToFile(file.getInputStream(), newFile);
                    FileSystemResource resource = new FileSystemResource(newFile);
                    params.add(file.getName(), resource);

                }
                RestTemplate restTemplate = new RestTemplate();

                // 设置请求头
                HttpHeaders headers = new HttpHeaders();
                Enumeration<String> headerNames = request.getHeaderNames();
                while (headerNames.hasMoreElements()) {
                    String headerName = headerNames.nextElement();
                    System.out.println(headerName + ":" + request.getHeader(headerName));
                    headers.set(headerName, request.getHeader(headerName));
                }

                // 手动设置请求头的token信息
                headers.set("Authorization", request.getHeader("Authorization"));

                // 用HttpEntity封装整个请求报文
                HttpEntity<MultiValueMap<String, Object>> files = new HttpEntity<MultiValueMap<String, Object>>(params, headers);

                String res = restTemplate.postForEntity(target, files, String.class).getBody();
                InputStream is = new ByteArrayInputStream(res.getBytes("UTF-8"));
                stream = is;
                // 其他请求例如get post put delete都可使用
            } else {
                ClientHttpRequest delegate = new SimpleClientHttpRequestFactory().createRequest(newUri, httpMethod);
                Enumeration<String> headerNames = request.getHeaderNames();
                // 设置请求头
                while (headerNames.hasMoreElements()) {
                    String headerName = headerNames.nextElement();
                    //System.out.println("所有参数:" + headerName + ":" + request.getHeader(headerName));
                    if(headerName.equalsIgnoreCase("range")){
                        System.out.println("range:分段大小"+ request.getHeader(headerName));
                    }
                    Enumeration<String> v = request.getHeaders(headerName);
                    List<String> arr = new ArrayList<>();
                    while (v.hasMoreElements()) {
                        arr.add(v.nextElement());
                    }
                    delegate.getHeaders().addAll(headerName, arr);
                }
                StreamUtils.copy(request.getInputStream(), delegate.getBody());
                // 执行远程调用
                ClientHttpResponse clientHttpResponse = delegate.execute();
                response.setStatus(clientHttpResponse.getStatusCode().value());

                // 设置响应头
                clientHttpResponse.getHeaders().forEach((key, value) -> value.forEach(it -> {
                    response.setHeader(key, it);
                }));
                stream = clientHttpResponse.getBody();
            }

            // 将获取到的输入流再次输出到页面输出流中
            StreamUtils.copy(stream, response.getOutputStream());
        }catch(Exception e){
            e.printStackTrace();
        }

    }

四、VUE的前端实现 

获得文件大小:

export function getFileSize(data){
  return request({
    url: KHEVAL_URL + '/khRule/proxy',
    method: 'HEAD',
    data: data,
    responseType: 'arraybuffer'
  })
}
  1. 分块下载
export function downloadFile(options) {
  return request({
    url: KHEVAL_URL + '/khRule/proxy',
    method: 'get',
    isLoading: true,
    responseType: 'arraybuffer',
    headers:
    {
      'Range': `bytes=`+options,
    }
  })
}
 async splitdownload(data) {
      const CHUNK_SIZE = 1024 * 1024 * 10 // 每次下载10MB
      this.showDownloadModal = true; // 开始下载前显示模态框
      try {
        await getFileSize().then(async (res) => {
          // 获取所有响应头
          const headers = res.headers;
          // 访问特定的响应头,例如 'content-type'
          const fileSize1 = headers['content-length'];
          console.log("文件大小" + fileSize1);
          let fileSize = Number(fileSize1)
          let offset = 0;
          let i = 0;
          const fileStream = []

          let totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
          this.downloadProgress = 0; // 初始化进度为0

          while (offset < fileSize) {
            const end = Math.min(offset + CHUNK_SIZE, fileSize)
            const options = '' + offset + '-' + (end - 1) + '';
            console.log("字段长度:" + options)
            // 确保 downloadFile 返回一个包含 ArrayBuffer 的响应
            const response = await downloadFile(options);
            // 直接从响应的 data 属性获取 ArrayBuffer,并创建 Blob
            const blob = new Blob([response.data], {type: 'application/zip;charset=UTF-8'}); // 根据实际文件类型调整MIME类型

            fileStream.push(blob)
            console.log("blob第" + i + "块,文件长度是:" + fileStream.length);
            offset = end

            // 更新进度
            this.downloadProgress = Math.floor(((i + 1) / totalChunks) * 100);

            i++
          }
          // 合并所有 Blob 片段
          const mergedBlob = new Blob(fileStream, {type: 'application/zip;charset=UTF-8'});
          ActionUtils.exportFile(
            mergedBlob,
            "heweiya22.zip"
          );
          // 创建下载链接并触发下载
          // 下载完成后,确保进度为100%
          this.downloadProgress = 100;
          this.showDownloadModal = false;
          // 下载完成后,可以在这里执行其他操作,比如提示用户下载完成
          console.log("文件下载完成!");
          this.$message({
            showClose: true,
            message: "文件下载完成!",
            type: "success",
          });
        });
      }catch (error) {
        this.showDownloadModal = false;
        this.$message({
          showClose: true,
          message: "文件在下载过程当中有错误产生,错误信息:"+error.message+",请重新下载!",
          type: "error",
        });
      }

    },

五、添加进度条

  1. 先写一个模态窗口前端

参考:Model.vue

<template>
  <div v-if="showModal" class="modal-mask">
    <div class="modal-wrapper">
      <div class="modal-container">
        <div class="modal-header">
          <slot name="header">文件下载进度</slot>
        </div>
        <div class="modal-body">
          <progress :value="value" max="100" class="progress"></progress>
          <p>{{ message }}</p>
        </div>
        <div class="modal-footer">
<!--          <slot name="footer">-->
<!--            <button @click="closeModal">关闭</button>-->
<!--          </slot>-->
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    showModal: Boolean,
    value: Number,
    message: String
  },
  methods: {
    closeModal() {
      this.$emit('close');
    }
  }
};
</script>

<style scoped>
.modal-mask {
  position: fixed;
  z-index: 9999; /* 确保模态框在最前面 */
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5); /* 半透明背景 */
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
}

.modal-wrapper {
  width: 30%; /* 或其他适合的宽度 */
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

.modal-body{
  border-bottom: 1px solid #eee;
  background-color: #eee; /* 更改为你想要的颜色 */
  padding: 20px; /* 可选,调整内边距 */
}

.modal-header {
  border-bottom: 1px solid #eee;
  color: #eee;
  background-color: #026dc6; /* 更改为你想要的颜色 */
  padding: 10px; /* 可选,调整内边距 */
}

.modal-footer {
  border-top: 1px solid #eee;
  text-align: right;
  background-color: #026dc6; /* 更改为你想要的颜色 */
  padding: 10px; /* 可选,调整内边距 */
}

.modal-body {
  position: relative; /* 为进度条绝对定位提供上下文 */
  overflow: hidden; /* 防止内容溢出 */
}

.progress {
  position: absolute; /* 使用绝对定位 */
  top: 10px; /* 调整位置 */
  width: 95%; /* 确保进度条宽度为模态框宽度 */
  height: 50px; /* 自定义进度条高度 */
  margin-bottom: 0; /* 移除默认的外边距 */
}

/* 可能需要的其他样式 */
</style>

​​​​​​​

  1. 再在调用的页面添加模式窗口
<modal v-if="showDownloadModal"
       :show-modal="showDownloadModal"
       :value="downloadProgress"
       :message="'已下载 '+downloadProgress+'%'"
       @close="showDownloadModal = false">
  <!-- 可以在这里自定义头部和底部内容 -->
</modal>
  1. 添加到下载逻辑里,显示下载进度
async splitdownload(data) {
  const CHUNK_SIZE = 1024 * 1024 * 10 // 每次下载10MB
  this.showDownloadModal = true; // 开始下载前显示模态框
  try {
    await getFileSize().then(async (res) => {
      // 获取所有响应头
      const headers = res.headers;
      // 访问特定的响应头,例如 'content-type'
      const fileSize1 = headers['content-length'];
      console.log("文件大小" + fileSize1);
      let fileSize = Number(fileSize1)
      let offset = 0;
      let i = 0;
      const fileStream = []

      let totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
      this.downloadProgress = 0; // 初始化进度为0

      while (offset < fileSize) {
        const end = Math.min(offset + CHUNK_SIZE, fileSize)
        const options = '' + offset + '-' + (end - 1) + '';
        console.log("字段长度:" + options)
        // 确保 downloadFile 返回一个包含 ArrayBuffer 的响应
        const response = await downloadFile(options);
        // 直接从响应的 data 属性获取 ArrayBuffer,并创建 Blob
        const blob = new Blob([response.data], {type: 'application/zip;charset=UTF-8'}); // 根据实际文件类型调整MIME类型

        fileStream.push(blob)
        console.log("blob第" + i + "块,文件长度是:" + fileStream.length);
        offset = end

        // 更新进度
        this.downloadProgress = Math.floor(((i + 1) / totalChunks) * 100);

        i++
      }
      // 合并所有 Blob 片段
      const mergedBlob = new Blob(fileStream, {type: 'application/zip;charset=UTF-8'});
      ActionUtils.exportFile(
        mergedBlob,
        "heweiya22.zip"
      );
      // 创建下载链接并触发下载
      // 下载完成后,确保进度为100%
      this.downloadProgress = 100;
      this.showDownloadModal = false;
      // 下载完成后,可以在这里执行其他操作,比如提示用户下载完成
      console.log("文件下载完成!");
      this.$message({
        showClose: true,
        message: "文件下载完成!",
        type: "success",
      });
    });
  }catch (error) {
    this.showDownloadModal = false;
    this.$message({
      showClose: true,
      message: "文件在下载过程当中有错误产生,错误信息:"+error.message+",请重新下载!",
      type: "error",
    });
  }
}

注:修改原来的request调用方法,去掉

isLoading: true,

  • 12
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值