前端如何 “打开” 用户的文件
在前端的业务中,常常会见到需要上传文件的场景。我们今天聊一聊这个场景的第一步,如何打开用户的本地文件,让用户选择呢?
你所熟知的 Input
传统来说,应对这个场景,浏览器给我们提供了非常便捷的方式,即使用 file 类型的 input 标签:
<input type="file"></input>
而且它还有丰富的控制属性:
-
accept: 限制用户选择哪些类型的文件
-
multiple: 是否允许选择多个文件
-
webkitdirectory: 是否只能选择文件夹
更有实用的信息,可以查阅 MDN。
尽管如此,它还是有个明显的缺点,那就是丑(🤪)
第三方组件
那像我这样的 “懒人” 不想改它的样式,又图人家的方便该怎么办呢?
哈哈,我们可以使用各种组件库,大多提供文件上传的组件,就比如 ArcoDesign.Upload , 方便又好用。
本来故事到这里就结束了,可是作为一名程序猿,怎么会满足于此。
File System Access API
其实一开始面对这个场景,我诉求就是:想在界面上例如 onClick 的用户交互之后,有这么一个 API 可以提供打开用户文件的能力。如此一来,UI和交互我都可以做的很灵活。
在一番搜索后,我发现当前浏览器其实是有这样子的接口的:showOpenFilePicker 。
div.addEventListener('click', () => {
// 打开文件 - 效果同点击 input.file
window.showOpenFilePicker(options);
});
它有着和 input 类似的控制属性 options :
-
types: 限制用户选择哪些类型的文件
-
multiple: 是否允许选择多个文件
-
excludeAcceptAllOption: 该值为 true 时,表示 types 无效
更有实用的信息,可以查阅 showOpenFilePicker。
兼容性问题
很可惜,直到现在这个 showOpenFilePicker 仍然还是实验属性,未能成为标准。
为了可以兼容绝大多数场景,我们可以通过下面的代码模拟:
/* 读取本地文件 */
export async function loadLocalFile(): Promise<IImportFileItem[]> {
if ((self as any)?.showOpenFilePicker) {
// 支持 showOpenFilePicker 的浏览器环境
const fileHandles = await (self as any).showOpenFilePicker({
multiple: true,
types: [
{
description: 'Markdown',
accept: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'text/markdown': ['.md'],
},
},
],
});
const files = await Promise.all(
fileHandles.map(async (handle: FileSystemFileHandle, index: number) => {
// 获取文件内容,该数据有 name 字段
const fileData = await handle.getFile();
// 读文件数据
const buffer = await fileData.arrayBuffer();
return buffer;
}),
);
return files;
}
// 使用 input 的兼容方案
return new Promise((resolve, reject) => {
let input = document.getElementById(INPUT_ID) as HTMLInputElement;
// 避免重复创建标签的开销
if (!input) {
input = document.createElement('input');
// 设置参数
input.id = INPUT_ID;
input.type = 'file';
input.multiple = true;
input.accept = '.md';
// 加入 body 并隐藏
input.style.display = 'none';
document.body.append(input);
}
input.addEventListener('change', async () => {
const files = await Promise.all(
Array.from(input.files || []).map(async (fileData, index) => {
// 读文件数据
const buffer = await fileData.arrayBuffer();
return buffer
}),
);
resolve(files);
});
// 模拟点击事件
if (input.showPicker) {
input.showPicker();
} else {
input.click();
}
});
}
具体原理就是,在不支持的情况下,创建一个 input 标签,并模拟它的点击事件。
想要参考完整的代码,可以参考:browser-fs-access 。
更多的接口
诸如此类的接口还有:
-
showDirectoryPicker : 选择文件夹
-
showSaveFilePicker:保存文件
附上 MDN 上关于 File System Access API 的文档。
转载自:https://juejin.cn/post/7236010330051133500