在JavaScript中从Base64字符串创建BLOB

721

我有一个Base64编码的二进制数据字符串:

const contentType = 'image/png';
const b64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';

我想创建一个包含这些数据的 blob: URL,并将其显示给用户:

const blob = new Blob(????, {type: contentType});
const blobUrl = URL.createObjectURL(blob);

window.location = blobUrl;

我还没能够弄清楚如何创建BLOB。

有些情况下,我可以通过改用data: URL来避免这个问题:

const dataUrl = `data:${contentType};base64,${b64Data}`;

window.location = dataUrl;

然而,在大多数情况下,data: URL 太过冗长。


在 JavaScript 中如何将 Base64 字符串解码为 BLOB 对象?

16个回答

1122
atob函数将把一个Base64编码的字符串解码成一个新的字符串,其中每个字符对应二进制数据中的每个字节。
const byteCharacters = atob(b64Data);

每个字符的代码点(charCode)将是该字节的值。我们可以通过对字符串中每个字符应用.charCodeAt方法来创建一个字节值数组。

const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
    byteNumbers[i] = byteCharacters.charCodeAt(i);
}

将这个字节值数组转换成真正的类型化字节数组可以通过将其传递给Uint8Array构造函数来实现。

const byteArray = new Uint8Array(byteNumbers);

将其放入数组并传递给Blob构造函数,可以将此转换为BLOB。

const blob = new Blob([byteArray], {type: contentType});

上面的代码可以工作。但是,通过分批处理 byteCharacters 而不是一次性处理所有内容,可以稍微提高性能。在我的初步测试中,512字节似乎是一个很好的分片大小。这给出了以下函数。

const b64toBlob = (b64Data, contentType='', sliceSize=512) => {
  const byteCharacters = atob(b64Data);
  const byteArrays = [];

  for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    const slice = byteCharacters.slice(offset, offset + sliceSize);

    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }

  const blob = new Blob(byteArrays, {type: contentType});
  return blob;
}

const blob = b64toBlob(b64Data, contentType);
const blobUrl = URL.createObjectURL(blob);

window.location = blobUrl;

完整示例:

const b64toBlob = (b64Data, contentType='', sliceSize=512) => {
  const byteCharacters = atob(b64Data);
  const byteArrays = [];

  for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    const slice = byteCharacters.slice(offset, offset + sliceSize);

    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }

  const blob = new Blob(byteArrays, {type: contentType});
  return blob;
}

const contentType = 'image/png';
const b64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';

const blob = b64toBlob(b64Data, contentType);
const blobUrl = URL.createObjectURL(blob);

const img = document.createElement('img');
img.src = blobUrl;
document.body.appendChild(img);


10
嗨Jeremy。我们在我们的Web应用程序中使用了此代码,在下载的文件较大时,它没有引起任何问题。因此,当用户使用Chrome或IE下载大于100MB的文件时,它在生产服务器上导致挂起和崩溃。我们发现在IE中,以下行引发了内存异常:“var byteNumbers = new Array(slice.length)”。但是在Chrome中,for循环也会导致同样的问题。我们无法找到适当的解决方案,所以我们转而直接使用window.open下载文件。你能提供一些帮助吗? - Akshay Raut
这对于Chrome和Firefox上的某些blob不起作用,但在Edge上起作用:/ - user9402741
1
这对于图片可以工作,但对于mp4视频无法工作。有人知道如何将视频的base64字符串转换为blob吗?任何帮助都将不胜感激! - SuperYegorius
1
atop seems deprecated. Better use Buffer.from(data ,'base64') - Florian Falk
在缓冲区上使用charCodeAt似乎无法正常工作。 - Andrei S
嘿,Jeremy,你能看一下这个链接吗:https://stackoverflow.com/questions/77319291/how-to-send-an-excel-spreadsheet-from-backend-api-to-angular-front-end-to-downlo? - undefined

603

