NodeJS+AngularJS下载文件损坏乱码问题求助
下载二进制文件(xlsx/zip)损坏乱码问题排查与修复
问题现象
通过NodeJS+Express构建的API下载xlsx、zip格式文件时,文件损坏无法正常打开,部分xlsx文件打开显示乱码(示例:��Ӫ��E���Q^���3�3�ϾhwR-��vY!c�O�y�����I�㱣�eG������[J�Gߌw������ES��lz�e�L�'Sr��`x2�R��Į��¦)。
核心问题根源
二进制文件被错误地当成JSON文本进行传输和解析,导致原始二进制数据的编码信息被破坏,最终出现文件损坏或乱码。
后端代码问题分析与修复
原代码问题点
- 将文件二进制Buffer包装在JSON响应中返回,二进制数据被序列化为JSON数字数组,丢失了原始二进制的编码完整性。
- 未设置正确的响应头(如
Content-Type、Content-Disposition),无法告知浏览器这是一个需要下载的二进制文件。
修复后的后端代码
public async downloadFile(patch: string, start: [number, number], timeout: number) { try { // ... 省略storageInfo获取逻辑 const file = this.bucket.file(storageInfo.$path); // 获取文件元信息,用于设置正确的Content-Type const [metadata] = await file.getMetadata(); const contentType = metadata.contentType || 'application/octet-stream'; return new Promise((resolve, reject) => { const readStream = file.createReadStream(); readStream.on('error', (err) => { readStream.destroy(); reject(err); }); // 设置响应头,标识文件类型和下载信息 readStream.setHeader('Content-Type', contentType); readStream.setHeader( 'Content-Disposition', `attachment; filename="${encodeURIComponent(storageInfo.fileName)}"` ); resolve(readStream); }); } catch (ex) { // ... 错误处理逻辑 throw ex; } }
说明:如果需要同时返回文件元信息,可将信息放在自定义响应头(如
X-File-Info)中,避免嵌套在响应体破坏二进制数据。
前端代码问题分析与修复
原代码问题点
- 使用
TextDecoder将二进制arraybuffer解码为文本后再做JSON.parse,这会把非UTF-8编码的二进制文件(xlsx/zip)当成文本解析,直接破坏原始数据。 - 从JSON中取出的数组转
Uint8Array时,数据已经是损坏状态,后续生成的Blob自然无法正常打开。
修复后的前端代码
public async previewFileFromGoogleStorage(hash: string): Promise<IFileDownload> { try { if (!hash) return null; const response: angular.IHttpResponse<ArrayBuffer> = await this.$http({ method: 'GET', url: `${this.getUrlDownload()}${hash}`, cache: false, timeout: this.timeout, responseType: "arraybuffer" }); // 从响应头解析文件名(后端已设置Content-Disposition) const contentDisposition = response.headers('Content-Disposition'); let fileName = 'download'; if (contentDisposition) { const matches = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); fileName = matches ? decodeURIComponent(matches[1].replace(/['"]/g, '')) : fileName; } // 从响应头获取文件类型 const fileType = response.headers('Content-Type') || this.getContentType(''); // 直接用原始arraybuffer生成Blob const fileBlob = new Blob([response.data], { type: fileType }); const fileURL = window.URL.createObjectURL(fileBlob); return { url: fileURL, name: fileName, type: fileType, blob: fileBlob }; } catch (ex) { HandleError.exception(ex); } } private downloadFile(): void { if (this.$rootScope.isIE) { window.navigator.msSaveOrOpenBlob(this.$scope.file.blob, this.$scope.file.name); } else { const link = document.createElement('a'); link.href = window.URL.createObjectURL(this.$scope.file.blob); link.download = this.$scope.file.name; link.click(); // 释放URL资源,避免内存泄漏 window.URL.revokeObjectURL(link.href); } }
内容的提问来源于stack exchange,提问作者azh
相关产品推荐
相关产品推荐

