1. 前言
作为一名前端开发工作中天天在写的html真的了解吗?这个问题在以前我的回答是肯定的,或者说html在前端技术栈内是一个被我忽视的存在,直到最近做了一个项目需要用到input元素的文件选择功能我才意识到原来我并不真的了解。所以本文主要是最近对input的重学和研究一番之后的总结。从以下两个方面介绍:
- input元素的基本特性
- input元素各种文件选择场景的实现
2. input元素的基本特性
2.1 input的类型
主要可以分为:
- 按钮类(button,checkbox)
- 日期时间类(date,datetime-local,month,week,time)
- 填写地址/个人信息类(text,email,file,tel,password)
- 其他类型参见mdn文档
2.2 file类型的详细介绍
作用:file类型的input元素用户可以选择一个或多个元素以表单提交的方式上传到服务器,或者通过Javascript的File API(File API在下文介绍的功能实现中承担十分重要的角色)对文件进行操作。
事件:change和input。监听选择文件改变使用change
属性:
-
accept: 定义了文件 input 应该接受的文件类型
- 一个以英文句号(".")开头的合法的不区分大小写的文件名扩展名。例如:
.jpg,.pdf或.doc。 - 一个不带扩展名的 MIME 类型字符串。
- 字符串
audio/*, 表示 “任何音频文件”。 - 字符串
video/*,表示 “任何视频文件”。 - 字符串
image/*,表示 “任何图片文件”。
- 一个以英文句号(".")开头的合法的不区分大小写的文件名扩展名。例如:
-
files:
FileList对象每个已选择的文件。如果multiple属性没有指定,则这个列表只有一个成员。 -
multiple: 是否允许用户选择多个文件
-
webkitdirectory: 表示在文件选择器界面中用户只能选择目录
3. input元素各种文件选择场景的实现
接下去笔者从简单到复杂依次介绍
3.1 选择单一文件
先上代码,Html结构如下:
<label for="file1">单个文件</label><input id="file1" type="file" accept="image/*">
Javascript如下:
addFileChangeListener('file1', 'change')
// 工具函数,下面其他案例都将使用到
function addFileChangeListener(idSelector, event, successCallback) {
document.getElementById(idSelector).addEventListener(event, e => {
const curFiles = Array.from(e.target.files)
if (!curFiles.length) return window.alert('请选择文件')
successCallback && successCallback(curFiles)
curFiles.forEach(file => {
if (validFileType(file, 'image')) {
const img = document.createElement('img')
img.src=URL.createObjectURL(file)
document.body.append(img)
}
})
})
}
// 验证文件类型
function validFileType(file, type) {
return file.type.indexOf(type) > -1
}
上面的代码相信大家都很熟悉,html部分首先设置input的type=file使得用户可以选择文件,其次设置accept="image/*"只允许用户选择图片文件。
在js部分监听input的change事件获取到用户选择的FileList对象,然后将图片添加到body中显示出来。
然后看看怎么实现多文件选择?相信这个功能大家也不陌生,下面介绍下怎么实现
3.2 多文件选择
其实只要在单文件选择的基础上改一下html就可以,代码如下
<label for="file1">单个文件</label><input id="file1" type="file" accept="image/*" multiple>
我们在input标签上添加了multiple属性就可以支持多选。在大多数场景下以上3.1和3.2的案例基本上就能覆盖了。但是总会有些特殊的业务场景,比如用户想直接选择一个文件夹下面的所有文件和子文件夹下面的所有文件,【注意需要包括子文件夹内的文件】。下面就来介绍实现方法,
3.3 选择目录下的所有文件
机智的你可能觉得这个功能使用multiple也能实现,只要在上传的时候ctrl+A选中所有文件就行。笔者最开始也这么想过,实测下来发现ctrl+A并不能选中后代文件夹。
实现方法是只要修改以上案例的html代码,在input标签上添加webkitdirectory属性就可以支持选择文件夹。
<label for="file1">选择目录下的所有文件及子文件夹</label><input id="file1" type="file" webkitdirectory>
使用webkitdirectory特性有几个注意点:
- webkitdirectory是非标准的特性:
- 使用了webkitdirectory后accept属性将会无效,所以没法通过设置input元素属性实现上传文件夹内的指定类型文件,只能用过js做文件类型校验。
接下来看看选中文件夹上传后我们获取到的数据如下:
可以看到能成功获取文件夹下所有文件包括后代文件夹内的文件,返回的是一个扁平化的File数组。到这一步是不是觉得大功告成了?秉着对功能和需求的不断挖掘,再来想想看如果现在要从获取的扁平化的数组中还原出文件原有的目录结构该如何实现呢?一开始会有点无从下手,不知道怎么做的时候那就仔细的查阅MDN文档。果然从看文档的过程中想到了实现方法。
3.4 选中文件夹下所有文件并获取其目录结构
从获取到的File数组中可以看到每一个File对象都有一个webkitRelativePath,这个属性中保存了文件的相对路径。这样我们就可以通过简单的计算还原出来原有的目录结构。话不多说,先上代码。
Html如下:
<div><label for="file4">选择或拖放目录下所有文件并获取目录结构</label><input id="file4" type="file" webkitdirectory></div>
Js如下:
addFileChangeListener('file4', 'change', (files) => {
// fileRoot就是包含目录结构的tree
const fileRoot = {
dir: files[0].webkitRelativePath.split('/')[0],
files: [],
children: []
}
files.forEach(file => {
addToParent(fileRoot, file.webkitRelativePath.split('/'), file)
})
})
/**
* 文件添加到父节点
* @param node 父节点
* @param path 路径数组
* @param file 文件对象
* @returns {*}
*/
function addToParent(node, path, file) {
if (path.length === 2) {
// 过滤掉mac的系统文件
if (node.dir === path[0] && file.name !== '.DS_Store') {
node.files.push(file)
}
return
}
let child = node.children.find(item => item.dir === path[1])
if (!child) {
child = {
dir: path[1],
files: [],
children: []
}
node.children.push(child)
}
addToParent(child, path.slice(1), file)
}
最后看一下控制台输出的fileRoot对象:
结尾
以上就是本文介绍的所有内容,相信已经能满足大部分选择文件的场景了。前端的知识海洋之广之深,需要我们不断去探索,在这次重新深入学习input标签和实现上述功能的过程中,查了很多文档,又发现了许多以前从未触及的知识。本文的所有代码可以在github获取