Vue3 实现页面快照截图功能(保姆级教程)

我爱海鲸 2026-04-02 18:30:12 暂无标签

简介快照上传

Vue3 实现页面快照截图功能(保姆级教程)

在 Vue3 项目中,网页截图/快照是非常实用的功能(比如生成海报、保存页面状态、导出报表等)。本文将带你用最主流的 html2canvas 库,零门槛实现 Vue3 截图功能,包含完整代码、踩坑避坑和细节优化。

一、核心技术选型

实现前端截图,我们选用 html2canvas(最成熟、兼容性最好的前端截图库):
  • 纯前端实现,无需后端参与
  • 支持 Vue3 / Vite / 浏览器全环境
  • 可将任意 DOM 节点转为图片(Base64 / 图片文件)

二、环境准备

1. 安装依赖

打开项目终端,执行安装命令:
# npm
npm install html2canvas
# yarn
yarn add html2canvas
# pnpm(推荐)
pnpm add html2canvas

2. 引入库

在需要使用截图功能的 Vue 文件中引入:
import html2canvas from 'html2canvas';

三、完整实现代码(Vue3 + Setup 语法糖)

 

<template>
  <div class="screenshot-container">
    <!-- 需要截图的区域(给这个 div 加 ref) -->
    <div ref="screenshotRef" class="screenshot-box">
      <h3>Vue3 快照截图演示区域</h3>
      <p>这是需要被截图的内容~</p>
      <img 
        src="https://picsum.photos/300/200" 
        alt="示例图片"
        crossOrigin="anonymous" 
      >
    </div>

    <!-- 截图按钮 -->
    <button @click="handleScreenshot" class="screenshot-btn">
      点击生成快照
    </button>

    <!-- 截图预览 -->
    <div v-if="screenshotUrl" class="preview-box">
      <h4>截图预览:</h4>
      <img :src="screenshotUrl" alt="快照预览">
      <a :href="screenshotUrl" download="vue3-screenshot.png">
        点击下载图片
      </a>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import html2canvas from 'html2canvas';

// 绑定需要截图的 DOM 节点
const screenshotRef = ref(null);
// 存储截图生成的 Base64 地址
const screenshotUrl = ref('');

// 核心:截图处理函数
const handleScreenshot = async () => {
  // 判空:确保 DOM 节点存在
  if (!screenshotRef.value) return;

  try {
    // 调用 html2canvas 生成截图(配置项优化)
    const canvas = await html2canvas(screenshotRef.value, {
      scale: window.devicePixelRatio * 2, // 高清截图(解决模糊问题)
      useCORS: true, // 开启跨域(解决网络图片不显示问题)
      logging: false, // 关闭控制台日志
      backgroundColor: null // 透明背景(需要白色背景可设为 #ffffff)
    });

    // 将 canvas 转为 Base64 图片地址
    screenshotUrl.value = canvas.toDataURL('image/png');
    alert('截图生成成功!');
  } catch (error) {
    console.error('截图失败:', error);
    alert('截图失败,请检查控制台错误信息');
  }
};
</script>

