业务里经常要生成分享海报:PC 分享图、H5 优惠券、年度账单。需求不同,方案也不同——第三方库、自己用 Canvas 画、丢给接口。各自都踩过跨域、字体、清晰度。
下面按 html2canvas、dom-to-image、前端 Canvas、接口出图整理兼容性和取舍。过程中的坑(跨域、文字、伪类)尽量写全,方便对照。
一、背景与业务
PC 侧主要是分享海报;H5 还有分享优惠券、年度账单、分享海报。实现时兼容性、还原度、性能要一起权衡。
二、html2canvas
版本 1.0.0-rc7。仓库 https://github.com/niklasvh/h… star 约 22k(统计于 2021-01-06)。首页 demo:https://html2canvas.hertzen.com/

兼容性抽测
-
安卓:6.0 koobee ✅;5.0.2 vivo Y51A ✅
-
-
-
iOS 微信内置:7.0.20 ✅;7.0.1 ✅
官方对浏览器的描述(需 Promise polyfill):
Browser compatibility
The library should work fine on the following browsers (with Promise polyfill):
Firefox 3.5+
Google Chrome
Opera 12+
IE9+
Safari 6+
As each CSS property needs to be manually built to be supported, there are a number of properties that are not yet supported.
优点:兼容面更宽。缺点:不少 CSS 转不过去——伪类、border dash、text-shadow 都不支持。还原度要和设计对齐,或改成切图。特性说明:https://html2canvas.hertzen.c…
rc5 在 iOS 上出现过生成不了图,退回 rc4 就好。https://github.com/niklasvh/h… issue 里有 iOS 某版本微信失败的反馈,当时没复现。
三、dom-to-image
版本 2.6.0。https://github.com/tsayen/dom… star 约 6.9k。
优点是元素齐、还原度高。缺点是不兼容 Safari,建议只在 Chrome 用,而且已经不再维护。
作者明确说不支持 Safari,原因是 foreignObject 的安全限制:
iOS 14.2 第一次生成常常丢图,第二次或第三次才正常。权宜之计是连调 2~3 次取最后一次;issue 里仍有机型不行。低端安卓会失败,多半是文字问题(旧笔记,不完全确定):