以下是不需要依赖库和插件的更简化方法。
它需要使用新的fetch API。(我可以使用吗?

var url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="

fetch(url)
.then(res => res.blob())
.then(console.log)

使用这种方法,您还可以轻松获取可读流、ArrayBuffer、文本和JSON。
(FYI:在Node中,node-fetch也可以使用此方法)

作为一个函数:

const b64toBlob = (base64, type = 'application/octet-stream') => 
  fetch(`data:${type};base64,${base64}`).then(res => res.blob())

但我鼓励你不要在首选中使用base64。有更好的方法来发送和接收二进制数据。JSON并不总是最佳选择。它占用更多带宽,并浪费处理时间(解/编码)。例如,使用canvas.toBlob而不是canvas.toDataURL,并使用FormData发送二进制文件。您还可以返回一个多部分负载并使用await response.formData()对来自服务器响应的内容进行解码。FormData可以双向传输。


我对Jeremy的ES6同步版本进行了简单的性能测试。
同步版本会阻止UI一段时间。保持开发工具打开可能会减慢获取性能。

document.body.innerHTML += '<input autofocus placeholder="try writing">'
// get some dummy gradient image
var img=function(){var a=document.createElement("canvas"),b=a.getContext("2d"),c=b.createLinearGradient(0,0,1500,1500);a.width=a.height=3000;c.addColorStop(0,"red");c.addColorStop(1,"blue");b.fillStyle=c;b.fillRect(0,0,a.width,a.height);return a.toDataURL()}();


async function perf() {
  
  const blob = await fetch(img).then(res => res.blob())
  // turn it to a dataURI
  const url = img
  const b64Data = url.split(',')[1]

  // Jeremy Banks solution
  const b64toBlob = (b64Data, contentType = '', sliceSize=512) => {
    const byteCharacters = atob(b64Data);
    const byteArrays = [];
    
    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
      const slice = byteCharacters.slice(offset, offset + sliceSize);
      
      const byteNumbers = new Array(slice.length);
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }
      
      const byteArray = new Uint8Array(byteNumbers);
      
      byteArrays.push(byteArray);
    }
    
    const blob = new Blob(byteArrays, {type: contentType});
    return blob;
  }

  // bench blocking method
  let i = 500
  console.time('blocking b64')
  while (i--) {
    await b64toBlob(b64Data)
  }
  console.timeEnd('blocking b64')
  
  // bench non blocking
  i = 500

  // so that the function is not reconstructed each time
  const toBlob = res => res.blob()
  console.time('fetch')
  while (i--) {
    await fetch(url).then(toBlob)
  }
  console.timeEnd('fetch')
  console.log('done')
}

perf()


3
如果base64编码后的字符串大小很大,比如超过了Opera URI大小的限制665536个字符,那么这仍然有效吗? - Daniel Kats
2
不知道,我知道它可能会是地址栏的限制,但使用AJAX进行操作可能是一个例外,因为它不需要渲染。你必须测试一下。如果是我,我第一次就不会获得base64字符串。认为这是一种不好的做法,需要更多的内存和时间来解码和编码。例如,使用createObjectURL而不是readAsDataURL要好得多。如果您使用ajax上传文件,请选择FormData而不是JSON,或者使用canvas.toBlob而不是toDataURL - Endless
31
更好的写法:await (await fetch(imageDataURL)).blob()。翻译:在这里,使用"await (await fetch(imageDataURL)).blob()"这段代码。 - icl7126
7
当然,如果你的目标是最新的浏览器,那么这需要该函数被包含在一个异步函数中。顺便说一句,await fetch(url).then(r => r.blob()) 更简洁。 - Endless
4
非常巧妙的解决方案,但根据我的了解,由于“访问被拒绝”错误,它不能在IE中运行(当然可以使用polyfill)。我猜fetch在Blob URL下暴露blob - 就像URL.createObjectUrl一样 - 这在ie11上无法工作。参考资料。也许有一些解决方法可以在IE11中使用fetch?这看起来比其他同步解决方案好得多 :) - Papi
显示剩余6条评论

107

优化后的实现(可读性较差):

function base64toBlob(base64Data, contentType) {
    contentType = contentType || '';
    var sliceSize = 1024;
    var byteCharacters = atob(base64Data);
    var bytesLength = byteCharacters.length;
    var slicesCount = Math.ceil(bytesLength / sliceSize);
    var byteArrays = new Array(slicesCount);

    for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
        var begin = sliceIndex * sliceSize;
        var end = Math.min(begin + sliceSize, bytesLength);

        var bytes = new Array(end - begin);
        for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
            bytes[i] = byteCharacters[offset].charCodeAt(0);
        }
        byteArrays[sliceIndex] = new Uint8Array(bytes);
    }
    return new Blob(byteArrays, { type: contentType });
}

