一、前言
层叠上下文(Stacking Context) : CSS 中的一个重要概念,决定了元素在 z 轴上的层叠顺序。html2canvas 需要正确解析这些信息以确保截图的层级关系正确。
Skyline 渲染引擎 :微信小程序的新一代渲染引擎,基于自研的渲染架构,相比传统 WebView 渲染具有更好的性能和更丰富的能力。
在 Web 开发中,分享海报是比较常见的需求,一般用于营销活动分享、用户 UGC 内容输出、社交传播等场景,而业界在具体技术实现上存在多种方案。本文将结合作者的开发经验,介绍前端生成海报的主要技术方案(包括 Canvas 原生绘制、自研 Canvas 插件、html2canvas 类插件、微信小程序 Skyline 等)。
二、主流技术方案详解
2.1 Canvas 原生绘制
-
创建/获取 Canvas 元素
-
加载外部资源(图片/字体)并处理跨域
-
按层级绘制背景、文字、图片等元素(根据 x/y 轴定位,文本换行需手动计算)
-
调用 toDataURL()
或 convertToBlob()
导出图片
代码示例:
// 获取Canvas上下文
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// 绘制渐变背景
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, "#4a90e2");
gradient.addColorStop(1, "#2c5aa0");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制文本(带阴影效果)
ctx.fillStyle = "white";
ctx.font = "bold 48px Arial";
ctx.textAlign = "center";
ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
ctx.shadowBlur = 10;
ctx.fillText("海报标题", canvas.width / 2, 200);
// 绘制图片
const img = new Image();
img.onload = function () {
ctx.drawImage(img, 0, 0, img.width, img.height);
};
img.src = "background.jpg";
// 绘制几何图形(圆形)
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fill();
// 导出图片
const dataURL = canvas.toDataURL("image/png");
const link = document.createElement("a");
link.download = "poster.png";
link.href = dataURL;
link.click();
优缺点:
✅ 灵活性高(像素级控制)
✅ 性能稳定(避免 DOM 渲染开销)
❌ 开发效率低,复杂布局需手动计算(如多列文本、动态间距)
❌ 代码复用性不高
典型应用场景:
适用于对性能要求极高、布局相对固定的海报生成,如游戏内截图、数据可视化图表等。
2.2 自研 Canvas 插件
为了解决 Canvas 原生绘制的复杂性和代码复用问题,我们可以基于业务需求封装自研的 Canvas 插件。以 GameSDK 海报插件为例(公司内部插件,无外部链接):
-
原理:基于 Canvas 原生 API 封装,提供声明式的海报生成方案
-
关键步骤:
-
定义海报配置(尺寸、背景、图层数组)
-
解析图层配置并计算布局位置
-
按图层类型(图片/文字/二维码/矩形)依次绘制
-
导出生成的海报图片
代码示例:
// 海报插件核心类
class Poster {
constructor(options) {
const { width, height, background = "#fff", views = [] } = options;
this._canvas = document.createElement("canvas");
this._context = this._canvas.getContext("2d");
this._canvas.width = width;
this._canvas.height = height;
this._views = views;
// 绘制背景
this._context.fillStyle = background;
this._context.fillRect(0, 0, width, height);
}
// 生成海报 base64 URL
async generateDataURL(type = "image/jpeg") {
// 处理图层位置和布局
this._formatViews = this.formatPosition(this._views);
this._formatViews = this.formatFlexBox(this._formatViews);
// 按类型绘制每个图层
for (const view of this._formatViews) {
switch (view.type) {
case "image":
await this._generateImage(view);
break;
case "text":
await this._generateText(view);
break;
case "qrcode":
await this._generateQRCode(view);
break;
case "rect":
await this._generateRect(view);
break;
}
}
return this._canvas.toDataURL(type);
}
// 绘制图片图层(支持跨域和旋转)
_generateImage(view) {
return new Promise((resolve, reject) => {
const image = new Image();
const { width, height, src, rotate } = view;
const { left, top } = view;
image.crossOrigin = "Anonymous";
image.onload = () => {
if (rotate) {
// 旋转绘制
this._context.save();
this._context.translate(left + width / 2, top + height / 2);
this._context.rotate((rotate * Math.PI) / 180);
this._context.drawImage(
image,
-width / 2,
-height / 2,
width,
height
);
this._context.restore();
} else {
this._context.drawImage(image, left, top, width, height);
}
resolve();
};
image.onerror = reject;
image.src = src;
});
}
// 其他绘制方法
}
使用示例
const poster = new Poster({
width: 750,
height: 1334,
background: "#f5f5f5",
views: [
// 图片
{
type: "image",
width: 200,
height: 200,
src: "https://p1.dailygn.com/img/g-marketing-act-assets/2021_09_17_17_31_03/game-sdk-logo_s7858.png~q75.png",
},
// 二维码
{
type: "qrcode",
width: 100,
height: 100,
left: 100,
top: 100,
text: location.href,
},
// 文字
{
type: "text",
width: 200,
height: 100,
text: "我是一串文字",
color: "red",
align: "center",
left: 0,
top: 0,
},
],
});
// 生成海报
poster.generateDataURL().then((dataURL) => {
const img = document.createElement("img");
img.src = dataURL;
document.body.appendChild(img);
});
优缺点:
✅ 开发效率高(声明式配置)
✅ 易于扩展(模块化设计)
❌ 需要长期维护,以覆盖更多的使用场景
❌ 不适用于过于复杂的布局
典型应用场景:
适用于有一定复杂度但布局相对规范的海报生成,如商品分享卡片、活动宣传海报等。
2.3 html2canvas
html2canvas 是一个流行的前端开源库,在 GitHub 上已有 30k+ 的 star,它允许开发者在浏览器中直接对网页进行"截图",以生成海报。
-
原理:通过读取 DOM 和应用于元素的不同样式,在 Canvas 上绘制页面内容
-
关键步骤:
-
资源预处理:处理图片跨域、字体加载等异步资源,确保渲染时可用
-
DOM 克隆:使用 DocumentCloner
在 iframe 中克隆目标元素,避免影响原页面
-
样式解析:通过 parseTree
深度优先遍历 DOM,为每个节点创建 ElementContainer
对象
-
层叠上下文构建:解析 z-index
、position
等属性,生成正确的绘制顺序
-
Canvas 渲染:使用 CanvasRenderer
将解析后的元素树绘制到 Canvas 上
代码示例:
// html2canvas 实现流程(简版)
const html2canvas = async (element, options = {}) => {
// 步骤 1: 初始化配置和上下文
// - 合并用户选项和默认值,创建 resourceOptions, windowOptions 等。
// - 创建 Context 对象,用于管理日志、缓存和资源请求。
const context = new Context(contextOptions, windowBounds);
// 步骤 2: DOM 克隆
// - 创建 DocumentCloner 实例。
// - 调用 toIFrame 方法,在一个隐藏的 iframe 中深度克隆目标 DOM。
// - 这个过程会等待 iframe 内的资源(如字体、图片)加载完成。
const documentCloner = new DocumentCloner(context, element, cloneOptions);
const container = await documentCloner.toIFrame(ownerDocument, windowBounds); // toIFrame 返回的是 iframe 容器
const clonedElement = documentCloner.clonedReferenceElement; // 克隆的目标元素在 cloner 的属性中
// 步骤 3: DOM 解析和样式计算
// - 使用 parseTree 递归遍历克隆后的 DOM 树。
// - 为每个元素创建 ElementContainer,并计算和存储其样式(getComputedStyle)。
const root = parseTree(context, clonedElement);
// 步骤 4: Canvas 渲染
// - 创建 CanvasRenderer 实例,传入渲染配置。
// - 调用 renderer.render 方法,它内部会先构建层叠上下文,然后按正确顺序绘制。
const renderer = new CanvasRenderer(context, renderOptions);
const canvas = await renderer.render(root); // render 方法接收 ElementContainer 树的根节点
// 步骤 5: 清理
// - 渲染完成后,移除用于克隆的 iframe 容器。
DocumentCloner.destroy(container);
return canvas;
};
使用示例:
// 基础用法
html2canvas(document.body).then(function (canvas) {
document.body.appendChild(canvas);
});
// 带配置选项的用法
html2canvas(element, {
allowTaint: false,
useCORS: true,
scale: 2, // 提高清晰度
width: 800,
height: 600,
backgroundColor: "#ffffff",
logging: false,
}).then((canvas) => {
// 转换为图片并下载
const link = document.createElement("a");
link.download = "screenshot.png";
link.href = canvas.toDataURL();
link.click();
});
// 截取特定区域
const targetElement = document.querySelector(".poster-container");
html2canvas(targetElement, {
x: 0,
y: 0,
width: targetElement.offsetWidth,
height: targetElement.offsetHeight,
scrollX: 0,
scrollY: 0,
}).then((canvas) => {
const dataURL = canvas.toDataURL("image/jpeg", 0.8);
// 处理生成的图片
});
// 处理跨域图片
html2canvas(element, {
useCORS: true,
proxy: "https://proxy-server.com/proxy",
allowTaint: false,
}).then((canvas) => {
// 处理结果
});
优缺点:
✅ 使用简单,少量配置即可
✅ 支持复杂 DOM 结构和 CSS 样式
❌ 依赖浏览器兼容性,不同浏览器渲染效果可能存在差异
❌ 存在多个已知问题和坑点,可能需要修改源码或寻找替代方案(可参考:html2canvas - 项目中遇到的那些坑点汇总(更新中...) - xing.org1^ - 博客园)
典型应用场景:
适用于需要将复杂 HTML 页面转换为图片的场景,如用户 UGC 内容分享、复杂布局的营销海报等。
2.4 wxml2canvas
wxml2canvas 是一款用于微信小程序的分享图生成插件,设计思想与 html2canvas 类似。不过由于微信小程序 API 的限制,以及插件本身功能相对简单,在能力上与 html2canvas 有一定差距。
-
原理:通过 SelectorQuery API 获取 WXML 元素样式信息,在 Canvas 上绘制页面内容
-
关键步骤:
-
图片预加载:预加载所有图片资源(网络图片下载、base64 转本地文件)
-
绘制任务分发:按类型(wxml、text、image、rect 等)分发绘制任务
-
WXML 节点解析:使用 wx.createSelectorQuery()
获取节点样式、位置和数据属性
-
坐标转换适配:处理相对定位、缩放比例和边界限制
-
Canvas 导出:延时保存,通过 wx.canvasToTempFilePath
导出图片
代码示例:
// wxml2canvas 实现流程(简版)
class Wxml2Canvas {
constructor(options = {}) {
// 1. 初始化配置
this.device = wx.getSystemInfoSync();
this.zoom = options.zoom || this.device.windowWidth / 375; // 缩放比例
this.element = options.element; // Canvas ID
this.width = options.width * this.zoom;
this.height = options.height * this.zoom;
this.background = options.background || '#ffffff';
this._init();
}
// 2. 主绘制入口
draw(data = {}, that) {
this.data = data;
// 2.1. 预加载所有图片资源
this._preloadImage(data.list).then(() => {
this._draw(); // 开始绘制
}).catch(this.errorHandler);
}
// 3. 绘制任务分发
_draw() {
let list = this.data.list || [];
let all = [];
// 3.1. 遍历绘制任务列表
list.forEach((item, i) => {
all[i] = new Promise((resolve, reject) => {
// 3.2. 根据类型分发绘制任务
switch (item.type) {
case 'wxml':
this._drawWxml(item, item.style, resolve, reject);
break;
case 'text':
this._drawText(item, item.style, resolve, reject);
break;
case 'image':
case 'radius-image':
this._drawRect(item, item.style, resolve, reject, 'image');
break;
// ... 其他类型
}
});
});
// 3.3. 等待所有绘制完成后导出
Promise.all(all).then(() => {
this._saveCanvasToImage();
});
}
// 4. WXML节点绘制(核心方法)
_drawWxml(item, style, resolve, reject) {
// 4.1. 获取WXML节点信息
this._getWxml(item, style).then((results) => {
// 4.2. 按top值排序,实现分层
let sorted = this._sortListByTop(results[0]);
let all = [];
// 4.3. 分别处理块级和行内元素
all = this._drawWxmlBlock(item, sorted, all, progress, results[1]);
all = this._drawWxmlInline(item, sorted, all, progress, results[1]);
Promise.all(all).then(resolve).catch(reject);
});
}
// 5. 获取WXML节点信息
_getWxml(item, style) {
const query = wx.createSelectorQuery();
// 5.1. 获取所有目标元素
const p1 = new Promise((resolve, reject) => {
query.selectAll(item.class).fields({
dataset: true,
size: true,
rect: true,
computedStyle: ['width', 'height', 'fontSize', 'color',
'backgroundColor', 'backgroundImage', ...]
}, (res) => {
// 5.2. 格式化图片信息
const formated = this._formatImage(res);
// 5.3. 预加载图片
this._preloadImage(formated.list).then(() => {
resolve(formated.res);
}).catch(reject);
}).exec();
});
// 5.4. 获取限制区域信息
const p2 = new Promise((resolve) => {
if (!item.limit) {
resolve({ top: 0, width: this.width / this.zoom });
return;
}
query.select(item.limit).fields({
dataset: true, size: true, rect: true
}, resolve).exec();
});
return Promise.all([p1, p2]);
}
// 6. 坐标转换适配
_transferWxmlStyle(sub, item, limitLeft, limitTop) {
// 6.1. 处理相对定位
sub.left = this._parseNumber(sub.left) - limitLeft + (item.x || 0) * this.zoom;
sub.top = this._parseNumber(sub.top) - limitTop + (item.y || 0) * this.zoom;
// 6.2. 处理padding等样式
// ...
return sub;
}
// 7. Canvas导出
_saveCanvasToImage() {
// 7.1. 延时保存(等待绘制完成,避免样式错乱)
setTimeout(() => {
wx.canvasToTempFilePath({
canvasId: this.element,
success: (res) => {
this.finishDraw(res.tempFilePath);
},
fail: this.errorHandler
});
}, this.device.system.indexOf('iOS') === -1 ? 300 : 100);
}
}
使用示例:
// WXML结构
// <view class="poster-container">
// <view class="poster-title draw_canvas"
// data-type="text"
// data-text="海报标题">
// 海报标题
// </view>
// <image class="poster-avatar draw_canvas"
// data-type="image"
// data-url="{{avatarUrl}}"
// src="{{avatarUrl}}" />
// </view>
// <canvas canvas-id="posterCanvas" class="poster-canvas"></canvas>
Page({
generatePoster() {
const drawer = new Wxml2Canvas({
width: 375,
height: 667,
element: "posterCanvas",
background: "#ffffff",
destZoom: 3,
finish: (url) => {
this.setData({ posterUrl: url });
wx.previewImage({ urls: [url] });
},
error: (err) => {
console.error("生成海报失败:", err);
},
});
// 定义绘制任务
const data = {
list: [
{
type: "wxml",
class: ".draw_canvas",
limit: ".poster-container",
x: 0,
y: 0,
},
],
};
drawer.draw(data);
}
});
优缺点:
✅ 使用简单(与原生 Canvas 绘制相比)
✅ 支持中等复杂度的图文布局
❌ 支持绘制类型有限,不支持自定义组件、Canvas 等类型,且对渐变(Gradients)、阴影(Shadows)、变形(Transforms)、滤镜(Filter)等 CSS 高级特性支持不佳
❌ 该插件已多年无人维护,如踩坑可能需要修改源码或寻找替代方案(如 wxml2canvas-2d 插件)
典型应用场景:
适用于微信小程序中相对简单的海报生成需求,如商品分享卡片、用户成就展示等。
2.5 Skyline
Skyline 是微信小程序推出的新一代渲染引擎,配合 Snapshot 组件 可以实现高性能的截图功能。与 wxml2canvas 插件相比,Skyline + Snapshot 不涉及 Canvas 绘制,海报开发效率更高。
-
原理:基于 Skyline 渲染引擎的原生截图能力,直接将渲染结果转换为图片
-
关键步骤:
-
配置 Skyline 渲染环境
-
使用 Snapshot 组件包裹需要截图的内容
-
调用 takeSnapshot
API 生成图片
-
保存或分享生成的图片
使用示例(以 uniapp 为例):
- 配置 Skyline 环境
在 manifest.json
的 mp-weixin
中添加 lazyCodeLoading 及 rendererOptions 配置:
"mp-weixin": {
"lazyCodeLoading" : "requiredComponents",
"rendererOptions" : { "skyline" : { "defaultDisplayBlock" : true } }
}
在 pages.json
的 pages->style
中添加 renderer 及 componentFramework 配置:
{
"pages" : [
{
"path" : "pages/demo/index",
"style" : {
"renderer" : "skyline",
"componentFramework" : "glass-easel"
}
}
]
}
- 页面实现
<template>
<snapshot id="poster" class="poster-container">
<view class="poster-content">
<view class="header">
<image class="avatar" :src="userAvatar" />
<view class="user-info">
<text class="username">{{ username }}</text>
<text class="activity">发起学习活动</text>
</view>
</view>
<view class="content">
<view class="progress-section">
<text class="progress-label">学习进度</text>
<text class="progress-value">{{ progress }}%</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
</view>
</view>
<view class="footer">
<image class="qrcode" :src="qrcodeUrl" />
<text class="qrcode-tip">长按识别小程序码</text>
</view>
</view>
</snapshot>
<button @click="generatePoster">生成海报</button>
</template>
- 截图逻辑
const generatePoster = () => {
uni
.createSelectorQuery()
.select("#poster")
.node()
.exec((res) => {
const node = res[0].node;
node.takeSnapshot({
type: "arraybuffer",
format: "png",
success: (snapshotRes) => {
// 保存到本地文件系统
const filePath = `${wx.env.USER_DATA_PATH}/poster.png`;
const fs = uni.getFileSystemManager();
fs.writeFileSync(filePath, snapshotRes.data, "binary");
// 保存到相册
uni.saveImageToPhotosAlbum({
filePath,
success: () => {
uni.showToast({ title: "保存成功" });
},
fail: (err) => {
console.error("保存失败:", err);
},
});
},
fail: (err) => {
console.error("截图失败:", err);
},
});
});
};
优缺点:
✅ 使用简单,少量配置即可
✅ 支持复杂布局,样式还原度高
❌ Skyline 环境下存在较多适配问题,如不支持原生导航栏、absolute
和 fixed
定位失效等(可参考:Skyline 渲染引擎常见问题),建议在单独的海报页面开启 Skyline
❌ 需要设置最低基础库版本为 3.0.1(Snapshot 组件要求),可能导致部分用户需要升级微信
❌ Skyline 暂不支持鸿蒙系统,需要走 WebView 渲染
典型应用场景:
适用于微信小程序中复杂布局的海报生成,如用户游戏战绩、复杂布局的营销海报等。
三、技术选型总结与建议
在前端海报生成的技术选型中,不同方案各有优劣,需要根据具体的业务场景、开发效率要求和技术栈来选择。一般来说,微信小程序中 Skyline 方案的开发效率较高,其次是 wxml2canvas 类插件;H5 场景中,复杂布局下 html2canvas 开发效率较高,简单布局下自研 Canvas 插件开发效率较高,原生 Canvas 绘制最为灵活,但开发效率相对较低。
3.1 技术方案对比
以下是各技术方案的详细对比(注:表格为粗略预估数据,非实际评测结果,具体效果需结合实际场景验证):
技术方案 |
适用场景 |
开发效率 |
维护难度 |
性能表现 |
布局复杂度支持 |
兼容性 |
Canvas 原生绘制 |
H5/小程序 |
⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
自研 Canvas 插件 |
H5/小程序 |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐ |
html2canvas |
H5 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
wxml2canvas |
小程序 |
⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐ |
Skyline + Snapshot |
小程序 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐ |
3.2 选型建议
-
微信小程序场景:优先选择 Skyline + Snapshot 方案,开发效率高且样式还原度好;如需兼容低版本微信或鸿蒙系统,可考虑 wxml2canvas 作为降级方案
-
H5 场景:复杂布局推荐 html2canvas,简单布局可选择自研 Canvas 插件或原生 Canvas 绘制
-
跨平台场景:建议封装自研 Canvas 插件,统一 H5 和小程序的实现逻辑
-
高性能要求:原生 Canvas 绘制性能最佳,但开发成本较高
以上技术方案已在线上环境验证过,但在不同的需求背景及设备要求下,效果可能会有所差异。建议根据具体场景进行充分的评估和测试。