<style scoped>
.screenshot-container {
  max-width: 600px;
  margin: 50px auto;
  padding: 20px;
}
.screenshot-box {
  padding: 20px;
  border: 2px dashed #42b983;
  border-radius: 8px;
  margin-bottom: 20px;
}
.screenshot-btn {
  padding: 10px 20px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.preview-box {
  margin-top: 20px;
  padding: 10px;
  border: 1px solid #eee;
}
.preview-box img {
  max-width: 100%;
  margin: 10px 0;
}
</style>

压缩图片到200k以内

const handleScreenshot = async () => {
  if (!screenshotRef.value) return

  let targetElement = null
  let scrollContainer = null
  let originalTargetStyle = null
  let originalContainerStyle = null

  try {
    // 你要求“base64 压到 200k以内”。base64 文本长度大约是 blob 字节数 * 4/3
    // 所以 blob 需要小于等于约 150KB 才能保证 base64 文本 <= 200KB 左右。
    const targetBase64MaxChars = 200 * 1024 // 200KB(按字符数)
    const targetMaxBytes = Math.max(10 * 1024, Math.floor((targetBase64MaxChars * 3) / 4) - 1024)

    const blobToBase64 = (blob) =>
      new Promise((resolve) => {
        const reader = new FileReader()
        reader.onload = () => {
          const result = String(reader.result || '')
          // data URL 形如:data:image/jpeg;base64,xxxx
          const base64 = result.includes(',') ? result.split(',')[1] : result
          resolve(base64)
        }
        reader.onerror = () => resolve('')
        reader.readAsDataURL(blob)
      })

    const canvasToJpegBlob = (sourceCanvas, quality) =>
      new Promise((resolve) => {
        sourceCanvas.toBlob((b) => resolve(b), 'image/jpeg', quality)
      })

    const compressCanvasToUnderLimit = async (sourceCanvas, maxBytes) => {
      // JPEG 通常比 PNG 小很多(而且你“不考虑像素” -> 可以接受更低清晰度)
      const qualitySteps = [0.9, 0.8, 0.7, 0.6, 0.5, 0.42, 0.35, 0.28, 0.22, 0.18, 0.14, 0.1, 0.08, 0.06, 0.04, 0.02]
      for (let i = 0; i < qualitySteps.length; i++) {
        const q = qualitySteps[i]
        const blob = await canvasToJpegBlob(sourceCanvas, q)
        if (blob && blob.size <= maxBytes) return { blob, quality: q }
      }
      const blob = await canvasToJpegBlob(sourceCanvas, qualitySteps[qualitySteps.length - 1])
      return { blob, quality: qualitySteps[qualitySteps.length - 1] }
    }

    targetElement = screenshotRef.value
    scrollContainer = targetElement.closest('.app-wrapper') || null

    // 保存原样式(用于恢复)
    originalTargetStyle = {
      height: targetElement.style.height,
      overflow: targetElement.style.overflow,
      transform: targetElement.style.transform
    }
    originalContainerStyle = scrollContainer
      ? {
          overflowY: scrollContainer.style.overflowY,
          overflowX: scrollContainer.style.overflowX,
          height: scrollContainer.style.height,
          overflow: scrollContainer.style.overflow
        }
      : null

    // 1) 避免滚动容器裁剪:临时把滚动容器改成可见
    if (scrollContainer) {
      scrollContainer.style.overflowY = 'visible'
      scrollContainer.style.overflowX = 'hidden'
      scrollContainer.style.height = 'auto'
      scrollContainer.style.overflow = 'visible'
    }

    // 2) 强制展开目标元素完整高度
    targetElement.style.height = `${targetElement.scrollHeight}px`
    targetElement.style.overflow = 'visible'
    targetElement.style.transform = 'none'

    await nextTick()

    // 3) 等图片加载(减少出现“部分区域空白”)
    const images = Array.from(targetElement.querySelectorAll('img'))
    await Promise.all(
      images.map((img) => {
        if (img.complete) return Promise.resolve()
        return new Promise((resolve) => {
          img.onload = () => resolve()
          img.onerror = () => resolve()
        })
      })
    )

    const totalWidth = targetElement.scrollWidth
    const totalHeight = targetElement.scrollHeight
    const deviceScale = window.devicePixelRatio || 1

    // 用缩放确保“最终拼接后的长图”不会超出 canvas 最大尺寸限制
    // 你不考虑像素 -> 可以把清晰度降到很低来满足 200KB
    const maxOutputSidePx = 16000
    const estimateInitialScale =
      Math.min(
        1,
        maxOutputSidePx / (totalWidth * deviceScale),
        maxOutputSidePx / (totalHeight * deviceScale)
      ) || 0.05

    const maxCanvasHeight = 16000 // 每段 html2canvas 的渲染上限(CSS 高度会据此分块)

    const renderLongCanvas = async (exportScale) => {
      const scale = deviceScale * exportScale
      const chunkCssHeight = Math.max(400, Math.floor(maxCanvasHeight / scale))

      const outW = Math.max(1, Math.ceil(totalWidth * scale))
      const outH = Math.max(1, Math.ceil(totalHeight * scale))

      const finalCanvas = document.createElement('canvas')
      finalCanvas.width = outW
      finalCanvas.height = outH

      const ctx = finalCanvas.getContext('2d')
      if (!ctx) throw new Error('finalCanvas 2D context is null')

      // 先填充白底,避免透明导致“黑块”
      ctx.fillStyle = '#ffffff'
      ctx.fillRect(0, 0, outW, outH)

      for (let y = 0; y < totalHeight; y += chunkCssHeight) {
        const chunkHeight = Math.min(chunkCssHeight, totalHeight - y)

        const chunkCanvas = await html2canvas(targetElement, {
          x: 0,
          y,
          width: totalWidth,
          height: chunkHeight,
          scale,
          useCORS: true,
          logging: false,
          backgroundColor: '#ffffff',
          scrollX: 0,
          scrollY: 0,
          windowWidth: totalWidth,
          windowHeight: chunkHeight
        })

        const drawY = Math.floor(y * scale)
        ctx.drawImage(chunkCanvas, 0, drawY)
      }

      return finalCanvas
    }

    let exportScale = estimateInitialScale
    let finalBlob = null
    let finalQuality = 0
    let finalCanvas = null

    // 最多尝试六次:如果仍超 base64 字符数,就进一步降低缩放重渲染
    for (let attempt = 0; attempt < 6; attempt++) {
      finalCanvas = await renderLongCanvas(exportScale)
      const result = await compressCanvasToUnderLimit(finalCanvas, targetMaxBytes)
      finalBlob = result.blob
      finalQuality = result.quality

      // 用 blob size 估算 base64 文本长度(避免先转 base64 再判断)
      // base64 length ~= ceil(bytes/3)*4
      const estBase64Chars = finalBlob ? Math.ceil(finalBlob.size / 3) * 4 : Infinity
      const okBlob = finalBlob && finalBlob.size <= targetMaxBytes
      const okBase64 = estBase64Chars <= targetBase64MaxChars

      console.log(
        `长图压缩尝试:attempt=${attempt + 1}`,
        'blobBytes=', finalBlob ? finalBlob.size : null,
        'estBase64Chars=', estBase64Chars,
        'quality=', finalQuality,
        'exportScale=', exportScale
      )

      if (okBlob && okBase64) break
      exportScale *= 0.8
    }

    if (!finalBlob) throw new Error('截图生成失败:finalBlob 为空')

    const base64 = await blobToBase64(finalBlob)
    window.__GX_SCREENSHOT_LONG_BASE64 = base64
    // 打印“带前缀”的 dataURL(用于你直接复制到浏览器地址栏查看)
    const dataUrl = `data:image/jpeg;base64,${base64}`
    window.__GX_SCREENSHOT_LONG_DATA_URL = dataUrl
    // 控制台/复制超长字符串可能会显示被截断;为避免你粘贴不完整,这里自动复制到剪贴板。
    const copyToClipboard = async (text) => {
      // 优先使用现代剪贴板 API
      try {
        if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
          await navigator.clipboard.writeText(text)
          return true
        }
      } catch (_) {}

      // 兜底:textarea 选中复制
      try {
        const ta = document.createElement('textarea')
        ta.value = text
        ta.style.position = 'fixed'
        ta.style.left = '-9999px'
        ta.style.top = '-9999px'
        document.body.appendChild(ta)
        ta.focus()
        ta.select()
        const ok = document.execCommand('copy')
        document.body.removeChild(ta)
        return ok
      } catch (_) {}

      return false
    }

    try {
      // 先复制纯 base64(上传通常用这个)
      const okBase64 = await copyToClipboard(base64)

      // 再复制带前缀的 dataURL(用于你直接粘贴到浏览器地址栏预览)
      const okDataUrl = await copyToClipboard(dataUrl)
    } catch (e) {
      console.warn('复制到剪贴板失败:', e)
    }

    // 只保留一条:直接把 dataURL 打出来,让 DevTools 直接按“图片”渲染/预览
    // 注意:控制台可能仍有显示截断,但 DevTools 通常能识别 dataURL 并显示图片预览
    console.log(dataUrl)

    if (base64.length > targetBase64MaxChars) {
      alert(
        `长图已生成,但 base64 仍超 200KB:base64Len=${base64.length} chars,JPEG=${(finalBlob.size / 1024).toFixed(
          1
        )}KB,quality=${finalQuality}。\n预览请打开 window.__GX_SCREENSHOT_LONG_DATA_URL;上传请用 window.__GX_SCREENSHOT_LONG_BASE64。`
      )
    } else {
      alert(
        `长图生成完成:JPEG=${(finalBlob.size / 1024).toFixed(1)}KB,quality=${finalQuality},base64Len=${base64.length} chars。\n预览请打开 window.__GX_SCREENSHOT_LONG_DATA_URL;上传请用 window.__GX_SCREENSHOT_LONG_BASE64。`
      )
    }
  } catch (error) {
    console.error('截图失败:', error)
    alert('截图失败,请检查控制台')
  } finally {
    // 恢复样式,避免影响页面正常显示
    if (targetElement && originalTargetStyle) {
      targetElement.style.height = originalTargetStyle.height
      targetElement.style.overflow = originalTargetStyle.overflow
      targetElement.style.transform = originalTargetStyle.transform
    }
    if (scrollContainer && originalContainerStyle) {
      scrollContainer.style.overflowY = originalContainerStyle.overflowY
      scrollContainer.style.overflowX = originalContainerStyle.overflowX
      scrollContainer.style.height = originalContainerStyle.height
      scrollContainer.style.overflow = originalContainerStyle.overflow
    }
  }
};