3
将字节切片成块的原因是什么?如果我不使用它,是否存在任何不利或风险? - Alfred Huang
在使用Ionic 1 / Angular 1开发Android应用时表现非常出色。如果不使用Slice,可能会遇到OOM(Android 6.0.1)的问题。 - Jürgen 'Kashban' Wahlmann
7
我能够找到的唯一一个示例,可以在企业环境中无缝地与IE 11和Chrome的任何文档类型配合使用。 - santos
1
需要解释一下,例如,为什么它具有更高的性能? - Peter Mortensen
测试结果显示,在IE11上性能下降了约50%,在Chrome上性能下降了近100%,与被接受的答案相比。请注意,Chrome大约比IE11快5倍。我不知道为什么这个“优化”的答案会得到这么多赞。 - zoran404
显示剩余3条评论

40

以下是我的TypeScript代码,可以很容易地转换为JavaScript并且您可以使用它。

/**
 * Convert BASE64 to BLOB
 * @param base64Image Pass Base64 image data to convert into the BLOB
 */
private convertBase64ToBlob(base64Image: string) {
  // Split into two parts
  const parts = base64Image.split(';base64,');

  // Hold the content type
  const imageType = parts[0].split(':')[1];

  // Decode Base64 string
  const decodedData = window.atob(parts[1]);

  // Create UNIT8ARRAY of size same as row data length
  const uInt8Array = new Uint8Array(decodedData.length);

  // Insert all character code into uInt8Array
  for (let i = 0; i < decodedData.length; ++i) {
    uInt8Array[i] = decodedData.charCodeAt(i);
  }

  // Return BLOB image after conversion
  return new Blob([uInt8Array], { type: imageType });
}

5
虽然这段代码可能是解决方案,但附上解释包括在此处的可以帮助提高您发布内容的质量。请记住,您正在回答未来读者的问题,而这些人可能不知道您建议代码的原因。 - Johan

26

对于像我一样的复制粘贴爱好者,这里有一个可在Chrome、Firefox和Edge上使用的已经调试好的下载功能:

window.saveFile = function (bytesBase64, mimeType, fileName) {
var fileUrl = "data:" + mimeType + ";base64," + bytesBase64;
fetch(fileUrl)
    .then(response => response.blob())
    .then(blob => {
        var link = window.document.createElement("a");
        link.href = window.URL.createObjectURL(blob, { type: mimeType });
        link.download = fileName;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    });
}

1
和Safari(至少在桌面上) - Endless
谢谢回答,这是一个示例代码: fetch(base64) .then((response) => response.blob()) .then((blob) => { const blobUrl = URL.createObjectURL(blob); window.open(blobUrl); }); - undefined

23

为了支持所有浏览器,特别是在Android上,也许您可以添加以下内容:

try{
    blob = new Blob(byteArrays, {type : contentType});
}
catch(e){
    // TypeError old Google Chrome and Firefox
    window.BlobBuilder = window.BlobBuilder ||
                         window.WebKitBlobBuilder ||
                         window.MozBlobBuilder ||
                         window.MSBlobBuilder;
    if(e.name == 'TypeError' && window.BlobBuilder){
        var bb = new BlobBuilder();
        bb.append(byteArrays);
        blob = bb.getBlob(contentType);
    }
    else if(e.name == "InvalidStateError"){
        // InvalidStateError (tested on FF13 WinXP)
        blob = new Blob(byteArrays, {type : contentType});
    }
    else{
        // We're screwed, blob constructor unsupported entirely
    }
}

谢谢,但是如果我读得正确的话,你上面写的代码片段有两个问题:(1)在最后一个 else-if 的 catch() 中的代码与 try() 中的原始代码相同:"blob = new Blob(byteArrays, {type : contentType})" ...我不知道为什么你建议在原始异常之后重复相同的代码?... (2)BlobBuilder.append() 无法接受字节数组,而是要使用 ArrayBuffer。因此,在使用此 API 之前,输入的字节数组必须进一步转换为其 ArrayBuffer。参考:https://developer.mozilla.org/en-US/docs/Web/API/BlobBuilder - Panini Luncher

