一文讲清楚文件上传和下载、文件夹拖拽上传、文件拖拽下载、大文件分片上传等原理
本文使用Nodejs和H5来说明,不基于任何库。
一、上传
1.1 单文件
先看客户端,也就是浏览器端H5相关代码。
<input type="file" onchange="doUploadFile" multiple="true" />
function doUploadFile(event){let file = event.target.files[0] // 比如我们上传选择的第一个文件let formData = new FormData()formData.append('file', file) // 需要上传的文件内容formData.append('name', encodeURIComponent(file.name)) // 文件名称fetch("./cloud/upload-file.do",{method: "POST",headers:{},body: formData})}

这样,客户端部分就写好啦(上面是原理说明,所有非必要参数我们都省略了,实际业务可以自己补充更多参数)。
运行一下看看前端上传的时候具体带的参数什么样子:

这里应该没有什么疑问,很常规(如果有问题,可以评论区留言或者划线告诉我们,说实话,俺有时候也不清楚说的读者能不能看懂,又怕太啰嗦,很纠结)。
那么看看服务端 upload-file.do 是怎么接收这个文件的吧(Nodejs代码):
const { createServer } = require('http')const { writeFileSync } = require("fs")createServer(function (request, response) {// POST请求,数据格式用formDataif (request.method === "POST" && /^multipart\/form-data/.test(request.headers["content-type"])) {// 第一步:解析出formData数据(包括文件名称和文件内容)new Promise(function(resolve, reject){let boundary = "--" + request.headers["content-type"].split("boundary=")[1]let chunks = []request.on('data', (chunk) => { chunks.push(chunk); })request.on('end', () => {let buffer = Buffer.concat(chunks)let parts = buffer.toString("binary").split(boundary)// 去掉结尾的--.filter(part => part && part !== '--\r\n')let formData = {}parts.forEach(part => {let [header, body] = part.split('\r\n\r\n')body = body.slice(0, -2) // 去掉结尾的\r\nlet nameMatch = header.match(/name="([^"]+)"/)if (nameMatch) {let name = nameMatch[1]formData[name] = body}})resolve(formData)})})// 第二步:文件写入磁盘即可.then(function(params){writeFileSync(params.name, params.file)})}})
客户端的原理也很简单,它收到的data,也就是一个个chunk,收集好以后,按照规定格式切换一下,就得到了前端传的name和file了(更多参数也是一样的)。
然后,把文件file写入磁盘,文件名是name就可以了。到此,一个小文件上传就成功了~
1.2 大文件
大文件其实和单个小文件区别不大,就是前端多了一步大文件切片上传,然后后端多一步切片合并写入。
整体思路如下:

还是来看看具体实现吧,先看前端:
function doUploadBigFile(event){let file = event.target.files[0]let sharedSize = 10 * 1024 * 1024 // 一片多大let sharedCount = Math.ceil(file.size / sharedSize) // 多少片for(let index=0;index<sharedCount;index++){ // 一片片上传// 这里和单个小文件一样,只是多了片的信息,方便后台知道怎么拼接let formData = new FormData()let start = index * sharedSizelet end = Math.min(file.size, start + sharedSize)formData.append('file', file.slice(start, end))formData.append('name', encodeURIComponent(file.name))formData.append('total', sharedCount)formData.append('index', index)formData.append('size', sharedSize)fetch("./cloud/upload-slice.do",{method: "POST",headers:{},body: formData})}}

可以看见,好多个upload-slice.do请求,是的,一片片的。
现在,再来看看服务端。解析formData数据等前面单个小文件一样,就不赘述了,这里直接从数据解析后开始:
let count=0then(function(params){writeFileSync(params.name+"_cache_"+params.index, params.file) // 保持当前片数据count+=1 // 表示又得到了一个新片// 如果所有片都获取了,就可以拼接成最终的文件了if (count >= params.total) {let fd = openSync(params.name, 'a')writeFileSync(fd, "")for (let index = 0; index < params.total; index++) {let buff = readFileSync(params.name + "_cache_" + index)appendFileSync(fd, buff)unlinkSync(params.name + "_cache_" + index)}closeSync(fd)}})
收到每一片文件的时候就保存起来,等所有片都收到了,合并获得最终文件。
如此,大文件的问题也解决了(大文件断点续传在这个基础上也可以实现,不过那是业务上的东西了,这里就展开说明)。
1.3 文件夹
文件夹的话,其实就是多个文件,主要是前端获取文件列表的方式的改变,然后后端保存文件的时候,不能直接用name,而是需要用路径。
还是先看看前端不一样的地方:
<input type="file" onchange="doUploadFile" multiple="true"webkitdirectory mozdirectory odirectory />
是的,就是这一个区别,虽然你选择的是文件夹,其实最终doUploadFile获取到的还是多个文件。
当然,这些多个文件你是可以知道它相对路径的,看一下例子截图:

所以,其余地方需要改变一点的就是,要告诉后台`webkitRelativePath: “mcp-server/tools.js”`文件相对位置就可以了(不然后台获取到的都是平铺的文件,我们需要保留文件夹的结构,当然,空文件夹会被直接无视)。
1.4 拖拽上传
这里依旧没有什么特别的,还是前端的处理略有不同:
<divondragover="doDragover"ondrop="doDrop">你可以把电脑桌面的文件或文件夹拖拽到这里进行上传~</div>
ondragover只是取消默认效果:
function doDragover(event){event.preventDefault()}
doDrop中就可以获取需要上传的文件列表:
function doDrop(event){event.preventDefault()// 需要上传的文件列表event.dataTransfer.files}

二、下载
2.1 单文件
对于前端而言,非常简单,比如:
<ahref="/cloud/download.do?name=myfile.zip"download="myfile.zip">下载文件</a>
可能有的地方这里的href值是类似 /clound/myfile.zip这样的,其实和这里的写法没有什么本质区别,后台只要可以返回一个文件流就可以了,差异只是开发者习惯问题。
再看看服务端:
createServer(function (request, response) {// 去request里面读取name获得,很常规,不说明了let filePath = "./myfile.zip"let fileInfo = statSync(filePath)createReadStream(filePath).pipe(response)})

2.2 拖拽下载
依旧只是前端部分略有不同(后端一模一样),我们来看一下:
<divdraggable="true"ondragstart="doDragstart">拖拽我到电脑桌面就可以下载了</div>
doDragstart里面是关键代码(只支持文件,文件夹不可以):
function doDragstart(event){event.dataTransfer.effectAllowed = "copyMove"event.dataTransfer.setData("DownloadURL", "text/plain:myfile.zip:" + window.location.origin + "/cloud/download.do?name=myfile.zip")}

夜雨聆风