四、代码核心讲解

1. 关键 API 说明

ref="screenshotRef":给需要截图的 DOM 绑定引用,Vue3 中精准获取节点
html2canvas(dom, options):核心方法,接收 DOM 节点和配置项,返回 canvas 对象
canvas.toDataURL('image/png'):将 canvas 转为 Base64 格式图片,可直接用于预览、下载、上传

2. 核心配置项(必看)

配置项
作用
scale
放大比例,解决截图模糊问题,推荐值:window.devicePixelRatio * 2
useCORS: true
解决跨域图片不显示(网络图片、CDN 图片必须开启)
backgroundColor: null
生成透明背景截图,默认白色
logging: false
关闭控制台调试日志,更整洁

五、高频踩坑避坑指南(博客必备干货)

1. 截图模糊?

解决方案:添加 scale 配置,放大渲染分辨率
scale: window.devicePixelRatio * 2 // 高清核心

2. 网络图片/后台返回图片不显示?

解决方案:
  1. 配置 useCORS: true
  2. 图片标签添加 crossOrigin="anonymous"
<img src="图片地址" crossOrigin="anonymous">

3. 滚动页面只截可见区域?

解决方案:截图前临时将元素高度设为完整高度,或使用 scrollY: 0 配置

4. 圆角/阴影样式丢失?

