1. Demand scenario
The download of large file resources on the server is too slow, the page does not display anything, and the experience is too poor. Therefore, it is necessary to increase the progress bar to optimize the display
2. Implementation principle
-
Send an asynchronous HTTP request, listen to the onprogress event, read the downloaded resource and the total size of the resource to get the download percentage
-
After the resource request is completed, convert the file content into a blob, and download the file through the browser through the a tag
3. React implementation steps
1. Host static resources
Prerequisite: react project created by create-react-app
Put the static resource file in the public folder, so that after starting the project, you can directly access the static resource through http://localhost:3000/1.pdf. In actual work, it must be directly accessing the resources on the server
2. Package hook
New useDownload.ts
import {<!-- --> useCallback, useRef, useState } from 'react'; interface Options {<!-- --> fileName: string; //Downloaded file name onCompleted?: () => void; //Callback method for request completion onError?: (error: Error) => void; //Callback method for request failure } interface FileDownReturn {<!-- --> download: () => void; //download cancel: () => void; //Cancel progress: number; //Download progress percentage isDownloading: boolean; //Whether it is downloading } 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); //default asynchronous request xhr.responseType = 'blob'; xhr.onprogress = (e) => {<!-- --> / / Determine whether the resource length can be calculated if (e. length Computable) {<!-- --> const percent = Math. floor((e. loaded / e. total) * 100); setProgress(percent); } }; xhr.onload = () => {<!-- --> if (xhr. status === 200) {<!-- --> //The request resource is completed, and the content of the file is converted to a blob const blob = new Blob([xhr. response], {<!-- --> type: 'application/octet-stream' }); //Download resources through a tag const link = document. createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = decodeURIComponent(fileName); link. click(); window.URL.revokeObjectURL(link.href); onCompleted & amp; & amp; onCompleted(); } else {<!-- --> onError & amp; & amp; onError(new Error('Download failed')); } setIsDownloading(false); }; xhr.onerror = () => {<!-- --> onError & amp; & amp; onError(new Error('Download failed')); setIsDownloading(false); }; xhrRef.current.send(); //Send request setProgress(0); //Reset the progress to 0 each time it is sent setIsDownloading(true); }, [fileName, onCompleted, onError, url]); const cancel = useCallback(() => {<!-- --> xhrRef.current?.abort(); //Cancel request setIsDownloading(false); }, [xhrRef]); return {<!-- --> download, cancel, progress, isDownloading, }; }
3. Use hook
import {<!-- --> memo } from 'react'; import useFileDown from './useDownload'; const list = [ {<!-- --> fileName: 'The history of urban development.pdf', url: 'http://localhost:3000/1.pdf', type: 'pdf', }, {<!-- --> fileName: 'table.xlsx', url: 'http://localhost:3000/table.xlsx', type: 'xlsx', }, {<!-- --> fileName: 'Report.doc', url: 'http://localhost:3000/report.doc', type: 'doc', }, ]; interface Options {<!-- --> url: string; fileName: string; } const Item = memo(({<!-- --> url, fileName }: Options) => {<!-- --> //Each item needs to have its own useFileDown hook const {<!-- --> download, cancel, progress, isDownloading } = useFileDown(url, {<!-- --> fileName }); return ( <div> <span style={<!-- -->{<!-- --> cursor: 'pointer' }} onClick={<!-- -->download}> {<!-- -->fileName} </span> {<!-- -->isDownloading ? ( <span> {<!-- -->`Downloading: ${<!-- -->progress}`} <button onClick={<!-- -->cancel}>Cancel download</button> </span> ) : ( '' )} </div> ); }); const Download = () => {<!-- --> return ( <div> {<!-- -->list. map((item, index) => ( <Item url={<!-- -->item.url} fileName={<!-- -->item.fileName} key={<!-- -->index} /> ))} </div> ); }; export default Download;
4. vue implementation steps
1. Host static resources
Premise: Vue project created by vite
Put the static resource file in the public folder, so that after starting the project, you can directly access the static resource through http://127.0.0.1:5173/1.pdf
2. Package hook
Create new hooks/useDownload.ts (new hooks folder)
import {<!-- --> ref } from "vue"; export interface Options {<!-- --> fileName: string; onCompleted?: () => void; //Callback method for request completion onError?: (error: Error) => void; //Callback method for request failure } export interface FileDownReturn {<!-- --> download: () => void; //download cancel: () => void; //Cancel progress: number; //Download progress percentage isDownloading: boolean; //Whether it is downloading } 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); //Default asynchronous request xhr.responseType = "blob"; xhr.onprogress = (e) => {<!-- --> / / Determine whether the resource length can be calculated if (e. length Computable) {<!-- --> const percent = Math. floor((e. loaded / e. total) * 100); progress.value = percent; } }; xhr.onload = () => {<!-- --> if (xhr. status === 200) {<!-- --> //The request resource is completed, and the content of the file is converted to a blob const blob = new Blob([xhr. response], {<!-- --> type: "application/octet-stream", }); //Download resources through a tag const link = document. createElement("a"); link.href = window.URL.createObjectURL(blob); link.download = decodeURIComponent(fileName); link. click(); window.URL.revokeObjectURL(link.href); onCompleted & amp; & amp; onCompleted(); } else {<!-- --> onError & amp; & amp; onError(new Error("Download failed")); } isDownloading. value = false; }; xhr.onerror = () => {<!-- --> onError & amp; & amp; onError(new Error("Download failed")); isDownloading. value = false; }; xhrRef.value.send(); //Send request progress.value = 0; //Reset the progress to 0 each time it is sent isDownloading. value = true; }; const cancel = () => {<!-- --> xhrRef.value?.abort(); //Cancel request isDownloading. value = false; }; return {<!-- --> download, cancel, progress, isDownloading, }; }
3. Use hook
- Modify App.vue
<script setup lang="ts"> import Item from "./components/Item.vue"; const list = [ {<!-- --> fileName: "History of Urban Development.pdf", url: "http://127.0.0.1:5173/1.pdf", type: "pdf", }, {<!-- --> fileName: "table.xlsx", url: "http://127.0.0.1:5173/table.xlsx", type: "xlsx", }, {<!-- --> fileName: "Report.doc", url: "http://127.0.0.1:5173/report.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"> Downloading:{<!-- -->{<!-- --> progress }} <button @click="cancel">Cancel download</button></span > </div> </template> /> </div> </div> </template>
- New 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"> Downloading:{<!-- -->{<!-- --> progress }} <button @click="cancel">Cancel download</button></span > </div> </template>
5. Possible problems: lengthComputable is false
Reason 1: The backend response header does not return Content-Length;
Solution: Just let the backend add it
Reason 2: gzip compression is enabled
After enabling gzip, the server enables file chunk encoding by default (the response header returns Transfer-Encoding: chunked). Block encoding divides the “message” into several blocks of known size, and the blocks are sent next to each other. When using this transmission method to respond, the Content-Length header information will not be transmitted, even if it is carried, it will be inaccurate
Respectively for gzip compression, block encoding:
For example, there is a js file with a size of 877k, and the size of the network request is 247k. But the printed e.loaded finally returns 877k
Solution: The backend stores the file size in other fields, such as: header[‘x-content-length’]