JS实现浏览器下载文件和转换数据格式
实际开发中难免会遇到下载文件的需求,比如下载图片、下载视频、下载 PDF 等。 现在使用 js 手动实现这个功能。并且可能会对数据格式进行转换。
外部下载
对于一些有跨域限制的下载地址,在浏览器内部下载显然是不现实的,但是可以通过 a 标签实现下载功能 原理很简单,就是创建一个 a 标签,设置其 href 属性为下载地址,然后触发点击事件。 如果对跨域进行了处理,设置了 Access-Control-Allow-Origin 响应头头,那么也能 内部下载
通过 a 标签下载
通过 a 标签手动触发点击事件下载文件,浏览器会自动下载文件
// 创建一个 a 标签
const link = document.createElement("a")
// 设置下载的文件地址
link.href = "https://example.com/file.pdf"
// 设置下载的文件名,浏览器也会根据下载的文件名来命名文件
link.download = "file.pdf"
// 触发点击事件
link.click()手动调用 a 标签的 click 方法,浏览器会自动下载文件
通过 window 的 API 实现下载
浏览器提供了 window.open 方法,可以打开一个新的窗口,并指定下载的文件地址
window.open("https://example.com/file.pdf", "_blank")也可以使用 window.location.href 来实现下载
window.location.href = "https://example.com/file.pdf"这种实现是在当前窗口打开下载地址,如果需要下载的文件是跨域的,那么需要服务器端设置 Access-Control-Allow-Origin 头信息
内部下载
如果下载的文件是同源的,那么可以在浏览器内部下载,参考浏览器的 同源策略
提示
如果下载的文件是跨域的,那么需要服务器端设置 Access-Control-Allow-Origin 头信息
使用 Blob 下载
请求下载地址,获取文件的二进制数据,然后使用 Blob 对象来创建下载的文件
// 请求下载地址,获取文件的二进制数据
const response = await fetch("https://example.com/file.pdf")
// 使用 Blob 对象来创建下载的文件
const blob = await response.blob()
// 创建一个 a 标签
const link = document.createElement("a")
// 设置下载的文件地址
link.href = URL.createObjectURL(blob)
// 设置下载的文件名
link.download = "file.pdf"
// 触发点击事件
link.click()
// 释放内存
URL.revokeObjectURL(link.href)axios({
url: "www.baidu.pdf",
method: "GET",
responseType: "blob", // 这里就是转化为blob文件流,指定响应类型为二进制数据
headers: {
// 'Content-Type': 'application/json; application/octet-stream',
token: "token", // 可以携带token
},
}).then((res) => {
// 转换为 blob 地址,这是在内存中的地址,blob: 协议
const href = URL.createObjectURL(res.data)
const link = document.createElement("a")
link.download = "附件.pdf"
link.href = href
link.click()
URL.revokeObjectURL(href) // 释放内存
})多线程下载
在浏览器环境中使用多线程下载,并监听下载进度
function getDownloadFile(file, block) {
return new Promise(async resolve => {
const response = await fetch(file.url, {
headers: {
"Connection": "keep-alive",
},
cache: "no-store",
mode: "cors",
credentials: "omit"
})
// 获取到 Reader 对象
const reader = response.body.getReader()
// 获取文件大小
const contentLength = +response.headers.get('Content-Length')
// 已接收的数据长度
let receivedLength = 0
// 保存的字节数据
let chunks = []
// 循环读取文件
while (true) {
// 读取文件
const { done, value } = await reader.read()
// 读取完成
if (done) break
// 保存字节数据
chunks.push(value)
// 已接收的数据长度
receivedLength += value.length
// 回调函数,返回读取到的长度用于外部计算进度
block(value.length)
// 计算下载进度百分比
// const progress = (receivedLength / contentLength) * 100
// console.log(`文件 ${file.index + 1} 下载进度: ${progress.toFixed(2)}%`)
}
// 创建一个 Uint8Array 对象,用于保存接收到的字节数据
const buffer = new Uint8Array(receivedLength)
// 保存字节数据的偏移量
let position = 0
// 循环保存字节数据
for (let chunk of chunks) {
// 保存字节数据
buffer.set(chunk, position)
// 更新偏移量
position += chunk.length
}
resolve({
...file
buffer
})
})
// 下载分片文件
async function downloadSplitFile(data) {
console.log(data)
// 需要下载的文件信息
// files 对象结构 [{ index: 0, url: 'http://example.com/file.pdf', start: 0 }, { index: 1, url: 'http://example.com/file2.pdf', start: 11220 }]
// index:第几个分片, url:下载地址,start:数据保存的偏移值(后面可以自己计算这个偏移值)
const files = data.files
// 下载的总大小
let downloadTotal = 0
// 定时器,用于监听下载进度
let timer = setInterval(() => {
// 计算进度
const progress = downloadTotal * 100 / data.length
// 更新下载进度
downloadProgressEl.textContent = `${downloadTotal}/${data.length}(${progress.toFixed(2)}%)`
}, 1000)
// 下载分片文件
const promises = files.map(file => getDownloadFile(file, (size) => {
// 更新下载的总大小
downloadTotal += size
})
)
// 等待所有分片文件下载完成
// Promise.all 是并行的,实现多线程下载文件,速度更快
const results = await Promise.all(promises).finally(() => {
// 下载完成,清除定时器
clearInterval(timer)
}).catch(err => {
downloadProgressEl.textContent = err.message
})
const resultBuffer = new Uint8Array(data.length)
// 对分片进行排序,并把分片的
results.sort((a, b) => a.index - b.index).forEach(file => {
// 这里偷懒了一下,这里是可以自己计算出偏移值的
resultBuffer.set(file.buffer, file.start)
// 没有 start 属性可以这样设置
// resultBuffer.set(file.buffer, file.index * buffer.length)
})
downloadProgressEl.textContent = '下载完成'
// 创建 blob 对象
const blob = new Blob([resultBuffer], { type: 'application/octet-stream' })
// 创建 a 标签
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = fileNameEl.textContent
a.click()
// 释放内存
URL.revokeObjectURL(blob)
}关于 Blob
Blob 对象表示一个不可变原始二进制数据对象。Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
创建 Blob
const blob = new Blob([res.data], { type: "application/pdf" })URL.createObjectURL
URL.createObjectURL 方法会返回一个 DOMString,包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的 URL 对象表示指定的 File 对象或 Blob 对象。
提示
URL.createObjectURL 方法返回的 URL 是内存中的地址,blob: 协议,浏览器不会对它进行缓存,所以需要手动释放内存。
内存限制:每个 origin 的内存限制为 50 MB,超过限制会抛出 NS_ERROR_FILE_TOO_BIG 错误。
ArrayBuffer
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 不能直接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区中的数据。
下载文件,使用 fetch 转换为 ArrayBuffer 数据
// 转换为 ArrayBuffer
const arrayBuffer = fetch(url).then((res) => res.arrayBuffer())
// 使用 blob 下载
// 创建 blob 对象,type 为文件的 mimeType 类型
const blob = new Blob([arrayBuffer], { type: "application/octet-stream" })
const a = document.createElement("a")
a.href = URL.createObjectURL(blob)
a.download = "file.pdf"
a.click()
URL.revokeObjectURL(blob)FileReader
FileReader 是 HTML5 File API 的一部分,它允许在客户端对用户选择的文件进行异步读取。使用 FileReader,我们可以读取用户的文件,并基于其内容执行各种操作,如预览图片、读取文本文件等。
Reader 对象可以读取文件,并将其转换为 ArrayBuffer 数据
const reader = new FileReader()
reader.readAsArrayBuffer(file)FileReader 的主要方法:
readAsArrayBuffer(file)- 读取文件并将其内容解读为二进制数据的ArrayBuffer对象。readAsBinaryString(file)- 读取文件并将其内容解读为二进制字符串。readAsDataURL(file)- 读取文件并将其内容解读为一个基于数据URL格式的字符串。readAsText(file, [encoding])- 读取文件并将其内容解读为纯文本。encoding参数是可选的,表示文本的编码。
事件处理:
loadstart- 开始读取文件时触发。progress- 读取文件过程中触发,可以用来实现加载进度条。load- 文件读取操作成功完成时触发。abort- 文件读取操作被中断时触发。error- 文件读取操作出错时触发。loadend- 文件读取操作完成,无论成功或失败都会触发。
使用 FileReader 的步骤:
- 创建一个
<input type="file">元素,允许用户选择文件。 - 监听
change事件,当文件选择发生变化时触发。 - 使用
FileReader对象读取文件。 FileReader对象的基本用法:
- 创建
FileReader对象:
const reader = new FileReader()- 监听
load事件,当文件读取操作成功完成时触发:
reader.onload = (event) => {
console.log(event.target.result)
}- 读取文件:
// 以文本方式读取
reader.readAsText(file)
// 以 DataURL 读取
reader.readAsDataURL(file)
// 二进制读取
reader.readAsArrayBuffer(file)
// readAsBinaryString等示例
下面是简单的使用示例
<input type="file" id="fileInput" />
<pre id="fileContent">选择文件以显示内容...</pre>document
.getElementById("fileInput")
.addEventListener("change", function (event) {
var file = event.target.files[0] // 获取文件列表中的第一个文件对象
if (file) {
var reader = new FileReader()
// 当读取操作成功完成时触发
reader.onload = function (e) {
document.getElementById("fileContent").textContent = e.target.result
}
// 选择文本格式读取文件
reader.readAsText(file)
}
})注意
1.FileReader 的局限性:FileReader 是异步的,不能获取读取进度。
2.安全性限制:出于安全考虑,FileReader API 只能在由用户触发的事件处理函数中被调用,例如在响应一个按钮点击或文件选择事件时。
3.FileReader 的替代品: Blob 对象:可以用来处理二进制文件数据。 Fetch API:可以与 Response.arrayBuffer() 方法结合使用,读取本地或网络上的二进制文件。 使用 FileReader 是处理用户文件上传内容的一种有效方式,但请记住,对于大型文件,读取操作可能需要一些时间,因此提供适当的用户反馈(如加载指示器)是很重要的。
Blob、File、ArrayBuffer、Uint8Array 等转换
假如后端传过来一个 a.jpg 图片文件,但这个文件的数据类型是 ArrayBuffer,想要用 URL.createObjectURL 展示图片,如何做到?
createObjectURL 函数的参数是 File 对象、Blob 对象或者 MediaSource 对象。 因此就要将 ArrayBuffer 转成这三者中的其一类型。
ArrayBuffer、File 相互转换
ArrayBuffer 转成 File 直接调用 new File 构造函数即可:
function bufToFile(buf, filename) {
return new File([buf], filename)
}File 函数的第一个参数是一个包含 ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的数组,第二个参数表示文件名称。
File 转成 ArrayBuffer 需要借助 FileReader 类。
function fileToBuf(file, cb) {
// 创建 FileReader 对象
var fr = new FileReader()
var filename = file.name
// 读取 ArrayBuffer 二进制数据
fr.readAsArrayBuffer(file)
// 监听事件
fr.addEventListener(
"loadend",
(e) => {
var buf = e.target.result
// 回调函数
cb(buf, filename)
},
false
)
}上面函数中,fr 是 FileReader 的实例,readAsArrayBuffer 可以读取指定的 Blob 或 File 内容, 当读取完成后会触发 loadend 事件,同时 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。 我们的 fileToBuf 接受一个回调,它接受 ArrayBuffer 和 filename 两个参数。
ArrayBuffer 与 Blob 互转
首先说一下 ArrayBuffer 转成 Blob。跟 ArrayBuffer 转成 File 很像。也是调用 new Blob 构造函数:
function bufToBlob(buf, mimeType) {
// 创建 Blob 对象
return new Blob([buf], { type: mimeType })
}Blob 函数的第二个参数与 File 函数的第二个参数略有不同,Blob 是一个对象, 对象中有一个 type 属性,默认值为 “”,它代表了将会被放入到 blob 中的数组内容的 MIME 类型。 Blob 的第一个参数也是一个由 ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的数组。
提示
DOMString 是 DOM 字符串,比如:<a id="a"><b id="b">hey!</b></a>。它的 type 则是:text/html。
然后是 Blob 转成 ArrayBuffer。
Blob 转成 ArrayBuffer 也是通过 FileReader 类进行转换。上面的 File 转 ArrayBuffer 我们稍微更改一下即可:
function blobToBuf(blob, cb) {
var fr = new FileReader()
var type = blob.type
fr.readAsArrayBuffer(blob)
fr.addEventListener(
"loadend",
(e) => {
var buf = e.target.result
cb(buf, type)
},
false
)
}Blob 与 File 互转
File 对象其实是特殊类型的 Blob,且可以用在任意的 Blob 类型的上下文中。 比如说,FileReader, URL.createObjectURL(), createImageBitmap(), 及 XMLHttpRequest.send() 都能处理 Blob 和 File。File 接口也继承了 Blob 接口的属性。 这两个东西互转感觉没必要,如果要转的话,可以利用 FileReader 作为桥梁, 先转成 ArrayBuffer,然后在转成相应的 Blob 或者 File。
var xhr = new XMLHttpRequest()
xhr.open("GET", "/myfile.png", true)
// 指定接受类型
xhr.responseType = "blob"
xhr.onload = function (oEvent) {
var blob = xhr.response
// ...
}
xhr.send()fetch(url)
.then((res) => res.blob())
.then((blob) => {
console.log(blob)
})Uint8Array
Uint8Array 是 ArrayBuffer 的视图,用于操作二进制数据。
1. TextEncoder => ArrayBuffer
let encoder = new TextEncoder()
// 字符 转 Uint8Array
let uint8Array = encoder.encode("你好啊")
// Uint8Array 转 ArrayBuffer
let arrayBuffer = uint8Array.bufferUint8Array 使用
使用 Reader 对象,将 response.body 读取为 Uint8Array 数组
// ... 省略请求代
const response = fetch("http://example.com")
// 获取 Reader 对象
const reader = response.body.getReader()
const contentLength = +response.headers.get("Content-Length")
let receivedLength = 0
// 存储 Uint8Array 数组
let chunks = []
while (true) {
// 读取数据,value: 读取到的是 Uint8Array
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
receivedLength += value.length
block(value.length)
// 计算下载进度百分比
// const progress = (receivedLength / contentLength) * 100
// console.log(`文件 ${file.index + 1} 下载进度: ${progress.toFixed(2)}%`)
}
// 将 Uint8Array 数组拼接成一个 Uint8Array
const buffer = new Uint8Array(receivedLength)
let position = 0
for (let chunk of chunks) {
// 将 Uint8Array 数组拼接成一个 Uint8Array
buffer.set(chunk, position)
// 更新位置
position += chunk.length
}将 file 转换成 DataURL
1. 使用 URL.createObjectURL
let img = document.getElementById("img")
let file = document.getElementById("file")
file.onchange = function () {
let imgFile = this.files[0]
img.src = URL.createObjectURL(imgFile)
img.onload = function () {
URL.revokeObjectURL(this.src)
}
}2. 使用 FileReader
let img = document.getElementById("img")
let file = document.getElementById("file")
file.onchange = function (e) {
let imgFile = this.files[0]
let fileReader = new FileReader()
fileReader.readAsDataURL(imgFile)
fileReader.onload = function () {
img.src = this.result
}
}将 DataURL 转换成 File
function dataURLToFile(dataUrl, fileName) {
const dataArr = dataUrl.split(",")
const mime = dataArr[0].match(/:(.*);/)[1]
const originStr = atob(dataArr[1])
return new File([originStr], fileName, { type: mime })
}
dataURLToFile("data:text/plain;base64,YWFhYWFhYQ==", "测试文件")
// File {name: '测试文件', lastModified: 1640784525620, lastModifiedDate: Wed Dec 29 2021 21:28:45 GMT+0800将 canvas 转成 DataURL
document.querySelector("#file").onchange = function () {
canvasToDataURL(this.files[0]).then((res) => console.log(res))
}
function canvasToDataURL(file) {
return new Promise((resolve) => {
const img = document.createElement("img")
img.src = URL.createObjectURL(file)
img.onload = function () {
const canvas = document.createElement("canvas")
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext("2d")
ctx.drawImage(img, 0, 0)
resolve(canvas.toDataURL("image/png", 1))
}
})
}将 DataURL 转成 canvas
function dataUrlToCanvas(dataUrl) {
return new Promise((resolve) => {
const img = new Image()
img.src = dataUrl
img.onload = function () {
const canvas = document.createElement("canvas")
canvas.width = this.width
canvas.height = this.height
const ctx = canvas.getContext("2d")
ctx.drawImage(this, 0, 0)
resolve(canvas)
}
})
}
const dataUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUh..."
dataUrlToCanvas(dataUrl).then((res) => document.body.appendChild(res))