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. 网络图片/后台返回图片不显示?
解决方案:
-
配置 useCORS: true
-
图片标签添加 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');
// 调用上传接口...
七、适用场景
-
后台系统报表导出截图
-
活动页面生成分享海报
-
表单/页面状态保存
-
富文本内容导出图片
总结
-
Vue3 截图首选 html2canvas,轻量易用、兼容性强
-
核心三步:获取DOM → 调用库生成canvas → 转图片预览/下载
-
必配参数:scale(高清)、useCORS(跨域图片)
-
完整代码可直接复用,适配绝大多数业务场景