React和Vue怎么实现文件下载进度条

这篇文章主要介绍“React和Vue怎么实现文件下载进度条”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“React和Vue怎么实现文件下载进度条”文章能帮助大家解决问题。

一、需求场景

下载服务端大文件资源过慢,页面没有任何显示,体验太差。因此需增加进度条优化显示

二、实现原理

  • 发送异步HTTP请求,监听onprogress事件,读取已下载的资源和资源总大小得到下载百分比

  • 在资源请求完成后,将文件内容转为blob,并通过a标签将文件通过浏览器下载下来

三、react 实现步骤

1. 托管静态资源

前提:通过create-react-app创建的react项目

将静态资源文件放到public文件夹下,这样启动项目后,可直接通过http://localhost:3000/1.pdf 的方式访问到静态资源。在实际工作中,肯定是直接访问服务器上的资源

2. 封装hook

新建useDownload.ts

import { useCallback, useRef, useState } from 'react';

interface Options {
  fileName: string; //下载的文件名
  onCompleted?: () => void; //请求完成的回调方法
  onError?: (error: Error) => void; //请求失败的回调方法
}

interface FileDownReturn {
  download: () => void; //下载
  cancel: () => void; //取消
  progress: number; //下载进度百分比
  isDownloading: boolean; //是否下载中
}

export default function useFileDown(url: string, options: Options): FileDownReturn {
  const { fileName, onCompleted, onError } = options;
  const [progress, setProgress] = useState(0);
  const [isDownloading, setIsDownloading] = useState(false);
  const xhrRef = useRef<XMLHttpRequest | null>(null);

  const download = useCallback(() => {
    const xhr = (xhrRef.current = new XMLHttpRequest());
    xhr.open('GET', url); //默认异步请求
    xhr.responseType = 'blob';
    xhr.onprogress = (e) => {
      //判断资源长度是否可计算
      if (e.lengthComputable) {
        const percent = Math.floor((e.loaded / e.total) * 100);
        setProgress(percent);
      }
    };
    xhr.onload = () => {
      if (xhr.status === 200) {
        //请求资源完成,将文件内容转为blob
        const blob = new Blob([xhr.response], { type: 'application/octet-stream' });
        //通过a标签将资源下载
        const link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = decodeURIComponent(fileName);
        link.click();
        window.URL.revokeObjectURL(link.href);
        onCompleted && onCompleted();
      } else {
        onError && onError(new Error('下载失败'));
      }
      setIsDownloading(false);
    };
    xhr.onerror = () => {
      onError && onError(new Error('下载失败'));
      setIsDownloading(false);
    };
    xhrRef.current.send(); //发送请求
    setProgress(0); //每次发送时将进度重置为0
    setIsDownloading(true);
  }, [fileName, onCompleted, onError, url]);

  const cancel = useCallback(() => {
    xhrRef.current?.abort(); //取消请求
    setIsDownloading(false);
  }, [xhrRef]);

  return {
    download,
    cancel,
    progress,
    isDownloading,
  };
}

3. 使用hook

import { memo } from 'react';

import useFileDown from './useDownload';

const <span style="color:rgb(73 238 255)">listspan> = [
  {
    <span style="color:rgb(98 189 255)">fileNamespan>: '城市发展史起.pdf',
    <span style="color:rgb(255 111 119)">urlspan>: ' <span style="color:rgb(73 238 255)">httpspan>://<span style="color:rgb(98 189 255)">localhostspan>:3000/1.pdf',
    <span style="color:rgb(73 238 255)">typespan>: 'pdf',
  },
  {
    <span style="color:rgb(98 189 255)">fileNamespan>: '表格.xlsx',
    <span style="color:rgb(255 111 119)">urlspan>: '<span style="color:rgb(73 238 255)">httpspan>://<span style="color:rgb(98 189 255)">localhostspan>:3000/表格.xlsx',
    <span style="color:rgb(73 238 255)">typespan>: 'xlsx',
  },
  {
    <span style="color:rgb(98 189 255)">fileNamespan>: '报告.doc',
    <span style="color:rgb(255 111 119)">urlspan>: '<span style="color:rgb(73 238 255)">httpspan>://<span style="color:rgb(98 189 255)">localhostspan>:3000/报告.doc',
    <span style="color:rgb(73 238 255)">typespan>: 'doc',
  },
];
interface Options {
  <span style="color:rgb(255 111 119)">urlspan>: string;
  <span style="color:rgb(98 189 255)">fileNamespan>: string;
}

const Item = memo(({ <span style="color:rgb(255 111 119)">urlspan>, <span style="color:rgb(98 189 255)">fileNamespan> }: Options) => {
  //每项都需拥有一个属于自己的 useFileDown hook
  const { download, cancel, progress, isDownloading } = useFileDown(<span style="color:rgb(255 111 119)">urlspan>, { <span style="color:rgb(98 189 255)">fileNamespan> });

  return (
    <<span style="color:rgb(255 111 119)">divspan>>
      <span style={{ <span style="color:rgb(255 211 0)">cursorspan>: 'pointer' }} onClick={download}>
        {<span style="color:rgb(98 189 255)">fileNamespan>}
      </span>
      {isDownloading ? (
        <span>
          {`下载中:${progress}`}
          <button onClick={cancel}>取消下载</button>
        </span>
      ) : (
        ''
      )}
    </<span style="color:rgb(255 111 119)">divspan>>
  );
});

const Download = () => {
  return (
    <<span style="color:rgb(255 111 119)">divspan>>
      {<span style="color:rgb(73 238 255)">listspan>.map((<span style="color:rgb(73 238 255)">itemspan>, index) => (
        <Item <span style="color:rgb(255 111 119)">urlspan>={<span style="color:rgb(73 238 255)">itemspan>.<span style="color:rgb(255 111 119)">urlspan>} <span style="color:rgb(98 189 255)">fileNamespan>={<span style="color:rgb(73 238 255)">itemspan>.<span style="color:rgb(98 189 255)">fileNamespan>} key={index} />
      ))}
    </<span style="color:rgb(255 111 119)">divspan>>
  );
};

export default Download;

四、vue 实现步骤

1. 托管静态资源

前提:通过vite创建的vue项目

将静态资源文件放到public文件夹下,这样启动项目后,可直接通过http://127.0.0.1:5173/1.pdf 的方式访问到静态资源

2. 封装hook

新建hooks/useDownload.ts(新建hooks文件夹)

import { ref } from "vue";

export interface Options {
  fileName: string;
  onCompleted?: () => void; //请求完成的回调方法
  onError?: (error: Error) => void; //请求失败的回调方法
}

export interface FileDownReturn {
  download: () => void; //下载
  cancel: () => void; //取消
  progress: number; //下载进度百分比
  isDownloading: boolean; //是否下载中
}

export default function useFileDown(
  url: string,
  options: Options
): FileDownReturn {
  const { fileName, onCompleted, onError } = options;
  const progress = ref(0);
  const isDownloading = ref(false);

  const xhrRef = ref<XMLHttpRequest | null>(null);

  const download = () => {
    const xhr = (xhrRef.value = new XMLHttpRequest());
    xhr.open("GET", url); //默认异步请求
    xhr.responseType = "blob";
    xhr.onprogress = (e) => {
      //判断资源长度是否可计算
      if (e.lengthComputable) {
        const percent = Math.floor((e.loaded / e.total) * 100);
        progress.value = percent;
      }
    };
    xhr.onload = () => {
      if (xhr.status === 200) {
        //请求资源完成,将文件内容转为blob
        const blob = new Blob([xhr.response], {
          type: "application/octet-stream",
        });
        //通过a标签将资源下载
        const link = document.createElement("a");
        link.href = window.URL.createObjectURL(blob);
        link.download = decodeURIComponent(fileName);
        link.click();
        window.URL.revokeObjectURL(link.href);
        onCompleted && onCompleted();
      } else {
        onError && onError(new Error("下载失败"));
      }
      isDownloading.value = false;
    };
    xhr.onerror = () => {
      onError && onError(new Error("下载失败"));
      isDownloading.value = false;
    };
    xhrRef.value.send(); //发送请求
    progress.value = 0; //每次发送时将进度重置为0
    isDownloading.value = true;
  };

  const cancel = () => {
    xhrRef.value?.abort(); //取消请求
    isDownloading.value = false;
  };

  return {
    download,
    cancel,
    progress,
    isDownloading,
  };
}

