JS 把 img 元素转 Base64:canvas + 跨域 tainted 问题

前端要把一张 <img> 转成 Base64——通常是为了上传、缓存、内联到别的地方。最直接的方式是 canvas。

基础版:canvas.toDataURL

function imgToBase64(img, type = "image/png") {
  const canvas = document.createElement("canvas");
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;

  const ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);

  return canvas.toDataURL(type);
}

const img = document.querySelector("img");
img.onload = () => console.log(imgToBase64(img));

关键点:

  • naturalWidth/naturalHeight,不是 width/height(后者是显示尺寸,会被 CSS 缩放)
  • 图片必须加载完成才能 draw,判 img.complete 或者监听 onload
  • type 可选 image/pngimage/jpegimage/webp
  • JPEG 可以传第二个参数控制画质:canvas.toDataURL("image/jpeg", 0.8)

只有 URL 没有 img 元素

async function urlToBase64(url) {
  const img = new Image();
  img.crossOrigin = "anonymous";     // 关键,让 canvas 不 tainted
  img.src = url;
  await new Promise((res, rej) => {
    img.onload = res;
    img.onerror = rej;
  });

  const canvas = document.createElement("canvas");
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  canvas.getContext("2d").drawImage(img, 0, 0);
  return canvas.toDataURL("image/png");
}

跨域的 tainted 坑

跨域图片经 canvas 一处理,toDataURL 直接抛:

SecurityError: Tainted canvases may not be exported

原因:canvas 一旦画了跨域资源就”被污染”,浏览器不让你导出数据(防止读取用户其它站的私有图片)。

要出数据,需要满足

  1. 图片服务器返回 Access-Control-Allow-Origin: *(或匹配你的域)
  2. <img> 元素设置 crossOrigin="anonymous"

如果对方服务器没配 CORS,只能:

  • 走自己的 Nginx 反代加 CORS 头
  • fetch + blob + FileReader 绕开 canvas

更稳的方案:fetch + blob + FileReader

async function urlToBase64(url) {
  const res = await fetch(url);
  const blob = await res.blob();
  return await new Promise(resolve => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  });
}

返回的就是:

data:image/png;base64,iVBORw0KGgoAAAANS...

优势:

  • 不经过 canvas,没有 tainted 问题
  • 支持任何图片格式,包括 GIF 动图(canvas 会把动图变成第一帧)
  • 保持原始质量,不会重新压缩

但 fetch 依然受 CORS 约束——服务器不配 CORS 时仍然会失败,只是不再看到”tainted”的错误,而是看到网络请求被拦。

想跳过 CORS 的最后一招

在自己的服务端加个代理:

浏览器 → your-server/proxy?url=xxx → 目标图片

服务端拿到图片,加上 CORS 头再回给浏览器。前端就走同源了。

Base64 有多大

Base64 编码后体积变大约 33%。10 KB 的图变 13.3 KB 左右。做本地缓存 / 内联小图(favicon、图标)合适;大图别用 Base64,走 URL 更省。

一句话总结

canvas.toDataURL 一行搞定,跨域首选 fetch → blob → FileReader。用 canvas 就得先解决 CORS,别硬撞 tainted 报错。