Code前端首页关于Code前端联系我们

npm

terry 4小时前 阅读数 49 #Vue

Vue3项目怎么用html2canvas完美生成高清不模糊的长截图?

最近在做的一个Vue3数据分析后台,需要把仪表盘页面一键导出成PDF,核心就是先用html2canvas把组件截成高清图,再调用jsPDF生成文件,一开始踩了好多坑,截出来的图要么文字虚、图表糊,要么跨域图片直接白块,要么长页面漏截下半截,折腾了好几天才把所有问题捋顺,今天就把这些实用经验整理成问答,帮大家少走弯路。

html2canvas和Vue3的兼容性怎么样?需要安装哪些依赖?

兼容性这块完全不用担心,html2canvas的1.x甚至0.5x版本都能适配Vue3,现在最新稳定版是1.4.1,推荐大家直接装最新的,修复了之前不少内存泄漏和跨域处理的bug。

依赖的话,只需要装html2canvas本身就行,不用额外配Vue3的专属插件,因为它本质是一个纯前端的DOM转Canvas库,和Vue的响应式、组件化框架没有强绑定,调用起来和在原生JS里差不多,安装命令用npm、yarn、pnpm都可以:

# yarn
yarn add html2canvas
# pnpm
pnpm add html2canvas

基础截Vue3单组件的步骤是什么?

基础步骤其实很简单,但有几个细节新手容易漏。

第一步,是选对要截的DOM元素,在Vue3里,推荐用ref获取DOM节点,别用document.querySelector这些原生选择器,因为Vue3的组件可能有异步渲染、条件渲染,用ref能更精准地拿到挂载后的真实DOM,比如要截一个id为dashboard的div,可以这样写:

<template>
  <div ref="dashboardRef" class="dashboard">
    <!-- 这里是要截的内容,比如图表、表格、文字 -->
  </div>
  <button @click="handleCapture">生成截图</button>
</template>
<script setup>
import { ref } from 'vue'
import html2canvas from 'html2canvas'
const dashboardRef = ref(null)
const handleCapture = async () => {
  // 一定要等DOM节点存在再执行
  if (!dashboardRef.value) return
  try {
    const canvas = await html2canvas(dashboardRef.value)
    // 这里可以把canvas转成图片,或者转成blob给jsPDF用
    const imgUrl = canvas.toDataURL('image/png')
    // 比如直接在页面展示看看效果
    const img = document.createElement('img')
    img.src = imgUrl
    document.body.appendChild(img)
  } catch (error) {
    console.error('截图失败:', error)
  }
}
</script>

刚才的代码里加了个小判断,就是检查ref.value是否存在,这点很重要,比如有时候handleCapture绑定的按钮在组件刚挂载时就点击了,ref还没绑定上,就会报错。

截出来的文字、图表虚得厉害怎么办?

这是很多人遇到的第一个大问题,其实根本原因是像素比不匹配——html2canvas默认会用浏览器的window.devicePixelRatio(DPR)来计算清晰度,但有时候如果设备DPR不高,或者我们需要生成的是用于打印、分享的高清长图,默认设置就不够用了。

解决方法也很简单,就是手动调整scale参数,同时设置useCORS(后面讲跨域会细讲)、backgroundColor这些辅助参数,scale设为2-3就足够了,太高的话会导致canvas生成慢、内存占用大,甚至有些浏览器会因为canvas太大直接崩溃。

那具体怎么改刚才的handleCapture函数呢?

