前端文件上传下载

前端文件上传下载,这里总有一种适合你!

image-20220917233226567

简介

遇到前端文件上传下载,可能会先想到前人已经写好的轮子(模块或者第三方库),引入就能用了。如果想进一步了解,可以接着往下看,本文主要讲述前端文件上传下载,可能涉及到前端文件的数据类型,例如ArrayBuffer, TypedArray, DataView, Blob, File, Base64, FileReader等。

下载

超链接下载

最常见的一种简单实现方式。

下载本地资源:

给超链接加上 download 属性

1
<a href="./img/logo.png" download="testDownload">直接下载图片</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建一个<a></a>标签
const a = document.createElement('a')
// 给a标签的href属性值加上地址,注意:这里是绝对路径,不用加 点.
a.href = '/xxxTemplate.xlsx'
// 设置下载文件文件名,这里加上.xlsx指定文件类型,pdf文件就指定.fpd即可
a.download = 'xxx模板.xlsx'
// 障眼法藏起来a标签
a.style.display = 'none'
// 将a标签追加到文档对象中
document.body.appendChild(a)
// 模拟点击了<a>标签,会触发<a>标签的href的读取,浏览器就会自动下载了
a.click()
// 一次性的,用完就删除a标签
a.remove()

下载服务器资源

这就需要用到Blob url或者Base64 data

Blob url,可以使用window.URL.createObjectURL(blob)方法生成 Blob url,然后将Blob url赋值给超链接的href属性,然后模拟点击超链接进行下载。

Blob url,简单的理解一下就是将一个fileBlob类型的对象转为UTF-8UTF-16等字符串,并保存在当前操作的document下,存储在内存中。

生成blob url使用的方法是URL.createObjectURL(file/blob)。清除方式只有页面unload()事件或者使用URL.revokeObjectURL(objectURL)手动清除 。

Base64 data,可以直接把Base64 data赋值给超链接的href属性,然后模拟点击超链接进行下载。

下面我将模拟后端返回ArrayBufferBlob对象和base64数据来实现超链接方式下载。

后端返回 ArrayBuffer:

因为生成Blob url的参数只能是BlobFile对象,所以对于后端返回的ArrayBuffer我们需要先转成Blob或者File对象然后再生成Blob url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function aDownload1() {
// 模拟后端返回 ArrayBuffer
const str = 'hello huatree!'
let ab = new ArrayBuffer(str.length)
let ia = new Uint8Array(ab)
for (let i = 0; i < str.length; i++) {
ia[i] = str.charCodeAt(i)
}

const a = document.createElement('a')
// 设置文件名为test
a.download = 'test'
// 将 ArrayBuffer 转成Blob,这里也可以转成File对象。
const blob = new Blob([ia], { type: 'text/plain' })
// 生成Blob url。这里可以使用Blob对象或者File对象
a.href = window.URL.createObjectURL(blob)
a.style.display = 'none'
document.body.appendChild(a)
a.click()
// 释放内存
window.URL.revokeObjectURL(a.href)
// 移除a元素
document.body.removeChild(a)
}

后端返回 Blob:

直接生成Blob url就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function aDownload2() {
// 模拟后端返回 Blob
const blob = new Blob(['hello', 'randy'], { type: 'text/plain' })

const a = document.createElement('a')
// 设置文件名为test
a.download = 'test'
// 直接生成Blob url。这里可以使用Blob对象或者File对象
a.href = window.URL.createObjectURL(blob)
a.style.display = 'none'
document.body.appendChild(a)
a.click()
// 释放内存
window.URL.revokeObjectURL(a.href)
// 移除a元素
document.body.removeChild(a)
}

后端返回base64:

这种情况少见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function aDownload3() {
// 模拟后端返回 Base64
// 这里就是将本地的图片转为base64
const b1 = await img2base64('./imgs/logo.png')

const a = document.createElement('a')
a.download = 'test'
// 给超链接赋值Base64也是可以的。
a.href = b1
a.style.display = 'none'
document.body.appendChild(a)
a.click()
// 移除超链接
document.body.removeChild(a)
}

showSaveFilePicker API 下载

showSaveFilePicker 是一个新的api,调用该方法后会显示允许用户选择保存路径的文件选择器。

1
const FileSystemFileHandle = Window.showSaveFilePicker(options)
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
async function download3(blob, filename) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [
{
description: 'text file',
accept: {
'text/plain': ['.txt']
}
},
{
description: 'jpeg file',
accept: {
'image/jpeg': ['.jpeg']
}
}
]
})
const writable = await handle.createWritable()
await writable.write(blob)
await writable.close()
return handle
} catch (err) {
console.error(err.name, err.message)
}
}

function showSaveFilePickerDownload() {
// 模拟blob文件
const blob = new Blob(['hello', 'randy'], { type: 'text/plain' })

download3(blob, 'test.txt')
}

当你点击下载后会出现文件选择界面,及你建议的文件名和文件类型选择。

