背景和问题陈述
我们目前正在开发一个具有API后端的单页应用程序。该API向单页应用程序提供在服务器上按需动态生成的文件。有些文件生成所需的时间很短,而其他文件需要更长时间(大于5秒)。现在我们面临一个问题,即在三星Galaxy手机上的"Samsung Internet"浏览器中无法下载生成时间超过5秒的文件。在这些设备上,没有显示保存文件的提示。实际上,文件确实被下载了,但是设备上没有显示保存文件的提示。因此,用户无法在这些设备上打开或存储文件。
我们测试过的所有桌面浏览器以及三星Galaxy手机上的Chrome浏览器都可以正常下载文件。只是在三星Galaxy手机上的"Samsung Internet"浏览器中无法正常工作。
我的三星Galaxy手机和"Samsung Internet"浏览器的版本信息:
- 型号:三星Galaxy S22+
- One UI 版本:5.1
- Android 版本:13
- "Samsung Internet" 应用版本:21.0.0.41(这是"Samsung Internet"应用的最新版本)
- 克隆存储库:https://github.com/tobias-graf-p/file-download-issue。
- 进入
server
文件夹,并在计算机上启动服务器。请查阅server
文件夹内的README
文件以获取详细说明。 - 进入
client-js
文件夹,并从计算机上提供客户端服务。请查阅client-js
文件夹内的README
文件以获取详细说明。 - 现在通过导航到
http://localhost:8081
在计算机上打开客户端。您应该可以下载两个文件。文件1将立即提供,而文件2将延迟6秒后提供。 - 现在使用与计算机连接到同一网络的三星Galaxy手机。使用"Samsung Internet"浏览器导航至
http://your-local-ip-address:8081
(将your-local-ip-address
替换为实际计算机的本地IP地址)。在这里,您应该能够下载文件1。但是,如果尝试下载文件2,则没有任何反应(6秒后)。"Samsung Internet"浏览器不会向用户显示打开或保存文件2的提示(就像对文件1的处理方式一样)。
- 在您的三星Galaxy手机上启用
USB调试
功能:- 进入
设置
>关于手机
>软件信息
- 点击
版本号
七次 - 进入
设置
>开发者选项
- 启用
USB调试
- 进入
- 使用Google Chrome检查客户端:
- 通过USB线将手机连接到电脑。
- 在电脑上的Google Chrome浏览器中输入
chrome://inspect/
。 - 在手机上确认权限请求。
- 等待设备出现在电脑上的Chrome浏览器中。
- 在手机的"Samsung Internet"浏览器中导航至
http://your-local-ip-address:8081
(将your-local-ip-address
替换为实际的本地IP地址)。 - 等待页面在电脑上的Chrome浏览器中显示。
- 点击
inspect
function serveFile(res, filePath, fileName, contentType) {
const contentDisposition = `attachment; filename="${encodeURIComponent(fileName)}"`;
res.setHeader('Content-Disposition', contentDisposition);
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
res.setHeader('Content-Type', contentType);
res.setHeader('Access-Control-Allow-Origin', '*');
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
}
const server = http.createServer((req, res) => {
if (req.url === '/file1') {
const filePath = path.join(__dirname, 'file1.txt');
serveFile(res, filePath, 'file1.txt', 'text/plain');
} else if (req.url === '/file2') {
setTimeout(() => {
const filePath = path.join(__dirname, 'file2.txt');
serveFile(res, filePath, 'file2.txt', 'text/plain');
}, 6000);
} else {
res.statusCode = 404;
res.end('Not found');
}
});
下载文件的代码(客户端),完整文件
async function downloadFile(endpoint) {
console.log('downloadFile()');
console.log('endpoint', endpoint);
const response = await fetch(endpoint);
console.log('response', response);
const blob = await response.blob();
console.log('blob', blob);
const url = URL.createObjectURL(blob);
console.log('url', url);
const contentDispositionHeader = response.headers.get('Content-Disposition');
const fileName = getFileName(contentDispositionHeader);
console.log('fileName', fileName);
const downloadLinkTag = document.createElement('a');
downloadLinkTag.href = url;
downloadLinkTag.download = fileName;
console.log('before click');
downloadLinkTag.click();
console.log('after click');
setTimeout(() => URL.revokeObjectURL(url), 0);
}
function getFileName(contentDispositionHeader) {
let fileName = contentDispositionHeader
.split(';')[1]
.split('=')[1];
if (fileName.startsWith('"')) {
fileName = fileName.substring(1, fileName.length - 1);
}
if (fileName.endsWith('"')) {
fileName = fileName.substring(0, fileName.length - 2);
}
return decodeURI(fileName);
}
在Github存储库中还有一个第二个客户端(简单的Angular应用程序),您也可以使用它来重现该问题。该客户端甚至包含了三种不同的方法来下载文件(使用带有对象URL的a标签,使用FileSaver.js和使用FileReader),这些方法都失败了(没有出现文件延迟提示)。
三种方法的代码,请参考完整文件。
private downloadFile(apiUrl: string): void {
this.http
.get(apiUrl, { responseType: 'blob', observe: 'response' })
.subscribe(response => {
const fileName = this.getFileNameFromHeaders(response.headers);
console.log('fileName', fileName);
//
// Approach #1: a-tag with object url
//
console.log('approach #1');
const data = response.body;
if (!data) {
console.log('no data');
return;
}
console.log('data', data);
const url = URL.createObjectURL(data);
console.log('url', url);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
console.log('before click');
link.click();
console.log('after click');
setTimeout(() => URL.revokeObjectURL(url), 0);
//
// Approach #2: FileSaver.js
//
console.log('approach #2');
const blob = new Blob([response.body as Blob], {type: 'text/plain'});
console.log('blob', blob);
console.log('before saveAs');
saveAs(blob, fileName);
console.log('after saveAs');
//
// Approach #3: FileReader
//
console.log('approach #3');
const reader = new FileReader();
reader.onloadend = function(e) {
console.log('reader.result', reader.result);
const link = document.createElement('a');
document.body.appendChild(link);
link.href = reader.result as string;
link.download = fileName;
const clickEvent = new MouseEvent('click');
console.log('before dispatch click event');
link.dispatchEvent(clickEvent);
console.log('after dispatch click event');
setTimeout(()=> {
document.body.removeChild(link);
}, 0)
}
console.log('response.body', response.body);
console.log('before readAsDataURL');
reader.readAsDataURL(response.body as Blob);
console.log('after readAsDataURL');
});
}
private getFileNameFromHeaders(headers: HttpHeaders): string {
const contentDisposition = headers.get('Content-Disposition');
if (!contentDisposition) {
return 'unknown.txt';
}
let fileName = contentDisposition
.split(';')[1]
.split('=')[1];
if (fileName.startsWith('"')) {
fileName = fileName.substring(1, fileName.length - 1);
}
if (fileName.endsWith('"')) {
fileName = fileName.substring(0, fileName.length - 2);
}
return decodeURI(fileName);
}
附加信息
- 我们发现,如果服务器需要一些时间(> 5秒)来响应,问题不仅会出现,而且如果在模拟单击锚点之前,在客户端添加超过5秒的延迟,问题也会出现。看起来,“三星互联网”浏览器在用户点击下载按钮后应用了5秒的超时(这可能不应该发生)。
- 此外,我们发现从a标签中删除
download
属性实际上会使“三星互联网”浏览器显示已下载的文件。但它会在浏览器窗口中显示已下载文件的内容,这对我们来说不是一个选项。(我们希望提示用户打开或存储文件。)我们可以通过使用const data = new Blob([data as Blob], {type: 'application/octet-stream'});
代替const data = response.body;
来欺骗“三星互联网”浏览器再次显示提示,但是这种方法会丢失文件名。(浏览器现在要求用户使用blob guid作为文件名存储文件,这对我们来说也不是一个选项。)
问题
我们做错了什么吗?有没有其他方法可以在三星Galaxy手机上显示下载提示? 这可能是“三星互联网”浏览器的一个bug吗?