描述:
前端:文件上传到服务端
方式:选择上传、拖拽上传、复制粘贴上传
文件类型:单文件、多文件、图片、压缩包
后端:接收文件并下载到本地
后端接收(express)
首先接收文件需要content-type: multipart/form-data。在前端上传文件时,会使用boundary进行内容的分割,那么我们下载文件时也需要根据它进行分割。
首先来看一下完整的content-type,boundary是可以自行设置的,或者使用默认值也可以:
'content-type': 'multipart/form-data; boundary=--------------------------xxxxx'
接下来就是具体实现,本文使用了express。express的路由函数可以接收两个参数req、res,可以使用req.on配合Buffer类型进行文件内容的接收
router.post('/', (req, res) => {
let data = Buffer.alloc(0)
req.on('data', (chunk) => {
data = Buffer.concat([data, chunk])
})
req.on('end', () => {
console.log(data)
})
})
代码中的data就是后端接收到的内容了,类型是[object Uint8Array],打印如来如下:
----------------------------boundary(可以设置或者使用默认值)
Content-Disposition: form-data; name="test-1"; filename="x.txt"; filename*=??.txt
Content-Type: text/plain
文件内容
----------------------------boundary
Content-Disposition: form-data; name="test-2"; filename="x.txt"; filename*=??.txt
Content-Type: text/plain
文件内容
----------------------------boundary--
可以清晰看到,boundary起到了分割的作用,所以接下来需要根据boundary进行对内容的分割。具体实现在uploadFile()函数中,第一个参数就是后端接收到的Uint8Array,第二个参数是boundary分割线。
boundary在实际获取时和Uint8Array中的分割线还具有一定差异,所以这里需要重新定义一下分割线。const type = `--${req.headers['content-type'].split('boundary=')[1]}`
function uploadFile(data, type) {
const filesArr = data.split(data, type).slice(1, -1)
filesArr.forEach((item, index) => {
const [head, body] = getHeadAndBody(item)
const CDispositionStr = head.split(head, '\r\n').slice(1)
const CDispositionObj = parseHeader(CDispositionStr[0].toString())
// 还有可能上传的是文本内容,不是一个文件
if (CDispositionObj.filename) {
fs.writeFile(path.resolve(__dirname, "../public/upload", CDispositionObj.filename), body.slice(0, -2), (err) => {
if (err) {
throw err
}
})
}
})
}
在uploadFile()函数中,先要对data进行分割,但是Buffer本身没有split函数,所以这里我们自己实现一个。
Buffer.prototype.split = function (buffer, type) {
const res = []
let offset = 0;
let index = buffer.indexOf(type, 0)
while (index !== -1) {
res.push(buffer.slice(offset, index))
offset = index + type.length
index = buffer.indexOf(type, index + type.length)
}
res.push(buffer.slice(offset))
return res
}
分割完成后,数组中的每一项就对应了一个文件,如下:
Content-Disposition: form-data; name="test-1"; filename="x.txt"; filename*=??.txt
Content-Type: text/plain
文件内容
所以想要保存为文件还需要对以上内容进行分割,具体在getHeadAndBody()函数中
function getHeadAndBody(buffer) {
const res = []
let index = buffer.indexOf('\r\n\r\n')
res.push(buffer.slice(0, index))
res.push(buffer.slice(index + 4))
return res
}
并在parseHeader()中拿到对应的filename
function parseHeader(header) {
const arr = header.split("; ")
const CDispositionObj = {}
arr.forEach(item => {
let [name, val] = item.split("=")
if (!val) {
name = name.split(": ")[1]
val = ""
}
val = (val[0] === '"' && val[val.length - 1] === '"') ? val.slice(1, val.length - 1) : val
CDispositionObj[name] = val
})
return CDispositionObj
}
最后通过fs.writeFile写入文件
前端上传
这里前端部分只写了基本样式,以实现功能为主。
选择上传
选择上传可以选择单文件,也可以选择多文件。同时图片、压缩包也支持上传。
选择上传主要使用的是<input>标签,type设置为file,multiple属性就是设置为可以多选:
<input type="file" multiple />
然后需要通过js获取<input>标签的所有文件,核心函数如下:
let inputVal = document.getElementById("File")
const uploadFiles = inputVal.files
uploadFileHandler(uploadFiles)
function uploadFileHandler(fileList) {
const uploadFormData = new FormData()
for (const uploadFile of fileList) {
uploadFormData.append(uploadFile.name, uploadFile)
}
// 向后端发送请求
let res = uploadFile(uploadFormData)
}
先拿到DOM元素,遍历文件,添加到FormData中
拖拽上传
拖拽上传和选择上传都是需要借助
FormData进行发送,不同的是前端如何实现拖拽逻辑。
先看效果
拖拽前长这样:
拖拽后长这样:
拖拽主要使用的是drag和drop,需要配合dataTransfer来获取文件。
这里以图片为例,<img>是想要上传的图片,<div>是拖拽区域。
<div ondrop="dropHandler(event)" ondragover="dragover_handler(event)"></div>
<img src="./测试资源/1.png" draggable id="img" ondragstart="ondragstartHandler(event)">
img:
draggable 表示可以被拖拽
ondragstart 开始拖拽时执行
div:
ondragover 拖拽结束
ondrop 放入时执行
// 开始拖放时,向dataTransfer定义一个值,为图片的id
function ondragstartHandler(e) {
e.dataTransfer.setData("dragData", e.target.id)
}
// 图片放入时,要将图片移动到div内,同时获取文件列表
function dropHandler(e) {
let id = e.dataTransfer.getData("dragData");
let doc = document.getElementById(id);
e.target.appendChild(doc);
// 获取文件列表
let dragFileList = e.dataTransfer.files
}
function dragover_handler(e) {
e.preventDefault();
}
复制粘贴上传
需要借助navigator.clipboard来获取剪切板的内容,同样这里以图片为例。
navigator.clipboard.read()可以获取剪切板的内容,对内容进行遍历,每一个clipboardItem都有一个types属性,如果是png图片的话,image/png则是我们需要的类型。
通过clipboardItem.getType(type)可以拿到一个图片的blob类文件对象,可以通过new window.File([blob], filename)转换成File类型,最后append到FormData上。
button.addEventListener("click", async (e) => {
let clipboardList = await navigator.clipboard.read()
const uploadFormData = new FormData()
for (const item of clipboardList) {
for (const type of item.types) {
if (type === 'image/png') {
let blob = await item.getType(type)
let file = new window.File([blob], "test.png")
uploadFormData.append(file.name, file)
}
}
}
})