拖拽上传文件以及文件夹
前阵子开发了公司的文档管理系统,公司同事提出要能上传文件夹,以及拖拽上传文件夹和文件。
# 核心原理
拖拽事件
浏览器支持拖拽文件/文件夹到页面元素。通过监听dragenter
,dragover
,dragleave
,drop
事件,可以做自定义拖拽交互获取拖拽内容
在drop
事件中,可以通过event.dataTransfer.items
获取拖拽的内容。这些items可能是文件,也可能是文件夹解析文件结构
每个item
可以通过webkitGetAsEntry()
方法获取一个FileSystemEntry
对象。
FileSystemEntry
有两种类型:
FileSystemFileEntry: 表示文件
FileSystemDirectoryEntry: 表示文件夹
如果是文件夹(
isDirectory
),可以递归调用其createReader().readEntries()
方法,遍历子文件和子文件夹如果是文件(
isFile
),可以通过fileEntry.file(callback)
获取File
对象
递归遍历
通过递归遍历所有的FileSystemDirectoryEntry
,可以获取文件夹下的所有文件,并且可以拿到每个文件的相对路径(entry.fullPath),这样就能保留原有的文件夹结构。上传文件
获取到所有File对象后,可以用FormData
逐个或批量传到后端,通常会吧文件的相对路径一同上传,后端根据这个路径还原文件价的结构
# 流程梳理
- 用户拖拽文件夹
↓ - 监听 drop 事件
↓ - 遍历 dataTransfer.items
↓ - item.webkitGetAsEntry()获取FileSystemEntry对象
↓ - 判断 isDirectory 还是 isFile
↓ - 递归读取所有文件
↓ - 获取 File 对象和相对路径
↓ - FormData上传到后端
# 实现
拖拽文件到这里,打开F12看拖拽的文件
<div class="file-drop">
<div class="drop-zone" id="dropZone">
<p>拖拽文件到这里,打开F12看拖拽的文件</p>
</div>
</div>
2
3
4
5
.drop-zone {
width: 300px;
height: 200px;
border: 2px dashed #ccc;
text-align: center;
padding: 20px;
margin: 20px;
}
.drop-zone.dragover {
background-color: #e1e1e1;
border-color: #999;
}
2
3
4
5
6
7
8
9
10
11
12
// 检查浏览器支持
if (window.File && window.FileReader && window.FileList && window.Blob) {
console.log('浏览器支持文件上传');
} else {
console.error('浏览器不支持文件上传');
}
if('webkitGetAsEntry' in DataTransferItem.prototype) {
console.log('浏览器支持文件夹拖拽');
}else{
console.log('浏览器支持文件夹拖拽');
}
const dropZone = document.getElementById('dropZone')
// 阻止默认行为
let list = ['dragenter', 'dragover', 'dragleave', 'drop']
list.forEach(evantName => {
dropZone.addEventListener(evantName, preventDefaults, false)
document.body.addEventListener(evantName, preventDefaults, false)
})
// 高亮显示拖拽区域
let list1 = ['dragenter','dragover']
list1.forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
})
// 撤销高亮状态
let list2 = ['dragleave','drop']
list2.forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false)
})
dropZone.addEventListener('drop', handleDrop, false)
// 拖拽文件或文件夹到网页的默认行为,比如直接下载文件,直接打开文件,这些都是要禁止的
function preventDefaults(e) {
e.preventDefault()
e.stopPropagation()
}
function highlight(e) {
dropZone.classList.add('dragover')
}
function unhighlight(e) {
dropZone.classList.remove('dragover')
}
async function handleDrop(e) {
// e.dataTransfer.items 获取拖拽时所有条目
const items = e.dataTransfer.items;
const allFiles = []
for(let i = 0;i < items.length;i++) {
const item = items[i]
if(item.kind === 'file') {
// 获取FileSystemEntry对象
const entry = item.webkitGetAsEntry()
if(entry) {
// 遍历目录获取所有文件以及相对路径
const files = await collectFiles(entry)
allFiles.push(...files)
uploadFolder(allFiles)
}
}
}
}
function collectFiles(entry, path = '', files = []) {
return new Promise((resolve) => {
if(entry.isFile) {
//如果是文件,通过 fileEntry.file(callback) 获取 File 对象
entry.file(file => {
files.push({
file,
relativePath: path + '/'+file.name
})
resolve(files)
})
}else if(entry.isDirectory) {
//如果是文件夹,递归调用其 createReader().readEntries() 方法,遍历子文件和子文件夹。
const reader = entry.createReader()
reader.readEntries(entries => {
const promises = entries.map(ent => collectFiles(ent, path + '/' + ent.name, files))
Promise.all(promises).then(() => resolve(files))
})
}
})
}
function uploadFolder(files) {
console.log(files)
const formData = new FormData()
files.forEach((file, index) => {
formData.append(`files[${index}]`, file)
formData.append(`paths[${index}]`, file.relativePath)
})
formData.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
关键API说明
event.dataTransfer.items
拖拽时获取的所有条目(文件/文件夹)webkitGetAsEntry()
获取文件或文件夹的入口(目前主流浏览器支持,标准化进程中)FileSystemDirectoryEntry.createReader().readEntries()
读取文件夹下的所有内容(文件和子文件夹)FileSystemFileEntry.file(callback)
获取文件对象entry.fullPath
获取文件或文件夹的相对路径
# 兼容性
- 目前仅chrome, Edge支持拖拽文件夹,Firefox/Safari暂不支持
- 选择文件夹(非拖拽)可用
<input type="file" webkitdirectory>
但也不是所有浏览器都支持
// 检查浏览器是否支持文件夹拖拽
function checkFolderDragSupport() {
return 'webkitGetAsEntry' in DataTransferItem.prototype;
}
// 检查浏览器是否支持目录选择
function checkDirectorySupport() {
return 'webkitdirectory' in HTMLInputElement.prototype;
}
2
3
4
5
6
7
8
9
# 支持的浏览器
- Chrome 13+
- Edge 79+
- Filefox 50+(部分支持)
- safari 11.1+(部分支持)
# 降级策略
- 优先使用拖拽上传
- 不支持拖拽时使用目录选择
- 都不支持时显示提示信息