const handleCapture = async () => {
  if (!dashboardRef.value) return
  try {
    const canvas = await html2canvas(dashboardRef.value, {
      // 手动设置像素比,建议2-3
      scale: 2,
      // 设置画布背景色,默认为透明,透明转PDF或者在深色背景下可能有问题
      backgroundColor: '#ffffff',
      // 允许渲染跨域图片,后面会单独讲这个参数的前提
      useCORS: true,
      // 允许加载图片的缓存
      allowTaint: false, // 这个和useCORS是二选一的,绝对不能同时设为true!
      // 关闭日志,生产环境可以开,但开发时可以看看有没有报错信息
      logging: false
    })
    const imgUrl = canvas.toDataURL('image/png', 1.0) // toDataURL的第二个参数是图片质量,PNG是无损的,设1.0就行
    // 展示或者下载的代码
    downloadImage(imgUrl, 'dashboard.png')
  } catch (error) {
    console.error('截图失败:', error)
  }
}
// 封装一个下载图片的小函数
const downloadImage = (imgUrl, fileName) => {
  const link = document.createElement('a')
  link.href = imgUrl
  link.download = fileName
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}

这里要划重点的是allowTaint和useCORS的区别:allowTaint设为true的话,即使图片跨域也能渲染,但渲染出来的canvas是“被污染的”,不能用toDataURL或者toBlob方法,会报错SecurityError,所以千万不要随便开allowTaint,要解决跨域问题,必须走useCORS的路子,而且要服务器配合。

跨域图片白块怎么办?完全没法截。

刚才提到了useCORS,但很多人用了还是白块,这是因为前端和服务器都要做配置,缺一不可。

先讲前端的配置,刚才的代码里已经加了useCORS: true,还有一点很容易被忽略:如果是Vue组件里用img标签加载的跨域图片,必须给img标签加上crossorigin属性,而且属性值要和服务器返回的Access-Control-Allow-Origin对应

比如img标签可以这样写:

<img 
  :src="imageUrl" 
  crossorigin="anonymous" 
  alt="跨域图片"
/>

crossorigin的属性值有两个:anonymous(不需要发送凭证,比如Cookie)和use-credentials(需要发送Cookie),大部分场景用anonymous就够了,如果服务器要求凭证才能访问,那不仅要把crossorigin设为use-credentials,还要给html2canvas加一个参数:withCredentials: true。

再讲服务器的配置,服务器必须在返回图片的响应头里加上Access-Control-Allow-Origin,允许我们的域名访问,不能只设为吗?如果用anonymous的话可以设为,但如果用use-credentials的话,必须设为具体的域名,比如https://your-vue3-app.com,而且还要加上Access-Control-Allow-Credentials: true。

这里还要提一个Vue3+Vite开发环境下的特殊情况:如果我们在开发环境里用了Vite的proxy代理跨域接口,但图片是直接访问CDN或者其他服务器的,没有走代理,那这时候img标签的src还是跨域的,必须走刚才的配置,如果图片走了Vite的proxy,那开发环境下就不需要配服务器的响应头了,因为Vite的proxy会自动处理,但生产环境还是要服务器自己配。

长页面漏截下半截怎么办?尤其是超过一屏的Vue3组件。

漏截长页面也是个常见问题,原因主要有三个:一是DOM元素的高度没有正确获取,二是html2canvas的canvas容器高度不够,三是DOM元素有滚动条但被隐藏了,或者滚动条的位置影响了截图范围

先排查第一个原因:检查要截的DOM元素的高度是否是实际内容的高度,而不是视口的高度,比如如果给div加了overflow: auto或者overflow: hidden,并且高度设为100vh,那div的clientHeight就是100vh,html2canvas只会截这100vh的内容,后面的就漏了,解决方法是,在截图前,暂时把DOM元素的overflow设为visible,把高度设为auto,截图完成后再改回来,这样html2canvas就能获取到完整的内容高度了。

第二个原因可以通过给html2canvas加width和height参数解决吗?其实不用,html2canvas会自动根据DOM元素的clientWidth和clientHeight(或者scrollWidth和scrollHeight,看配置)来计算canvas的大小,但刚才我们改了overflow和高度,它应该能自动算对,如果还是不行,可以手动设置canvas的width和height,但要乘以scale参数,因为scale会放大canvas。

第三个原因比较隐蔽,比如要截的DOM元素有滚动条,或者它的父元素有滚动条,那在截图前,最好把所有相关滚动条的scrollTop和scrollLeft设为0,防止截图偏移或者漏截。

