拖拽上传文件以及文件夹

更新时间: 2025-06-04 16:42:21

前阵子开发了公司的文档管理系统,公司同事提出要能上传文件夹,以及拖拽上传文件夹和文件。

# 核心原理

  1. 拖拽事件
    浏览器支持拖拽文件/文件夹到页面元素。通过监听dragenter,dragoverdragleave,drop事件,可以做自定义拖拽交互

  2. 获取拖拽内容
    drop事件中,可以通过event.dataTransfer.items获取拖拽的内容。这些items可能是文件,也可能是文件夹

  3. 解析文件结构
    每个item可以通过webkitGetAsEntry()方法获取一个FileSystemEntry对象。

FileSystemEntry有两种类型:

  • FileSystemFileEntry: 表示文件

  • FileSystemDirectoryEntry: 表示文件夹

  • 如果是文件夹(isDirectory),可以递归调用其createReader().readEntries()方法,遍历子文件和子文件夹

  • 如果是文件(isFile),可以通过fileEntry.file(callback)获取File对象

  1. 递归遍历
    通过递归遍历所有的FileSystemDirectoryEntry,可以获取文件夹下的所有文件,并且可以拿到每个文件的相对路径(entry.fullPath),这样就能保留原有的文件夹结构。

  2. 上传文件
    获取到所有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>
1
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;
}
1
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}`);
    });
}
1
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;
}
1
2
3
4
5
6
7
8
9

# 支持的浏览器

  • Chrome 13+
  • Edge 79+
  • Filefox 50+(部分支持)
  • safari 11.1+(部分支持)

# 降级策略

  • 优先使用拖拽上传
  • 不支持拖拽时使用目录选择
  • 都不支持时显示提示信息