相比 a 标签下载 的方式,showSaveFilePicker API 允许你选择文件的下载目录、选择文件的保存格式和更改存储的文件名称。不过可惜的是该 API 目前的兼容性还不是很好。详见

注意这种下载方式下载的文件不会出现在浏览器下载列表。

FileSaver 下载

FileSaver.js是在客户端保存文件的解决方案,非常适合在客户端上生成文件的 Web 应用程序。

Browser Constructs as Filenames Max Blob Size Dependencies
Firefox 20+ Blob Yes 800 MiB None
Firefox < 20 data: URI No n/a Blob.js
Chrome Blob Yes 2GB None
Chrome for Android Blob Yes RAM/5 None
Edge Blob Yes ? None
IE 10+ Blob Yes 600 MiB None
Opera 15+ Blob Yes 500 MiB None
Opera < 15 data: URI No n/a Blob.js
Safari 6.1+* Blob No ? None
Safari < 6 data: URI No n/a Blob.js
Safari 10.1+ Blob Yes n/a None

对于FileSaver.js 我们主要需要记住他的这个saveAs方法。

1
FileSaver.saveAs(Blob/File/Url, optional DOMString filename, optional Object { autoBom })

jszip 压缩下载

jszip可以让下载的文件转为zip格式。

jszip自己不具备下载功能,他只是提供了将文件压缩成zip包的功能,下载的话我们还是需要借助前面所说的FileSaver.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建 JSZip 对象
var zip = new JSZip()

// 把文件添加到前面创建的 JSZip 对象中,可以添加多个
zip.file('Hello.txt', 'Hello World\n')
// 添加第二个文件,文件是blob对象
zip.file('Hello2.txt', blob)
// 添加第二个文件,文件是file对象
zip.file('Hello3.txt', file)

// 生成 JSZip 文件
zip.generateAsync({ type: 'blob' }).then(function (content) {
// 这里需要用到上面说的 FileSaver.js
FileSaver.saveAs(content, 'example.zip')
})

附件形式下载

我们平时在浏览器输入图片链接地址,为什么有的图片是预览而有的却是直接下载呢?

这个链接在浏览器打开是直接预览

这个链接在浏览器打开是直接下载

这里就涉及到附件形式下载了。

我们可以通过设置 Content-Disposition 响应头来指示响应的内容以何种形式展示,是以内联(inline)的形式,还是以附件(attachment)的形式下载并保存到本地。filename 用来设置下载的文件的文件名。

1
2
3
Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

打开控制台可以发现,我们上面的第二张图就是设置了Content-Disposition响应头,所以我们输入完图片链接后会以附件的形式直接下载。

image-20220917231432235

了解更多,详见

上传

不管使用何种方式,都是先获取到文件对象然后在利用表单FormData对象进行传输。

单文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<input id="uploadFile1" type="file" accept="image/*" />

<script>
const upload = () => {
// 获取上传的input元素
const uploadFileEle = document.querySelector('#uploadFile1')
// 获取文件
const files = uploadFileEle.files
let formData = new FormData()
formData.append(fieldName, files[0])
// 进行请求
// axios.post(url, formData)
}
</script>

多文件上传

input元素里面添加multiple属性,表示支持多文件上传。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<input id="uploadFile2" type="file" accept="image/*" multiple />

<script>
const upload = () => {
// 获取上传的input元素
const uploadFileEle = document.querySelector('#uploadFile2')
// 获取文件
const files = uploadFileEle.files
let formData = new FormData()
// 需要在表单里面循环添加我们的文件
Object.values(files).forEach((file, i) => {
formData.append('file' + i, file)
})
// 进行请求
// axios.post(url, formData)
}
</script>

文件夹上传

input元素里面添加webkitdirectory属性,表示是文件夹上传。

该属性的兼容性如下,需要注意IE是完全不支持的。详见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<input id="uploadFile3" type="file" accept="image/*" webkitdirectory />

<script>
const upload = () => {
// 获取上传的input元素
const uploadFileEle = document.querySelector('#uploadFile3')
// 获取文件
const files = uploadFileEle.files
let formData = new FormData()
// 需要在表单里面循环添加我们的文件
Object.values(files).forEach((file, i) => {
formData.append('file' + i, file)
})
// 进行请求
// axios.post(url, formData)
}
</script>

以文件夹方式上传的话,在选择文件夹后会有个小提示。并且我们可以在File对象里面通过webkitRelativePath属性看到该文件的相对路径。

jszip 压缩上传

压缩上传就是将文件压缩成压缩包,然后再上传到服务端。压缩还是使用我们前面介绍的jszip库。

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
<input id="uploadFile4" type="file" accept="image/*" webkitdirectory />