那具体怎么改handleCapture函数呢?

const handleCapture = async () => {
  if (!dashboardRef.value) return
  const el = dashboardRef.value
  // 保存原来的样式,方便截图后恢复
  const originalStyle = {
    overflow: el.style.overflow,
    height: el.style.height,
    position: el.style.position, // 有时候加上position: relative会更稳定
    zIndex: el.style.zIndex // 防止被其他元素遮挡
  }
  // 保存父元素的滚动位置,或者el本身的滚动位置
  const parentScrollTop = el.parentElement?.scrollTop || 0
  const parentScrollLeft = el.parentElement?.scrollLeft || 0
  const elScrollTop = el.scrollTop
  const elScrollLeft = el.scrollLeft
  try {
    // 1. 先把所有滚动条滚到顶部
    if (el.parentElement) {
      el.parentElement.scrollTop = 0
      el.parentElement.scrollLeft = 0
    }
    el.scrollTop = 0
    el.scrollLeft = 0
    // 2. 临时修改样式,让DOM元素完全展开
    el.style.overflow = 'visible'
    el.style.height = 'auto'
    el.style.position = 'relative'
    el.style.zIndex = '9999'
    // 3. 延迟一点执行,确保DOM样式修改生效,尤其在Vue3有过渡动画的时候
    await new Promise(resolve => setTimeout(resolve, 100))
    const canvas = await html2canvas(el, {
      scale: 2,
      backgroundColor: '#ffffff',
      useCORS: true,
      allowTaint: false,
      logging: false,
      // 如果要截的是整个页面,可以把el换成document.body,但要注意body的margin
      // 这里可以加onclone回调,修改克隆的DOM,比如隐藏不需要的元素
      onclone: (clonedDoc) => {
        // 比如隐藏生成截图的按钮
        const clonedBtn = clonedDoc.querySelector('.capture-btn')
        if (clonedBtn) clonedBtn.style.display = 'none'
        // 比如给克隆的DOM加上和原来一样的背景色,防止透明
        const clonedEl = clonedDoc.querySelector('.dashboard')
        if (clonedEl) clonedEl.style.backgroundColor = '#ffffff'
      }
    })
    const imgUrl = canvas.toDataURL('image/png', 1.0)
    downloadImage(imgUrl, 'dashboard.png')
  } catch (error) {
    console.error('截图失败:', error)
  } finally {
    // 4. 无论截图成功还是失败,都要恢复原来的样式和滚动位置
    Object.assign(el.style, originalStyle)
    if (el.parentElement) {
      el.parentElement.scrollTop = parentScrollTop
      el.parentElement.scrollLeft = parentScrollLeft
    }
    el.scrollTop = elScrollTop
    el.scrollLeft = elScrollLeft
  }
}

刚才的代码里加了几个很实用的细节:一个是finally块里的恢复操作,这很重要,不然截图后页面就乱了;一个是setTimeout延迟100ms,有时候Vue3的响应式更新或者样式修改不是同步的,延迟一点能确保克隆的DOM是正确的;还有一个是onclone回调,这个回调是html2canvas在克隆DOM元素到内部的iframe之前执行的,我们可以在里面修改克隆的DOM,比如隐藏不需要的按钮、加载条,或者修改样式,不会影响原来的页面,非常好用。

还有哪些常见的坑需要注意?

除了刚才讲的几个主要问题,还有一些小细节也会导致截图失败或者效果不好,我整理了几个比较常见的:

渐变背景渲染异常

html2canvas对CSS渐变的支持不是特别完美,尤其是径向渐变、重复渐变,有时候会渲染成纯色或者乱码,解决方法是,尽量用线性渐变,如果必须用其他渐变,可以考虑用背景图片代替,或者在onclone回调里给克隆的DOM加上一个纯色的临时背景。

SVG图表渲染异常

