likes
comments
collection
share

如何使用 HTML 的 <input type="file"> 实现文件上传功能并封装为函数?

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

<input type="file">

在HTML中,通过 <input type="file> 标签可以让用户从其设备中选择一个或多个文件,并将文件提交到服务器或通过JS操作文件。在MDN文档中了解更多。

<label for="avatar">Choose a profile picture:</label>
​
<input type="file"
       id="avatar" name="avatar"
       accept="image/png, image/jpeg">

如何使用 HTML 的 <input type="file"> 实现文件上传功能并封装为函数?

指定文件格式

使用**accept**属性可以指定文件选择的格式,例如只选择word文件,则**accept=".doc,.docx,application/msword"**

<input
  type="file"
  id="docpicker"
  accept=".doc,.docx,application/msword" />
​

然而,这种指定并不是绝对的,用户可以在系统文件选择框中切换成"*",从而选择其他文件格式。

单选/多选

默认情况下,文件选择是单选的。通过multiple属性可以指定多选。

<input
  type="file"
  id="docpicker"
  multiple="true"
  accept=".doc,.docx,application/msword" />

使用JS获取文件

获取 <input type="file> 的DOM实例,通过HTMLInputElement.files来获取选择的文件。

<div>
  <label for="file">选择要上传的文件</label>
  <input type="file" id="file" name="file" multiple />
</div>
<script>
  var inputEl = document.getElementById('file');
  inputEl.addEventListener('change', () => {
    console.log(inputEl.files);
  });
</script>

**HTMLInputElement.files**中的file元素包含:namelastModifiedsizetype等信息。

封装文件选择功能

通常情况下,为了美观,我们不会仅使用原生的**<input type="file>**标签来实现文件选择功能。我们可以使用CSS对样式进行修改,或者通过其他方式实现点击其他元素来触发隐藏的**<input>**来选择文件。这种方式会比较繁琐,而引入组件库会增加复杂度。我们可以创建一个文件选择函数,它可以直接完成文件选择操作,而不必关心样式。 通过JS创建HTMLInputElement,并将其type属性设置为file,然后触发点击事件来选择文件。在创建实例后,不要将其挂载到DOM上,以避免出现在页面中。最后,添加change监听器到实例中,在回调函数中获取选中的文件。由于文件选择是一个异步事件,因此使用Promise来处理选择的文件。

export const selectFile = (accepts = ['*'], multiple = false) => {
  // 创建<input>标签
  const inputElem = document.createElement('input');
  // 设置属性`type`为`file`
  inputElem.setAttribute('type', 'file');
  // 设置属性`visibility`为`hidden`
  inputElem.setAttribute('visibility', 'hidden');
​
  if (Array.isArray(accepts) && accepts.length > 0) {
    // 设置文件格式
    inputElem.setAttribute('accept', accepts.join(','));
  }
​
  if (multiple) {
    // 设置多选文件
    inputElem.setAttribute('multiple', 'true');
  }
​
  // 主动触发点击事件
  inputElem.click();
​
  return new Promise((resolve, reject) => {
    // 在实例上添加监听
    inputElem.addEventListener('change', () => {
      if (!inputElem.files || inputElem.files?.length == 0) {
        reject();
      } else {
        const files = Array.from(inputElem.files);
        // 主要防止用户主动选择了非指定的文件格式
        if (illegalFiles(files)) {
          reject();
        }
        resolve(files);
      }
    });
  });
​
  function illegalFiles(files) {
    return !accepts.includes('*') && files.some((file) => !accepts.includes(`.${getFileExtension(file)}`));
  }
};

以下是**selectFile**函数的使用示例:

// 单选文件
selectFile(['.jpg', '.png'])
  .then((files) => {
    console.log(files);
    // 处理文件
  })
  .catch(() => {
    console.log('选择文件出错');
  });
​
// 多选文件
selectFile(['.jpg', '.png'], true)
  .then((files) => {
    console.log(files);
    // 处理文件
  })
  .catch(() => {
    console.log('选择文件出错');
  });