解决方案:给截图区域添加 overflow: hidden,确保样式正常渲染

六、进阶功能扩展

1. 直接下载截图(无需点击)

在生成 screenshotUrl 后,添加自动下载代码:
// 自动下载
const link = document.createElement('a');
link.download = 'vue3-快照.png';
link.href = screenshotUrl.value;
link.click();

2. 上传截图到服务器

将 Base64 转为 Blob 对象,用接口上传:
// Base64 转 Blob
const dataURLtoBlob = (dataurl) => {
  const arr = dataurl.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  while (n--) u8arr[n] = bstr.charCodeAt(n);
  return new Blob([u8arr], { type: mime });
};

// 上传
const blob = dataURLtoBlob(screenshotUrl.value);
const formData = new FormData();
formData.append('file', blob, 'screenshot.png');
// 调用上传接口...

七、适用场景

  • 后台系统报表导出截图
  • 活动页面生成分享海报
  • 表单/页面状态保存
  • 富文本内容导出图片

总结

  1. Vue3 截图首选 html2canvas,轻量易用、兼容性强
  2. 核心三步:获取DOM → 调用库生成canvas → 转图片预览/下载
  3. 必配参数:scale(高清)、useCORS(跨域图片)
  4. 完整代码可直接复用,适配绝大多数业务场景

你好:我的2025

上一篇:耗子尾汁