<script>
function generateZipFile(zipName, files, options = { type: 'blob', compression: 'DEFLATE' }) {
return new Promise((resolve, reject) => {
// 创建 JSZip 对象
const zip = new JSZip()
Object.values(files).forEach((file, i) => {
// 循环遍历 把文件添加到前面创建的 JSZip 对象中
zip.file('file' + i, file)
})
// 生成 JSZip 文件
zip.generateAsync(options).then(function (blob) {
zipName = zipName || Date.now() + '.zip'
const zipFile = new File([blob], zipName, {
type: 'application/zip'
})
resolve(zipFile)
})
})
}

async function uploadFile() {
// 获取上传的input元素
const uploadFileEle = document.querySelector('#uploadFile4')
// 获取文件
const files = uploadFileEle.files
// 获取相对路径
let webkitRelativePath = fileList[0].webkitRelativePath
// 获取文件夹的名字,用做zip包的名字
let zipFileName = webkitRelativePath.split('/')[0] + '.zip'
let zipFile = await generateZipFile(zipFileName, fileList)

let formData = new FormData()
formData.append('zipfile', zipFile)
// 进行请求
// axios.post(url, formData)
}
</script>

拖拽上传

要实现拖拽上传的功能,我们需要先了解与拖拽相关的事件。比如 drag、dragend、dragenter、dragover 或 drop 事件等。

dragenter:当拖拽元素或选中的文本到一个可释放目标时触发;
dragover:当元素或选中的文本被拖到一个可释放目标上时触发(每 100 毫秒触发一次);
dragleave:当拖拽元素或选中的文本离开一个可释放目标时触发;
drop:当元素或选中的文本在可释放目标上被释放时触发。
关于拖拽事件大家可以查看 mdn 官方文档笔者在这里就不细说了。

拖拽上传的核心是通过 DataTransfer 对象的 files 属性来获取文件列表,然后在利用 FormData 进行上传。
核心代码

1
2
3
4
5
6
7
8
9
10
11
12
dropAreaEle.addEventListener('drop', handleDrop, false)

function handleDrop(e) {
// 在dataTransfer对象上获取文件列表
const files = e.dataTransfer.files
let formData = new FormData()
Object.values(files).forEach((file, i) => {
formData.append('file' + i, file)
})
// 进行请求
// axios.post(url, formData)
}

复制粘贴上传

对于复制粘贴我们首先需要了解 Clipboard 对象。

我们可以通过 navigator.clipboard 来获取 Clipboard 对象,然后通过 navigator.clipboard.read()获取内容。但是对于不兼容的我们需要通过 e.clipboardData.items 来访问剪贴板中的内容。

下面的例子是获取剪切板里面的图片进行上传。

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
onst IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;
const uploadAreaEle = document.querySelector("#uploadArea");

// 监听粘贴事件
uploadAreaEle.addEventListener("paste", async (e) => {
e.preventDefault();
const files = [];
if (navigator.clipboard) {
let clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
if (IMAGE_MIME_REGEX.test(type)) {
const blob = await clipboardItem.getType(type);
files.push(blob);
}
}
}
} else {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (IMAGE_MIME_REGEX.test(items[i].type)) {
let file = items[i].getAsFile();
files.push(file);
}
}
}

// 有了files我们就可以利用FormData进行上传啦
let formData = new FormData();
files.forEach((file, i) => {
formData.append("file" + i, file);
});
// 进行请求
// axios.post(url, formData)
});

关于 FormData

前面的上传都涉及到了FormData,关于FormData很多小伙伴可能不太理解,笔者在这里详细讲解下关于FormData的相关api

FormData我们可以想像成js版的表单。功能和我们的html表单是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过FormData构造函数创建一个空对象
const formdata = new FormData()
// 可以通过append()方法来追加数据
formdata.append('name', 'randy')
// 通过get方法对值进行读取
console.log(formdata.get('name')) //randy
// 通过set方法对值进行设置
formdata.set('name', 'demi')
console.log(formdata.get('name')) //demi
// 获取key为age的所有值,返回值为数组类型
formdata.getAll('age')
// 判断是否包含key为name的数据
console.log(formdata.has('name')) //true
// 删除key为name的值
formdata.delete('name')

除了创建一个全新的formData,我们还可以基于一个现有表单进行初始化。

1
<form id="myForm">名称:<input type="text" name="name" value="randy" /></form>
1
2
3
4
5
// 根据id获得页面当中的form表单元素
const myForm = document.querySelector('#myForm')
// 将获得的表单元素作为参数,对formData进行初始化
const formdata = new FormData(myForm)
console.log(formdata.get('name')) // randy

对于formData 类似Object,支持keysvaluesentries三种遍历方式

1
2
3
formData.keys()
formData.values()
formData.entries()

了解更多,详见

阿里 oss 上传和下载

除了上面介绍的在自己服务器上传下载,我们还可能会碰到第三方服务器的上传和下载,例如oss

oss node 文档

相关链接

[1] 前端二进制 ArrayBuffer、TypedArray、DataView、Blob、File、Base64、FileReader 一次性搞清楚