瞬态用户激活行为记录
某个场景下用到了showSaveFilePicker这个API,但是发现这个API时灵时不灵,尤其是涉及到异步任务的时候。特此测试并记录一下该API某些特性。
前因
项目里有个大文件下载场景,出于安全考虑后端要求必须要加authorization请求头,所以直接a标签下载的路子行不通了,只能先下载再保存了。但是涉及到大文件的时候用户的老旧机器,内存总是不够用报错,所以又引入了showSaveFilePicker这个API,配合fetch的getReaderAPI解决这个痛点。虽然兼容性差了点,但用户也配合,总比功能用不了强。
坑
本来功能在本地测的好好的,但是上线之后有些用户反馈报错。一通检查之后发现,相同的代码,如果后端接口返回时间太长,就会触发浏览器的安全类型报错Error selecting file: SecurityError: Failed to execute 'showOpenFilePicker' on 'Window': Must be handling a user gesture to show a file picker.,大意就是为了安全着想,这个API必须由用户发起。
测试
html部分
1
2
3
4<button id="btn1">正常选择文件(会自动触发一次)</button>
<button id="btn2">用户点击4ms后选择文件(then)</button>
<button id="btn3">用户点击4885ms后选择文件(await)</button>
<button id="btn4">用户点击5000ms后选择文件(await)</button>js部分
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// 延迟
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 主体事件
const handleOpenPicker = async () => {
try {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
console.log('Selected file:', file.name);
} catch (error) {
console.error('Error selecting file:', error);
}
};
// 事件绑定
document.getElementById('btn1').addEventListener('click', async () => {
await handleOpenPicker();
});
document.getElementById('btn2').addEventListener('click', () => {
sleep(4).then(() => {
handleOpenPicker();
});
});
document.getElementById('btn3').addEventListener('click', async () => {
await sleep(4000);
await handleOpenPicker();
});
document.getElementById('btn4').addEventListener('click', async () => {
await sleep(8000);
await handleOpenPicker();
});
// 自动触发一次
window.addEventListener('load', async () => {
await handleOpenPicker();
});页面加载完以后,四个按钮挨个点击一遍,测试结果如下图

收获
File System Access这几个API,除了测试环境localhost意外,必须https协议的环境才能触发。另外,出于安全性考虑,瞬态用户激活是必需的。用户必须与页面或 UI 元素进行交互才能使该特性正常运行。如果激活已被触发,则用户代理会区分两种类型的用户激活窗口状态:粘性(sticky)和瞬态(transient)。瞬态激活只持续很短时间(5s往上,不固定),并且在某些情况下,当使用受保护的功能时可能会被消耗(停用),而粘性激活会持续到会话结束。