React + Spring 前后端如何显示base64文件下载进度条

本文介绍了在React前端和Spring后端如何实现Base64文件下载时的进度条功能。通过在axios中添加onDownloadProgress监听,并在后端设置合适的响应头,解决了文件下载进度的实时显示问题。同时,文章提到了在实现过程中遇到的挑战,如content-length与实际需求不符,以及浏览器安全策略限制自定义header的暴露,最终通过Access-Control-Expose-Headers解决。
摘要由CSDN通过智能技术生成

前言

因为业务要求,查了下怎么获取进度,但是资料很分散。虽然又被业务搁置了,还是记录下怎么实现的。

现有代码

前端React

axios向服务器端通信

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层的相关方法

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层调用

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

//...
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里面加入我们自己的头。

另外使得浏览器不会显示自己的进度条,需要加入以下方法:

windows.URL.revokeObjectURL(linkSource);

example代码实现:

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);
});
//...
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);

 

 附言

没钱懒得买wallabyjs,一个前端测试vscode插件,用起来确实省时间,比npm run test快了不知道多少。就是有试用期弹窗之后点continue trial就好,但是有时候直接没有,因为key过期了。一个小技巧就是删除掉用户文件夹下.wallaby中的key文件,就又能愉快得玩耍了,嘿嘿。别人说我对团队做出了巨大贡献,笑鼠噜。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值