Skip to content

JS实现浏览器下载文件和转换数据格式

3478字约12分钟

JavaScript文件流

2024-11-24

实际开发中难免会遇到下载文件的需求,比如下载图片、下载视频、下载 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 对象来创建下载的文件

使用 fetch 请求
// 请求下载地址,获取文件的二进制数据
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)

多线程下载

在浏览器环境中使用多线程下载,并监听下载进度

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

FileReaderHTML5 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 对象的基本用法:
  1. 创建 FileReader 对象:
const reader = new FileReader()
  1. 监听 load 事件,当文件读取操作成功完成时触发:
reader.onload = (event) => {
  console.log(event.target.result)
}
  1. 读取文件:
// 以文本方式读取
reader.readAsText(file)
// 以 DataURL 读取
reader.readAsDataURL(file)
// 二进制读取
reader.readAsArrayBuffer(file)
// readAsBinaryString等

示例

下面是简单的使用示例

index.html
<input type="file" id="fileInput" />
<pre id="fileContent">选择文件以显示内容...</pre>

注意

1.FileReader 的局限性:FileReader 是异步的,不能获取读取进度。

2.安全性限制:出于安全考虑,FileReader API 只能在由用户触发的事件处理函数中被调用,例如在响应一个按钮点击或文件选择事件时。

3.FileReader 的替代品Blob 对象:可以用来处理二进制文件数据。 Fetch API:可以与  Response.arrayBuffer()  方法结合使用,读取本地或网络上的二进制文件。 使用 FileReader 是处理用户文件上传内容的一种有效方式,但请记住,对于大型文件,读取操作可能需要一些时间,因此提供适当的用户反馈(如加载指示器)是很重要的。

BlobFileArrayBufferUint8Array 等转换

假如后端传过来一个 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 函数的第一个参数是一个包含 ArrayBufferArrayBufferViewBlob,或者 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 可以读取指定的 BlobFile 内容, 当读取完成后会触发 loadend 事件,同时 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。 我们的 fileToBuf 接受一个回调,它接受 ArrayBufferfilename 两个参数。

ArrayBufferBlob 互转

首先说一下 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 等对象构成的数组。

提示

DOMStringDOM 字符串,比如:<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。

使用 XHR 请求
var xhr = new XMLHttpRequest()
xhr.open("GET", "/myfile.png", true)
// 指定接受类型
xhr.responseType = "blob"

xhr.onload = function (oEvent) {
  var blob = xhr.response
  // ...
}

xhr.send()

Uint8Array

Uint8ArrayArrayBuffer 的视图,用于操作二进制数据。

1. TextEncoder => ArrayBuffer

let encoder = new TextEncoder()
// 字符 转 Uint8Array
let uint8Array = encoder.encode("你好啊")
// Uint8Array 转 ArrayBuffer
let arrayBuffer = uint8Array.buffer

Uint8Array 使用

使用 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))