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壓縮,分塊編碼:
例如有個877k大小的js文件,網絡請求的大小為247k。但是打印的e.loaded最終返回的是877k
解決方法:後端把文件大小存儲到其他字段,比如:header['x-content-length']
到此這篇關於React和Vue實現文件下載進度條的文章就介紹到這瞭,更多相關React Vue下載進度條內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- vue項目實現文件下載進度條功能
- 一文詳解如何根據後端返回的url下載json文件
- Vue文件下載進度條的實現過程
- 一次在vue中使用post進行excel表下載的實戰記錄
- vue導出excel文件流中文亂碼問題及解決