3. 使用hook

  • 修改App.vue

<<span style="color:rgb(255 211 0)">scriptspan> setup lang="<span style="color:rgb(144 255 173)">tsspan>">
import <span style="color:rgb(73 238 255)">Itemspan> from "./componen<span style="color:rgb(144 255 173)">tsspan>/<span style="color:rgb(73 238 255)">Itemspan>.vue";

const list = [
  {
    <span style="color:rgb(98 189 255)">fileNamespan>: "城市发展史起.<span style="color:rgb(255 111 119)">pdfspan>",
    <span style="color:rgb(255 111 119)">urlspan>: " <span style="color:rgb(73 238 255)">httpspan>://<span style="color:rgb(255 111 119)">127span>.0.0.1:5173/1.<span style="color:rgb(255 111 119)">pdfspan>",
    <span style="color:rgb(73 238 255)">typespan>: "<span style="color:rgb(255 111 119)">pdfspan>",
  },
  {
    <span style="color:rgb(98 189 255)">fileNamespan>: "表格.<span style="color:rgb(73 238 255)">xlsxspan>",
    <span style="color:rgb(255 111 119)">urlspan>: "<span style="color:rgb(73 238 255)">httpspan>://<span style="color:rgb(255 111 119)">127span>.0.0.1:5173/表格.<span style="color:rgb(73 238 255)">xlsxspan>",
    <span style="color:rgb(73 238 255)">typespan>: "<span style="color:rgb(73 238 255)">xlsxspan>",
  },
  {
    <span style="color:rgb(98 189 255)">fileNamespan>: "报告.<span style="color:rgb(255 111 119)">docspan>",
    <span style="color:rgb(255 111 119)">urlspan>: "<span style="color:rgb(73 238 255)">httpspan>://<span style="color:rgb(255 111 119)">127span>.0.0.1:5173/报告.<span style="color:rgb(255 111 119)">docspan>",
    <span style="color:rgb(73 238 255)">typespan>: "<span style="color:rgb(255 111 119)">docspan>",
  },
];
</<span style="color:rgb(255 211 0)">scriptspan>>

<<span style="color:rgb(98 189 255)">templatespan>>
  <<span style="color:rgb(255 111 119)">divspan>>
    <<span style="color:rgb(255 111 119)">divspan> v-for="(<span style="color:rgb(73 238 255)">itemspan>, <span style="color:rgb(255 95 0)">indexspan>) in list" :key="<span style="color:rgb(255 95 0)">indexspan>">
      <<span style="color:rgb(73 238 255)">Itemspan> :<span style="color:rgb(255 111 119)">urlspan>="<span style="color:rgb(73 238 255)">itemspan>.<span style="color:rgb(255 111 119)">urlspan>" :<span style="color:rgb(98 189 255)">fileNamespan>="<span style="color:rgb(73 238 255)">itemspan>.<span style="color:rgb(98 189 255)">fileNamespan>"<<span style="color:rgb(255 211 0)">scriptspan> setup lang="<span style="color:rgb(144 255 173)">tsspan>">
import useFileDown from "../hooks/<span style="color:rgb(98 189 255)">useDownloadspan>.<span style="color:rgb(144 255 173)">tsspan>";


const props = <span style="color:rgb(98 189 255)">definePropsspan><{ <span style="color:rgb(255 111 119)">urlspan>: string; <span style="color:rgb(98 189 255)">fileNamespan>: string }>();