22
看这个示例:https://jsfiddle.net/pqhdce2L/

function b64toBlob(b64Data, contentType, sliceSize) {
  contentType = contentType || '';
  sliceSize = sliceSize || 512;

  var byteCharacters = atob(b64Data);
  var byteArrays = [];

  for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    var slice = byteCharacters.slice(offset, offset + sliceSize);

    var byteNumbers = new Array(slice.length);
    for (var i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    var byteArray = new Uint8Array(byteNumbers);

    byteArrays.push(byteArray);
  }
    
  var blob = new Blob(byteArrays, {type: contentType});
  return blob;
}


var contentType = 'image/png';
var b64Data = Your Base64 encode;

var blob = b64toBlob(b64Data, contentType);
var blobUrl = URL.createObjectURL(blob);

var img = document.createElement('img');
img.src = blobUrl;
document.body.appendChild(img);


需要解释一下。 - Peter Mortensen

18

如果您可以在项目中添加一个依赖项,那么有一个很棒的npm包blob-util,它提供了一个方便的base64StringToBlob函数。将其添加到您的package.json后,您可以像这样使用它:

import { base64StringToBlob } from 'blob-util';

const contentType = 'image/png';
const b64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';

const blob = base64StringToBlob(b64Data, contentType);

// Do whatever you need with your blob...

2
这个答案对我在2021年使用Angular 12有效。 - MacD

16

对于图像数据,我发现使用canvas.toBlob(异步)更简单。

function b64toBlob(b64, onsuccess, onerror) {
    var img = new Image();

    img.onerror = onerror;

    img.onload = function onload() {
        var canvas = document.createElement('canvas');
        canvas.width = img.width;
        canvas.height = img.height;

        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

        canvas.toBlob(onsuccess);
    };

    img.src = b64;
}

var base64Data = 'data:image/jpg;base64,/9j/4AAQSkZJRgABAQA...';
b64toBlob(base64Data,
    function(blob) {
        var url = window.URL.createObjectURL(blob);
        // do something with url
    }, function(error) {
        // handle error
    });

1
我猜你会丢失一些信息...例如元信息,就像将任何图像转换为PNG格式一样,结果是不同的,此外这只适用于图像。 - Endless
我猜你可以通过从base64字符串中提取图像类型“image/jpg”,然后将其作为第二个参数传递到“toBlob”函数中来改进它,以便结果是相同的类型。除此之外,我认为这是完美的 - 它可以节省30%的流量和服务器上的磁盘空间(与base64相比),并且即使使用透明PNG也可以很好地工作。 - icl7126
1
该函数在处理大于2MB的图像时崩溃... 在Android中,我会收到异常:android.os.TransactionTooLarge - Ruben

16

我在这里发布一种更加明确的同步Base64转换方法。虽然异步的 fetch().blob() 方法很简洁,我非常喜欢这个解决方案,但是它在Internet Explorer 11上不起作用(也可能适用于Edge——我没有测试过),即使使用polyfill——请查看我的评论Endless' post获取更多详细信息。

const blobPdfFromBase64String = base64String => {
   const byteArray = Uint8Array.from(
     atob(base64String)
       .split('')
       .map(char => char.charCodeAt(0))
   );
  return new Blob([byteArray], { type: 'application/pdf' });
};

奖金

如果您想打印它,可以尝试以下方法:

const isIE11 = !!(window.navigator && window.navigator.msSaveOrOpenBlob); // Or however you want to check it
const printPDF = blob => {
   try {
     isIE11
       ? window.navigator.msSaveOrOpenBlob(blob, 'documents.pdf')
       : printJS(URL.createObjectURL(blob)); // http://printjs.crabbly.com/
   } catch (e) {
     throw PDFError;
   }
};

奖励 x 2 - 在Internet Explorer 11中在新标签页中打开BLOB文件

如果您能够在服务器上对Base64字符串进行预处理,则可以在某个URL下公开它,并在printJS中使用该链接 :)


可以告诉您,Edge现在是基于Chromium的,因此它应该支持Chrome可以做的大多数事情。 - Endless

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接