React + Spring 前后端如何显示base64文件下载进度条
前言
因为业务要求,查了下怎么获取进度,但是资料很分散。虽然又被业务搁置了,还是记录下怎么实现的。
现有代码
前端React
axios向服务器端通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| async post<T>(url: string, body: any): Promise<T>{ return axios .post(`${this.serverDomain}${url}`), JSON.stringify(body),{ headers: this.headers, }) .then( (axiosResponse)=>( return axiosReponse.data as T; ), (axiosError)=>( return axiosError?.reponse?.data || axiosError; ), ); }
|
service层的相关方法
1 2 3 4 5 6 7 8
| async download(ids: number[], locale: string): Promise<ApiResponse<Blob>> { const url = `${this.baseUrl}/download?locale=${locale}`; return this.httpService.post<FileReponse>(url, [...ids]).then( (response: any) => response, (error) => error, ); }
|
component层调用
1 2 3 4 5 6 7 8 9
| extractService.download(ids, intl.locale).then((response: ApiResponse<Blob>)=>{ const linkSource = `data:application/vnd.openxmlformas-officedocument.spreadsheetml.sheet;base64,^${reponse.data}`; const link = document.createElement(`a`); link.href = linkSource; link.download = 'extract.zip'; link.click(); });
|
后端Spring
1 2 3 4 5 6 7 8 9
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos); ... final var fileByteArray = baos.toByteArray(); ... final var fileResponseDto = new FileResponesDto(); fileResponseDto.setData(fileByteArray); return ResponseEntity.status(HttpStatus.SC_OK).body(fileResponseDto);
|
解决过程
一开始找解决方案的时候很迷茫,因为一方面后端处理生成文件的时间在我的认知里是不可预估的,一方面怎么获取实时下载进度也是难以想象的。在寻找的过程中,发现进度条可能只是用来处理已生成好的文件,就像chrome下载时有绿色转动的小圈进度条。另一方面原以为是react本身也可以通过方法的内部获取实时下载好的文件大小,我还以为需要webpack之类服务器端实时发包的功能。
在边找边试的过程中,也遇到了不少坑。首先是大部分资料用的都是过时的addEventListener,我还以为react也有,找了半天发现业务底层是axios,可以直接在parameter加onDownloadProgress。一些旁门左道试图使用transfer-encoding: chunked,这样加了之后反而弄巧成拙更加复杂了。之后发现只要在后端加入content-lengh就能获取文件长度,这个属性在前端的header里面也以total形式出现。但是加了之后反而下载不了,因为content-length是整个数据包的大小,而并非是指单个json数据包大小,如果只是下载文件倒是没有问题,像业务这种需要dto传送其他信息的,就不能用,因为到达length之后json数据包直接就结束传递了,所以导致json的数据不完整。解决方法是使用自定的属性头比如File-size,但是因为浏览器安全需要不允许非expose的header出现,所以浏览器会警告并停止获取json。如何去使浏览器信任自己的设置的header呢?就需要在response响应里面提前说好,在Access-Control-Expose-headers里面加入我们自己的头。
另外使得浏览器不会显示自己的进度条,需要加入以下方法:
1
| windows.URL.revokeObjectURL(linkSource);
|
example代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| async post<T>(url: string, body: any, onDownloadProgress: any): Promise<T>{ return axios .post(`${this.serverDomain}${url}`), JSON.stringify(body),{ headers: this.headers, onDownloadProgress: onDownloadProgress }) .then( (axiosResponse)=>( return axiosReponse.data as T; ), (axiosError)=>( return axiosError?.reponse?.data || axiosError; ), ); }
...
async download(ids: number[], locale: string, onDownloadProgress: (progressEvent: any) => void): Promise<ApiResponse<Blob>> { const url = `${this.baseUrl}/download?locale=${locale}`; return this.httpService.post<FileReponse>(url, [...ids],onDownloadProgress).then( (response: any) => response, (error) => error, ); }
...
const onDownloadProgress = (progressEvent: any) => { let total = progressEvent.currentTarget.getResponseHeader('File-Size'); let percentCompleted = total ? Math.floor(progressEvent.loaded / total) * 100).toFixed(2) : 1 } extractService.download(ids, intl.locale, onDownloadProgress).then((response: ApiResponse<Blob>)=>{ const linkSource = `data:application/vnd.openxmlformas-officedocument.spreadsheetml.sheet;base64,^${reponse.data}`; const link = document.createElement(`a`); link.href = linkSource; link.download = 'extract.zip'; link.click(); window.URL.revokeObjectURL(linkSource); });
|
1 2 3 4 5 6 7 8 9 10 11 12
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos); ... final var fileByteArray = baos.toByteArray(); ... final var fileResponseDto = new FileResponesDto(); fileResponseDto.setData(fileByteArray); return ResponseEntity.status(HttpStatus.SC_OK). .header("Access-Control-Expose-Headers", "File-Size") .header("File-Size", String.valueOf(fileByteArray.length)) .body(fileResponseDto);
|