likes
comments
collection
share

前端如何 “打开” 用户的文件

作者站长头像
站长
· 阅读数 49

在前端的业务中,常常会见到需要上传文件的场景。我们今天聊一聊这个场景的第一步,如何打开用户的本地文件,让用户选择呢?

你所熟知的 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

更多的接口

诸如此类的接口还有:

附上 MDN 上关于 File System Access API 的文档。

转载自:https://juejin.cn/post/7236010330051133500
评论
请登录