const { <span style="color:rgb(255 111 119)">urlspan>, <span style="color:rgb(98 189 255)">fileNamespan> } = props;

const { <span style="color:rgb(98 189 255)">downloadspan>, <span style="color:rgb(255 211 0)">cancelspan>, progress, <span style="color:rgb(98 189 255)">isDownloadingspan> } = useFileDown(<span style="color:rgb(255 111 119)">urlspan>, {
  <span style="color:rgb(98 189 255)">fileNamespan>,
});
</<span style="color:rgb(255 211 0)">scriptspan>>

<<span style="color:rgb(98 189 255)">templatespan>>
  <<span style="color:rgb(255 111 119)">divspan>>
    <span  @click="<span style="color:rgb(98 189 255)">downloadspan>">
      {{ <span style="color:rgb(98 189 255)">fileNamespan> }}
    </span>
    <span v-if="<span style="color:rgb(98 189 255)">isDownloadingspan>">
      下载中:{{ progress }} <button @click="<span style="color:rgb(255 211 0)">cancelspan>">取消下载</button></span
    >
  </<span style="color:rgb(255 111 119)">divspan>>
</<span style="color:rgb(98 189 255)">templatespan>> />
    </<span style="color:rgb(255 111 119)">divspan>>
  </<span style="color:rgb(255 111 119)">divspan>>
</<span style="color:rgb(98 189 255)">templatespan>>
  • 新建components/Item.vue

<<span style="color:rgb(255 211 0)">scriptspan> setup lang="<span style="color:rgb(144 255 173)">tsspan>">
import useFileDown from "../hooks/<span style="color:rgb(98 189 255)">useDownloadspan>.<span style="color:rgb(144 255 173)">tsspan>";


const props = <span style="color:rgb(98 189 255)">definePropsspan><{ <span style="color:rgb(255 111 119)">urlspan>: string; <span style="color:rgb(98 189 255)">fileNamespan>: string }>();

const { <span style="color:rgb(255 111 119)">urlspan>, <span style="color:rgb(98 189 255)">fileNamespan> } = props;

const { <span style="color:rgb(98 189 255)">downloadspan>, <span style="color:rgb(255 211 0)">cancelspan>, progress, <span style="color:rgb(98 189 255)">isDownloadingspan> } = useFileDown(<span style="color:rgb(255 111 119)">urlspan>, {
  <span style="color:rgb(98 189 255)">fileNamespan>,
});
</<span style="color:rgb(255 211 0)">scriptspan>>

<<span style="color:rgb(98 189 255)">templatespan>>
  <<span style="color:rgb(255 111 119)">divspan>>
    <span  @click="<span style="color:rgb(98 189 255)">downloadspan>">
      {{ <span style="color:rgb(98 189 255)">fileNamespan> }}
    </span>
    <span v-if="<span style="color:rgb(98 189 255)">isDownloadingspan>">
      下载中:{{ progress }} <button @click="<span style="color:rgb(255 211 0)">cancelspan>">取消下载</button></span
    >
  </<span style="color:rgb(255 111 119)">divspan>>
</<span style="color:rgb(98 189 255)">templatespan>>

五、可能遇到的问题:lengthComputable为false

原因一:后端响应头没有返回Content-Length;

解决办法:让后端加上就行

原因二:开启了gzip压缩

开启gzip之后服务器默认开启文件分块编码(响应头返回Transfer-Encoding: chunked)。分块编码把「报文」分割成若干个大小已知的块,块之间是紧挨着发送的。采用这种传输方式进行响应时,不会传Content-Length这个首部信息,即使带上了也是不准确的

分别为gzip压缩,分块编码:

React和Vue怎么实现文件下载进度条

例如有个877k大小的js文件,网络请求的大小为247k。但是打印的e.loaded最终返回的是877k

React和Vue怎么实现文件下载进度条

解决方法:后端把文件大小存储到其他字段,比如:header['x-content-length']

关于“React和Vue怎么实现文件下载进度条”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注盛行IT行业资讯频道,小编每天都会为大家更新不同的知识点。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: