html-to-image 库的核心图片嵌入逻辑

137 阅读3分钟

核心策略:将所有外部资源转换为内联 Data URLs

background:在几个库的使用过程中,发现htmltoimage没有出现,截图中有远程资源(如图片)导致截图失败的情况,而用domtoimage和html2canvas的时候则经常出现,追踪了一下源码,发现htmltoimage不需要通过重新请求远程资源加载后截图,而是截图之前将所有图片资源转换成base64 data URLs,来避开远程请求可能的跨域或请求失败问题

源码地址:(github.com/bubkoo/html…

1. embedImageNode 函数 - 图片处理的核心

async function embedImageNode<T extends HTMLElement | SVGImageElement>(
  clonedNode: T,
  options: Options,
) {
  // 1. 检查是否是图片元素且不是 data URL
  const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement)
  if (!(isImageElement && !isDataUrl(clonedNode.src)) &&
      !(isInstanceOfElement(clonedNode, SVGImageElement) && !isDataUrl(clonedNode.href.baseVal))) {
    return  // 如果已经是 data URL,跳过处理
  }

  // 2. 获取图片 URL 并转换为 data URL
  const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal
  const dataURL = await resourceToDataURL(url, getMimeType(url), options)

  // 3. 关键优化:替换原始 URL 为 data URL
  await new Promise((resolve, reject) => {
    clonedNode.onload = resolve
    clonedNode.onerror = options.onImageErrorHandler ? (...attributes) => {
      // 错误处理逻辑
    } : reject

    // 4. 性能优化
    if (image.loading === 'lazy') {
      image.loading = 'eager'  // 强制立即加载
    }

    // 5. 清除 srcset 避免干扰,设置新的 data URL
    if (isImageElement) {
      clonedNode.srcset = ''
      clonedNode.src = dataURL  // ✨ 关键:替换为 data URL
    }
  })
}

2. 为什么这种方式解决了远程图片问题

问题根源分析:

  • 传统截图库的问题:直接截图包含远程 URL 的 DOM,Canvas 在绘制时需要重新请求这些 URL
  • CORS 限制:重新请求时触发浏览器的跨域检查

html-to-image 的解决方案:

// 转换前的 DOM
<img src="<https://external-site.com/image.jpg>" />

// 转换后的 DOM (用于截图)
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..." />

核心思想:在截图之前,将所有外部图片 URL 都转换为内联的 base64 data URLs,这样:

  • ✅ 截图时不需要任何网络请求
  • ✅ 完全避免 CORS 问题
  • ✅ 截图结果完全自包含

3. embedBackground 函数 - CSS 背景图片处理

async function embedBackground<T extends HTMLElement>(clonedNode: T, options: Options) {
  // 处理各种背景图片属性
  (await embedProp('background', clonedNode, options)) ||
  (await embedProp('background-image', clonedNode, options))

  // 处理 mask 相关属性
  (await embedProp('mask', clonedNode, options)) ||
  (await embedProp('-webkit-mask', clonedNode, options)) ||
  (await embedProp('mask-image', clonedNode, options)) ||
  (await embedProp('-webkit-mask-image', clonedNode, options))
}

这解决了 CSS 背景图片的问题

/* 转换前 */
.element { background-image: url('<https://external-site.com/bg.jpg>'); }

/* 转换后 */
.element { background-image: url('data:image/jpeg;base64,...'); }

4. 完整的处理流程

export async function embedImages<T extends HTMLElement>(clonedNode: T, options: Options) {
  if (isInstanceOfElement(clonedNode, Element)) {
    await embedBackground(clonedNode, options)    // 1. 处理背景图片
    await embedImageNode(clonedNode, options)     // 2. 处理 img 元素
    await embedChildren(clonedNode, options)      // 3. 递归处理子元素
  }
}

5. 性能优化细节

// 懒加载转为立即加载
if (image.loading === 'lazy') {
  image.loading = 'eager'
}

// 清除 srcset 避免浏览器选择其他图片
clonedNode.srcset = ''

// 使用 Promise.all 并行处理所有子元素
const deferreds = children.map((child) => embedImages(child, options))
await Promise.all(deferreds)

所以在组件内部进行截图 + html-to-image 的远程图片处理策略,能避免大部分的远程请求图片资源失败而截图失败的问题:

  1. 样式调整

    // 确保所有内容都完全展示和加载
    await new Promise((resolve) => setTimeout(resolve, 500));
    
  2. html-to-image 的资源嵌入

    • 在调用 htmlToImage.toPng() 时
    • 库内部会调用 embedImages() 将所有远程图片转为 base64
    • 然后对这个"完全自包含"的 DOM 进行截图
  3. 结果

    • ✅ 没有网络请求(您的延时确保预加载)
    • ✅ 没有 CORS 问题(转换为 data URL)
    • ✅ 完整的内容展示(您的样式调整)
    • ✅ 高质量截图(库的优化处理)

总结

html-to-image 的核心优势在于:

"预处理策略":在截图前将所有外部资源(图片、字体、CSS等)都转换为内联资源,创建一个完全自包含的 DOM 副本,然后对这个副本进行截图。

这种方法从根本上解决了:

  • 远程资源加载问题
  • CORS 跨域问题
  • 网络延迟问题
  • 资源缓存问题