如何使用 HTML 的 <input type="file"> 实现文件上传功能并封装为函数?
<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">
指定文件格式
使用**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元素包含:name、lastModified、size、type等信息。
封装文件选择功能
通常情况下,为了美观,我们不会仅使用原生的**<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回调函数。
完整封装
封装的文件选择函数在用户没有选择文件时,点击【确定】按钮时调用者会决策失败。但是如果用户主动点击系统的文件选择框里的【取消选择】按钮,此时并不会触发inputElement
的change
事件,导致调用方无法识别。 虽然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