前端实现文件上传

566 阅读4分钟

本文代码 | github链接

描述:

​ 前端:文件上传到服务端

    ​ 方式:选择上传、拖拽上传、复制粘贴上传

    ​ 文件类型:单文件、多文件、图片、压缩包

​ 后端:接收文件并下载到本地

后端接收(express

首先接收文件需要content-type: multipart/form-data。在前端上传文件时,会使用boundary进行内容的分割,那么我们下载文件时也需要根据它进行分割。

首先来看一下完整的content-typeboundary是可以自行设置的,或者使用默认值也可以:

'content-type': 'multipart/form-data; boundary=--------------------------xxxxx'

接下来就是具体实现,本文使用了expressexpress的路由函数可以接收两个参数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设置为filemultiple属性就是设置为可以多选:

<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进行发送,不同的是前端如何实现拖拽逻辑。

先看效果

拖拽前长这样:

拖拽前.png

拖拽后长这样:

拖拽后.png

拖拽主要使用的是dragdrop,需要配合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类型,最后appendFormData上。

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)
            }
        }
    }
})