现在很多Vue3的图表库都是用SVG做的,比如ECharts、AntV G2,html2canvas对SVG的支持也一般,有时候会文字错位、图形变形,解决方法有两个:一个是把SVG先转成canvas,再把canvas放到DOM里截图;另一个是给SVG加上inline样式,不要用外部CSS或者Vue的scoped样式,因为html2canvas有时候会找不到scoped样式的类名前缀。

比如ECharts官方就提供了getDataURL方法,可以直接把图表转成图片URL,我们可以在截图前,先把ECharts的图表容器换成img标签,加载这个URL,截图完成后再换回来:

// 假设我们有一个ECharts的实例
import * as echarts from 'echarts'
const chartRef = ref(null)
let chartInstance = null
onMounted(() => {
  chartInstance = echarts.init(chartRef.value)
  // 初始化图表的代码
})
const handleCapture = async () => {
  if (!dashboardRef.value || !chartInstance) return
  // 先把ECharts转成图片
  const chartImgUrl = chartInstance.getDataURL({
    type: 'png',
    pixelRatio: 2, // 同样要设置像素比
    backgroundColor: '#ffffff'
  })
  // 把图表容器换成img标签
  const originalChartHTML = chartRef.value.innerHTML
  chartRef.value.innerHTML = ''
  const chartImg = document.createElement('img')
  chartImg.src = chartImgUrl
  chartImg.style.width = '100%'
  chartImg.style.height = '100%'
  chartRef.value.appendChild(chartImg)
  try {
    // 这里执行刚才的截图代码
    // ...
  } finally {
    // 恢复图表容器
    chartRef.value.innerHTML = originalChartHTML
    // 重新渲染ECharts,不然图表会消失
    chartInstance.resize()
  }
}

字体渲染异常

有时候截出来的图字体和页面上的不一样,或者是默认字体,这是因为html2canvas没有正确加载自定义字体,解决方法是,确保自定义字体已经完全加载完成再执行截图,可以用document.fonts.ready来判断:

const handleCapture = async () => {
  // 先等字体加载完成
  await document.fonts.ready
  // 再执行截图的其他代码
  // ...
}

自定义字体最好用woff或者woff2格式,兼容性更好,而且要在CSS里用@font-face引入,加上font-display: swap或者font-display: block,确保字体加载时不会出现空白。

定位元素渲染异常

如果要截的DOM元素里有position: fixed或者position: absolute的元素,有时候会渲染到错误的位置,或者超出截图范围,解决方法是,在onclone回调里,把position: fixed的元素改成position: absolute,并且设置top和left为相对于克隆DOM的位置;对于position: absolute的元素,要确保它的父元素在克隆的DOM里也是position: relative或者position: absolute。

有没有什么替代方案?如果html2canvas实在满足不了需求。

html2canvas虽然好用,但确实有一些局限性,比如对复杂CSS的支持不好、截图速度慢、内存占用大,如果你的项目对截图的要求特别高,比如要支持视频、复杂动画、或者非常大的长图,可以考虑以下几个替代方案:

服务端截图

服务端截图的效果最好,支持所有CSS和JS,比如用Puppeteer(基于Chrome Headless)、Playwright(微软出的,比Puppeteer更轻量、支持更多浏览器),不过服务端截图需要部署额外的服务,开发成本和运维成本都比较高,适合预算充足、对截图质量要求极高的项目。

其他前端截图库

除了html2canvas,还有一些其他的前端截图库,比如dom-to-image(和html2canvas类似,但API更简单,对一些CSS的支持更好)、rasterizeHTML.js(但已经很久没更新了),大家可以根据自己的项目需求选择试试。

今天我们从基础安装、兼容性讲到了常见的坑和解决方法,还提到了替代方案,html2canvas是Vue3项目里实现一键截图的首选工具,只要注意像素比、跨域、长页面、DOM样式这几个关键点,就能生成高清、完整的截图。

如果大家在使用过程中还有其他问题,欢迎在评论区留言讨论,我会尽量回复的。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门