前面写了一篇仿写el-upload组件,彻底搞懂文件上传,实现了选择/拖拽文件上传,我们经常看到一些网站支持直接选择整个文件夹上传,例如:宝塔面板、cloudflare托管、对象存储网站等等需要模拟文件路径存储文件的场景。那是怎么实现的呢?
依然从两方面来说:
- input选择文件夹
- 拖拽文件夹
input选择文件夹
在props.js中加一个属性,upload-folder是否支持上传文件夹
js
export default {
// 前面的省略了...
// 是否支持选择文件夹
'upload-folder': {
type: Boolean,
default: false
}
}
改一下input标签,依然是根据props的值动态判断是否支持上传文件夹。主要是webkitdirectory这个属性,由于不是一个标准属性,需要加浏览器前缀。
html
<input
type="file"
id="file"
:multiple="multiple"
:accept="accept"
:webkitdirectory="uploadFolder"
:mozdirectory="uploadFolder"
:odirectory="uploadFolder"
@change="handleChange"
>
注意 :支持选择文件夹时就只能选择文件夹,无法选择文件。
那么如何获取选择的文件夹呢?其实我们最终要上传的依然是文件,也就是file对象,文件夹也是一个特殊的文件。
依然是通过input 的onchange 事件回调拿到上传的event。
或者直接获取input这个dom对象,然后拿到files属性,结果是一样的。
js
// input选择文件回调
const handleChange = (event) => {
console.log('[ files ] >', event.target.files)
const inputDom = document.querySelector('#file')
console.log('[ files ] >', inputDom.files)
}
可以看到,比选择单个文件时,多了一个webkitRelativePath 属性,并且它是递归 选择的文件夹,拿到这个文件夹及其子文件夹下所有的文件,我们可以通过这个属性拿到上传时文件所在的文件夹名称 和路径。
拖拽文件夹
上篇文章讲过拖拽如何拿到文件,首先要准备一个用于拖拽放置的区域。 调用upload组件时,传入drag=true
。
html
<div
class="drag-box"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
将文件拖到此处,或<span>点击上传</span>
</div>
js
// 拖放进入目标区域
const handleDragOver = (event) => {
event.preventDefault()
}
// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log('[ event ] >', event)
}
注意:和input上传不同,拖拽时,是可以同时拖拽文件和文件夹的。
因为可以同时拖拽文件和文件夹,我们就不能直接使用event.dataTransfer.files
,如果刚好拖拽进来的是一个文件,那可以这么获取,如果是个文件夹呢?那就不行了。
这时候就要用到event.dataTransfer.items
了
js
// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
}
打印一下看看: 得到一个List 类型的数据,里面是两个DataTransferItem ,控制台无法直接查看它到底是个什么玩意儿。 看MDN,也看不出它具体是个啥。既然是List,遍历一下看看:
js
const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
for (const item of event.dataTransfer.items) {
console.log('[ item ] >', item)
}
}
可以看到不管是文件还是文件夹,都被识别成了file
,只不过图片是直接能识别出type为image/png
。
查看MDN,developer.mozilla.org/zh-CN/docs/...
点击查看item
的Prototype
,发现里面有个webkitGetAsEntry
方法,执行它就能拿到item的具体信息。
看方法名,带了个webkit
,但是这个方法除了Android Firefox
浏览器以外都可以用。
js
for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry()
console.log(entry)
}
依然拖动上面那个图片文件和一个文件夹: 可以看出,文件夹里面还有文件和文件夹,但是只显示了一个文件和一个文件夹,看来拖拽和input
上传不一样,它不会自动的把里面所有的文件递归列出来。
通过isDirectory
属性,就能区分是文件还是文件夹。除了这些基础属性以外,继续查看Prototype
,可以看到还有一系列方法:
先看怎么拿到文件
当entry
是一个文件时,它有两个方法:createWriter()
和file()
,查看MDN,developer.mozilla.org/en-US/docs/... createWriter()
已经废弃了,而且也不是我们今天要用的。 file()
才是我们要找的。 这不就是我们熟悉的file
对象吗,跟input
上传拿到的一毛一样。
再看怎么拿到文件夹
查看MDN的Drop API webkitGetAsEntry()方法,developer.mozilla.org/zh-CN/docs/... 可得,如果是文件夹,可以通过createReader
方法创建一个文件目录阅读器,然后通过readEntries
方法,重新拿到每个item
,这就是event.dataTransfer.items
里面的每个item
。 我们写一下试试 依然是之前那个图片和文件夹 只打印出了跟目录下一级的一个文件和一个文件夹,那下面还有一个文件怎么办呢?
递归呀!
写一个递归读文件的方法。
js
const readFiles = async (item) => {
if (item.isDirectory) {
// 是一个文件夹
console.log('=======文件夹=======');
const directoryReader = item.createReader();
// readEntries是一个异步方法
const entries = await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});
let files = [];
for (const entry of entries) {
const resultFiles = await readFiles(entry);
files = files.concat(resultFiles);
}
return files;
} else {
// 是一个文件
console.log('=======文件=======');
// file也是一个异步方法
const file = await new Promise((resolve, reject) => {
item.file(resolve, reject);
});
console.log('[ file ] >', file);
return [file];
}
}
handleDrop
方法也要改一下
js
// 拖拽放置
const handleDrop = async (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
const files = [];
const promises = [];
for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry();
console.log('[ entry ] >', entry);
promises.push(readFiles(entry));
}
const resultFilesArrays = await Promise.all(promises);
const allFiles = resultFilesArrays.flat();
console.log('[ All files ] >', allFiles);
}
再次拖拽上传看看 三个文件我们都拿到了。
总结
上传文件夹,还是直接使用input
比较简单,使用它能直接拿到文件夹下所有的文件,以及每个文件在本地的路径,代码量也少很多。
拖拽的好处是文件和文件夹能一起上传。