上述示例中,第一个selectFile函数调用将只允许选择 .jpg.png格式的单个文件,而第二个调用将允许选择 .jpg.png格式的多个文件。在选择文件之后,可以使用then回调处理返回的文件数组。如果选择文件时出现错误,则将会调用catch回调函数。

完整封装

封装的文件选择函数在用户没有选择文件时,点击【确定】按钮时调用者会决策失败。但是如果用户主动点击系统的文件选择框里的【取消选择】按钮,此时并不会触发inputElementchange事件,导致调用方无法识别。 虽然DOM没有提供监听【取消选择】的API,但是可以通过监听window对象的focus事件,再加上inputElement.files来判断用户是否点击了【取消选择】按钮。具体实现方式为,在调用selectFile函数前先监听window对象的focus事件,当事件触发时判断inputElement.files是否为空,如果为空则表示用户点击了【取消选择】按钮。需要注意的是,这种方法只能监听浏览器失焦事件,对于一些特殊情况可能无法生效。

export class BaseError extends Error {
  cause?: Error;
​
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = this.constructor.name;
  }
}
​
export class FileSelectCancelError extends BaseError {
  constructor() {
    super('Cancel select');
  }
}
​
export class IllegalFileError extends BaseError {
  accepts: string[];
​
  constructor(accepts: string[]) {
    super(`Please select files in ${accepts} format`);
    this.accepts = accepts;
  }
}
​
export const selectFile = (accepts = ['*'], multiple = false) => {
  // 创建<input>标签
  const inputElem = document.createElement('input');
  // 设置属性`type`为`file`
  inputElem.setAttribute('type', 'file');
  // 设置属性`visibility`为`hidden`
  inputElem.setAttribute('visibility', 'hidden');
​
  if (Array.isArray(accepts) && accepts.length > 0) {
    // 设置文件格式
    inputElem.setAttribute('accept', accepts.join(','));
  }
​
  if (multiple) {
    // 设置多选文件
    inputElem.setAttribute('multiple', 'true');
  }
​
  // 主动触发点击事件
  inputElem.click();
​
  return new Promise((resolve, reject) => {
    window.addEventListener(
      'focus',
      () => {
        // 这里必须异步处理`inputElement`
        setTimeout(() => {
          if (!inputElement.files || inputElement.files?.length === 0) {
            reject(new FileSelectCancelError());
          }
        }, 0);
      },
      { once: true }
    );
    inputElement.addEventListener('change', () => {
      if (!inputElement.files || inputElement.files?.length === 0) {
        reject(new FileSelectCancelError());
      } else {
        const files = Array.from(inputElement.files);
        if (illegalFiles(files)) {
          reject(new IllegalFileError(accepts));
        }
        resolve(files);
      }
    });
  });
​
  function illegalFiles(files: File[]): boolean {
    return !accepts.includes('*') && files.some((file) => !accepts.includes(`.${getFileExtension(file)}`));
  }
};

以下是**selectFile**函数的使用示例:

const onFileSelect = async () => {
  try {
    const files = await selectFile(['jpg', 'png'], true);
    // 处理选择的文件
    console.log(files);
  } catch (error) {
    if (error instanceof FileSelectCancelError) {
      console.log('取消选择文件');
    } else if (error instanceof IllegalFileError) {
      console.log(`只能选择 ${error.accepts} 格式的文件`);
    } else {
      console.error(error);
    }
  }
};
​
// 触发文件选择
onFileSelect();

在上面的示例中,**selectFile**函数会返回一个 Promise 对象,用于异步获取用户选择的文件。如果用户选择了文件,则 Promise 对象会被解析,返回选择的文件数组;如果用户取消选择,则 Promise 对象会被拒绝,返回一个 **FileSelectCancelError** 对象;如果用户选择了不合法的文件格式,则 Promise 对象也会被拒绝,返回一个 **IllegalFileError** 对象。在使用 **selectFile** 函数时,可以使用 try-catch 语句来捕捉 Promise 对象的解析和拒绝结果,并进行相应的处理。

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