什么?你居然还在用 html2canvas 截图?!

60 阅读4分钟

在前端开发的世界里, “截图” 是一个看似简单、实则暗藏玄机的需求。无论是用户反馈、内容分享,还是自动化测试、报表生成,我们总希望把屏幕上所见的内容“原封不动”地保存为一张图片。

于是,很多人第一时间会想到那个耳熟能详的库——html2canvas

“不就是 await html2canvas(element) 吗?一行代码搞定!”

但如果你还在生产环境中重度依赖 html2canvas 来实现高保真截图,那你可能正在默默承受着这些“看不见的代价”:

  • 图标变成方块 ❌
  • 跨域图片一片空白 ❌
  • 动态图表(如 ECharts)只剩骨架 ❌
  • 弹窗阴影错位、字体模糊、布局崩坏 ❌
  • 移动端截图慢到用户以为页面卡死 ❌

更可怕的是——你以为用户看到的就是截图里的样子,其实根本不是!


一、html2canvas 的“美丽谎言”

html2canvas 并没有真正“截图”。它所做的,是用 JavaScript 重新解析 DOM 和 CSS,再手动绘制到 Canvas 上。这就像让一个没学过美术的人,看着一幅画,然后凭记忆用铅笔临摹一遍。

它无法访问:

  • 浏览器底层渲染结果(比如 GPU 加速的 transform)
  • 跨域图片的真实像素(除非服务器开了 CORS)
  • <canvas><video><webgl> 等动态内容的当前帧
  • Shadow DOM 或 CSS-in-JS 注入的私有样式

结果就是:视觉上完美无缺的界面,截图出来却面目全非

更讽刺的是,html2canvas 的文档开头就写着:

“It doesn’t actually take a screenshot… it rebuilds the page based on the properties it reads from the DOM.”

可我们却把它当成了“截图工具”来用。


二、真正的解决方案:让浏览器自己画自己

既然人工“临摹”不可靠,那不如让浏览器亲自参与绘图。而现代 Web 标准早已为我们准备了更强大的武器——SVG + foreignObject

原理一句话:

把你的 DOM 包进一个 SVG 的 <foreignObject> 里,再把这个 SVG 当成图片画到 Canvas 上。

因为 <foreignObject> 允许在 SVG 中嵌入完整的 XHTML 内容,浏览器会用它自己的渲染引擎来绘制这段 HTML——和你在屏幕上看到的完全一致!

它能解决什么?

问题html2canvasforeignObject 方案
跨域图片❌ 白屏✅ 自动 fetch 转 base64
Canvas/ECharts❌ 空白✅ 主动提取 toDataURL 替换
复杂 CSS(transform/filter)⚠️ 错位✅ 完美还原
字体图标/自定义字体❌ 方块✅ 注入完整样式表
性能⚠️ 慢(JS 模拟)✅ 快(原生渲染)

三、实战:从“能用”到“好用”的跨越

下面是一个简化版的核心逻辑:

// 1. 克隆 DOM
const cloned = element.cloneNode(true) as HTMLElement;

// 2. 清理干扰元素(loading、iframe 等)
cloned.querySelectorAll('.ant-spin, iframe').forEach(el => el.remove());

// 3. 将外部图片转为 base64(解决跨域)
await Promise.all(
  Array.from(cloned.querySelectorAll('img')).map(async img => {
    if (!img.src.startsWith('data:')) {
      const blob = await fetch(img.src).then(r => r.blob());
      img.src = URL.createObjectURL(blob);
    }
  })
);

// 4. 同步 <canvas> 内容
const originCanvas = element.querySelector('canvas');
if (originCanvas) {
  const img = document.createElement('img');
  img.src = originCanvas.toDataURL();
  cloned.replaceChild(img, cloned.querySelector('canvas')!);
}

// 5. 注入全量 CSS
const style = document.createElement('style');
Array.from(document.styleSheets).forEach(sheet => {
  try {
    style.innerHTML += Array.from(sheet.cssRules)
      .map(rule => rule.cssText).join('\n');
  } catch (e) { /* 跨域忽略 */ }
});
cloned.prepend(style);

// 6. 构建 SVG
const svg = `
  <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
    <foreignObject width="100%" height="100%">
      <div xmlns="http://www.w3.org/1999/xhtml">
        ${new XMLSerializer().serializeToString(cloned)}
      </div>
    </foreignObject>
  </svg>
`;

// 7. 绘制到 Canvas
const img = new Image();
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
img.onload = () => {
  ctx.drawImage(img, 0, 0);
};

虽然代码略长,但换来的是像素级还原的截图体验。


四、别再将就了!

html2canvas 的价值在于“快速原型”或“内部工具”,但绝不适合对截图质量有要求的生产环境

如果你的用户正在因为截图错乱而投诉,如果你的运营团队发现分享图和实际页面对不上——是时候升级你的截图方案了。

真正的截图,不是“模拟渲染”,而是“委托渲染”。

让浏览器做它最擅长的事:渲染。而我们,只需巧妙地引导它把结果画到一张图上。


五、未来展望

随着 Web API 的演进,或许有一天我们会拥有原生的 element.screenshot() 方法。但在那一天到来之前,SVG + foreignObject 依然是最接近“真实截图”的可行方案。

所以,下次当你听到同事说“用 html2canvas 截个图吧”,不妨反问一句:

“什么?你居然还在用 html2canvas 截图?!”

是时候告别“伪截图”时代了。