用在 PC 或 Android 时,清晰度不必改源码,作者提供了参数:
清晰度问题:看到很多人说要去改源码,其实不用的,作者是提供了参数了
// 通过 scale 放大设备的 DPR 倍数
let elm = document.getElementById('my-node')
const scale = window.devicePixelRatio
this.loader.open()
await this.$nextTick()
domtoimage
.toBlob(elm, {
quantity: 1,
width: elm.offsetWidth * scale, // 画布放大
height: elm.offsetHeight * scale,
style: {
transform: `scale(${scale})`, // 元素放大
transformOrigin: '0 0',
},
})
.then(this.upload) // 处理结果
.catch(function (error) {
this.loader.close()
console.error('oops, something went wrong!', error)
})
四、前端 Canvas 手绘
画布与图片
let canvasBox = document.createElement('canvas')
let ctx = canvasBox.getContext(
'2d'
) as CanvasRenderingContext2D
let bgImag = ''
ctx.drawImage(bgImg, 0, 0)
图片跨域:
解决办法 :
1)使用 crossOrigin 属性。
解决 canvas 图片 getImageData、toDataURL 跨域问题
2)图片本身也需要允许跨域
3)设置 useCORS:true,原理相同,但使用以上跨域方法,若同时设置为 allowTaint:true ,仍然会认为画布已被污染而不可用;
文字
换行的核心:按每行最大宽度把字符串拆开再画。参考:https://www.zhangxinxu.com/wordpress/2018/02/canvas-text-break-line-letter-spacing-vertical/
核心:计算所有文字,根据每行可显示的最大宽度,来拆分成每行渲染
字体:只用默认或少量定制字体时,可用 Fontmin 裁切后 @font-face。涉及版权,必须确认你们有该字体授权。
@font-face {
font-family: "DIN Condensed";
// prettier-ignore
src: url("~@/assets/fonts/DIN-Condensed-Bold.woff") format("woff"), url("~@/assets/fonts/DIN-Condensed-Bold.ttf") format("ttf");
}
@font-face {
font-family: "PingFang SC Bold";
// prettier-ignore
src: url("~@/assets/fonts/PingFang-SC-Bold.ttf") format("ttf"), url("~@/assets/fonts/PingFang-SC-Bold.woff") format("woff");
}
ctx.fillText('20px PingFang SC Bold', x, y)
ctx.fillText('20px', x, y)
必须完整某字体、且文案动态变化时,让接口出 SVG,再画进 Canvas。注意直接 DOM 转 SVG 在安卓上也有失败:
svg to img
直接 domtoSvg 也有安卓失败的问题
let svg = 接口获取svg(参考年度账单)
let svgBase64: string =
'data:image/svg+xml;base64,' +
window.btoa(
unescape(encodeURIComponent(svg))
)
await this.loadImg(svgBase64).then(
img => {
ctxC.drawImage(
img,
~~(20 * this.scale),
~~(186 * this.scale)
)
}
)
手机端比例
尽量用整数,按 devicePixelRatio 和设计稿宽度算 scale,避免糊:
// 尽量使用整数
// devicePixelRatio
protected imgQuality = window.devicePixelRatio
// 和设计稿的比例 设计稿宽度是640
get scale() {
return (document.body.clientWidth * this.imgQuality) / 640
}
// 画布宽高,避免生成的图片模糊情况出现
canvasBox.width = Math.ceil(
document.body.clientWidth * this.imgQuality
)
canvasBox.height = Math.ceil(980 * this.scale)
// 二维码的宽度
get qrCodeSize() {
return ~~(155 * this.scale) // rpx * scale
}
线、圆、圆角矩形
// 画布和设计稿750的比例
scale = (document.body.clientWidth * this.imgQuality) / 750
// 线:
/**
* 画线
*/
export function drawLine(
ctx: CanvasRenderingContext2D,
data: {
x: number
y: number
width: number
height: number
strokeStyle: string
},
scale: number
) {
let x = data.x * scale
let y = data.y * scale
let dx = (data.x + data.width) * scale
let dy = data.y * scale
// 开始一个新的绘制路径
ctx.beginPath()
// 定义直线的起点坐标为(10,10)
ctx.moveTo(x, y)
// 定义直线的终点坐标为(50,10)
ctx.lineTo(dx, dy)
// 颜色
ctx.strokeStyle = data.strokeStyle
// 沿着坐标点顺序的路径绘制直线
ctx.stroke()
// 关闭当前的绘制路径
ctx.closePath()
}
// 圆:
/**
* 画圆
*/
export function drawCircle(
ctx: CanvasRenderingContext2D,
data: {
x: number // 圆心
y: number
size: number // 半径
fillStyle: string
},
scale: number
) {
ctx.save()
ctx.beginPath()
ctx.arc(
~~(data.x * scale),
~~(data.y * scale),
~~(data.size * scale),
0,
~~(data.size * scale * Math.PI)
)
ctx.fillStyle = data.fillStyle
ctx.fill()
ctx.restore()
}
// 矩形:
/**
* 画圆角矩形
*/
export function drawRoundRect(
ctx: CanvasRenderingContext2D,
data: {
x: number
y: number
width: number
height: number
radius: number
fillStyle: string
},
scale: number
) {
let width = ~~(data.width * scale)
let height = ~~(data.height * scale)
let radius = ~~(data.radius * scale)
let fillStyle = data.fillStyle
ctx.save()
ctx.translate(~~(data.x * scale), ~~(data.y * scale))
ctx.beginPath()
// 从右下角顺时针绘制,弧度从0到1/2PI
ctx.arc(width - radius, height - radius, radius, 0, Math.PI / 2)
// 矩形下边线
ctx.lineTo(radius, height)
// 左下角圆弧,弧度从1/2PI到PI
ctx.arc(radius, height - radius, radius, Math.PI / 2, Math.PI)
// 矩形左边线
ctx.lineTo(0, radius)
// 左上角圆弧,弧度从PI到3/2PI
ctx.arc(radius, radius, radius, Math.PI, (Math.PI * 3) / 2)
// 上边线
ctx.lineTo(width - radius, 0)
// 右上角圆弧
ctx.arc(width - radius, radius, radius, (Math.PI * 3) / 2, Math.PI * 2)
// 右边线
ctx.lineTo(width, height - radius)
ctx.closePath()
ctx.fillStyle = fillStyle
ctx.fill()
ctx.restore()
}
画整圆时 arc 的扫角应是 2 * Math.PI。上面 drawCircle 写成了 size * scale * Math.PI,半径一大圆弧就不闭合,需要对齐设计稿再改。
导出与保存
canvasBox.toBlob(async data => {
if (data) {
//上传到7niu
this.url = await uploadFile(data)
}
})
PC 下载:
import { saveAs } from 'file-saver'
saveAs(blob, filename)
H5:
长按保存
包壳的 H5 是可以实现直接下载的,但为了交互一致性,还是用了长按
五、接口生成与跨域
把元素和位置传给后端,接口直接回图。优点是前端几乎不用管兼容;缺点是字数、字体种类一多,服务器压力就大,元素越多越慢。后端同样要算换行高度(同一字体中英文宽度不同)和后续元素相对位置。
跨域图导不下来时,把下面这段看完。来源:https://segmentfault.com/q/10…

六、怎么选
无特殊情况,PC 用 dom-to-image 即可。H5 这边走过的路:
-
-
-
前端 Canvas(部分元素由接口画成图或 SVG 再贴上,兼容还行,但仍慢)
-
再回到 html2canvas(测过若干环境,项目当时未上线,还需观察)
当时看起来 html2canvas 更稳,但有些效果画不出来,要在还原度和性能之间取舍。不少 App 出海报更快,可能是原生能力,H5 本身有限制。
| 方案 |
适合 |
优点 |
短板 |
| html2canvas |
H5 多端 |
兼容面相对宽 |
伪类 / dash / text-shadow 不支持 |
| dom-to-image |
PC Chrome |
还原度高 |
不支持 Safari,停更;iOS 14.2 易丢图 |
| Canvas 手绘 |
版式固定、要控清晰度 |
跨域、字体、比例都可自己管 |
开发量大,文字换行和版权烦 |
| 接口出图 |
前端环境太乱 |
前端无兼容包袱 |
字多、元素多就慢,服务器压力大 |
七、参考
-
-
-
-
-
-
-
一句话总结:海报没有银弹:PC 优先 dom-to-image,H5 更常落到 html2canvas 或 Canvas;跨域、字体、DPR 才是真正耗时的地方。