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 list = [
  {
    fileName: '城市發展史起.pdf',
    url: ' http://localhost:3000/1.pdf',
    type: 'pdf',
  },
  {
    fileName: '表格.xlsx',
    url: 'http://localhost:3000/表格.xlsx',
    type: 'xlsx',
  },
  {
    fileName: '報告.doc',
    url: 'http://localhost:3000/報告.doc',
    type: 'doc',
  },
];
interface Options {
  url: string;
  fileName: string;
}

const Item = memo(({ url, fileName }: Options) => {
  //每項都需擁有一個屬於自己的 useFileDown hook
  const { download, cancel, progress, isDownloading } = useFileDown(url, { fileName });

  return (
    <div>
      <span style={{ cursor: 'pointer' }} onClick={download}>
        {fileName}
      </span>
      {isDownloading ? (
        <span>
          {`下載中:${progress}`}
          <button onClick={cancel}>取消下載</button>
        </span>
      ) : (
        ''
      )}
    </div>
  );
});

const Download = () => {
  return (
    <div>
      {list.map((item, index) => (
        <Item url={item.url} fileName={item.fileName} key={index} />
      ))}
    </div>
  );
};

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
<script setup lang="ts">
import Item from "./components/Item.vue";

const list = [
  {
    fileName: "城市發展史起.pdf",
    url: " http://127.0.0.1:5173/1.pdf",
    type: "pdf",
  },
  {
    fileName: "表格.xlsx",
    url: "http://127.0.0.1:5173/表格.xlsx",
    type: "xlsx",
  },
  {
    fileName: "報告.doc",
    url: "http://127.0.0.1:5173/報告.doc",
    type: "doc",
  },
];
</script>

<template>
  <div>
    <div v-for="(item, index) in list" :key="index">
      <Item :url="item.url" :fileName="item.fileName"<script setup lang="ts">
import useFileDown from "../hooks/useDownload.ts";


const props = defineProps<{ url: string; fileName: string }>();

const { url, fileName } = props;

const { download, cancel, progress, isDownloading } = useFileDown(url, {
  fileName,
});
</script>

<template>
  <div>
    <span style="cursor: pointer" @click="download">
      {{ fileName }}
    </span>
    <span v-if="isDownloading">
      下載中:{{ progress }} <button @click="cancel">取消下載</button></span
    >
  </div>
</template> />
    </div>
  </div>
</template>
  • 新建components/Item.vue
<script setup lang="ts">
import useFileDown from "../hooks/useDownload.ts";


const props = defineProps<{ url: string; fileName: string }>();

const { url, fileName } = props;

const { download, cancel, progress, isDownloading } = useFileDown(url, {
  fileName,
});
</script>

<template>
  <div>
    <span style="cursor: pointer" @click="download">
      {{ fileName }}
    </span>
    <span v-if="isDownloading">
      下載中:{{ progress }} <button @click="cancel">取消下載</button></span
    >
  </div>
</template>

五、可能遇到的問題:lengthComputable為false

原因一:後端響應頭沒有返回Content-Length;

解決辦法:讓後端加上就行

原因二:開啟瞭gzip壓縮

開啟gzip之後服務器默認開啟文件分塊編碼(響應頭返回Transfer-Encoding: chunked)。分塊編碼把「報文」分割成若幹個大小已知的塊,塊之間是緊挨著發送的。采用這種傳輸方式進行響應時,不會傳Content-Length這個首部信息,即使帶上瞭也是不準確的

分別為gzip壓縮,分塊編碼:

clipboard.png

例如有個877k大小的js文件,網絡請求的大小為247k。但是打印的e.loaded最終返回的是877k

7ACD3DB2BB1B4EF9B8CB59505F92C49E.jpg

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

到此這篇關於React和Vue實現文件下載進度條的文章就介紹到這瞭,更多相關React Vue下載進度條內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: