把网页导出为图片的两种方案以及其适用场景
先列出解决方案给出结论,再开启啰嗦模式:
- html 网页通过 html2canvas 插件 ---> canvas 画布通过(toDataURL/toBlob)---> png/pdf/jpg ---> 通过a标签 download 属性下载(方案一:纯前端方案)
- 直接用 phantom.js 或 casper.js 之类的 node 插件,用 headless WebKit 去模拟打开页面,然后导出为图片/pdf,接着上传到 OSS,把对应链接返回给前端,实现下载功能。(方案二:后端方案)
结论:当 html 内容在 canvas 可绘制能力范围内的,用方案一(版本2:blob 方式),当 html 内容超过了 canvas 可绘制能力范围,用方案二。
啰嗦模式中,主要描述的是使用方案一在开发过程中遇到的一些问题以及探究引起这些问题的原因,寻找解决方案的过程。
直观地对比两种方案
方案一:只需要一个前端参与 + 引入一个第三方插件就能搞定,而且实现的链路也不长。
方案二:需要前后端协同 + 项目需另起一个 node 环境 + OSS 资源 + 多用户并发问题 不管是从实现复杂度还是成本上,都比较大。
直观对比结果:在理想情况下,显然方案一更优。
方案一在开发中遇到的问题
最直观的表现为:当html网页内容过多时,图片下载失败。
先贴一下方案一(版本1)的代码
// canvas对象为 html2canvas 插件得到的canvas对象
var base64 = canvas.toDataURL();
var link = document.createElement('a');
link.textContent = 'download image';
link.href = base64;
link.download = "mypainting.jpeg";
link.click()
图片下载失败有两个原因:
- base64 图片通过 a 标签 href+download 方式下载对图片的尺寸也有上限。
- canvas 画布绘制能力有上限。
随后我抛出了这么几个问题:
- base64 图片本地实测大于 1.6M 的为什么下载不下来?具体的下载零界点怎么界定?
- 浏览器本地下载大图(大于 1.6M,网上查到一些资料说是 2M)有什么替代方案?
- 影响 canvas 画布容积上限的因素有哪些?(画布宽?高?面积?颜色复杂程度?)上限具体是多少?
为了回答这几个问题,我写了个小 demo(代码详见本文最后): 页面上总共两个元素,一个按钮和一张 canvas 画布,点下按钮就可以把 canvas 作为图片导出。我们可以改变 canvas 的宽高或者颜色复杂度去观察 canvas 的渲染状态和图片的下载状态。
可以贴下我实验测得的结果【canvas 画布采用纯黑色渲染,下载方式是 base64(demo 中 mode 的值为 base64)】:
宽*高 canvas渲染状态 base64长度 图片可下载与否 图片尺寸
1000*32700 canvas可以被渲染 base64长度 511403 图片可下载
1000*32800 canvas不能被渲染
2400*32700 canvas可以被渲染 base64长度 1226803 图片可下载 图片尺寸1.4M
2600*32700 canvas可以被渲染 base64长度 2084390 图片可下载 图片尺寸1.6M
2700*32700 canvas可以被渲染 base64长度 2152518 图片不可下载
8200*32700 canvas可以被渲染 base64长度 6397386 图片不可下载
8300*32700 canvas不能被渲染
回答问题1:
从实验结果可以看到,当 base64 长度为 2084390 时候,图片可下载,当 base64 长度为 2152518 时候,图片下载失败。先推测一下,这个下载极限就是 base64 长度为 2 X 1024 X 1024 = 2097152。那为什么会下载失败呢?先来看下 canvas.toDataURL() MDN 文档上对 Data URLs 的定义
Data URLs,即为前缀为
data:scheme
的 URL,其允许内容创建者向文档中嵌入小文件。
在 data URLs 相关的常见问题中能看到
长度限制,虽然 Firefox 支持无限长度的
data
URLs,但是标准中并没有规定浏览器必须支持任意长度的data
URIs。比如 Opera 11浏览器限制 URLs 最长为 65535 个字符,这意外着 data URLs 最长为 65529 个字符(如果你使用纯文本 data:,而不是指定一个 MIME 类型的话,那么 65529 字符长度是编码后的长度,而不是源文件)。
所以 data URLs 当初的提出就是为了小文件,不同的浏览器对最长 data URLs 有自己的字符限制,chrome下载限度是 base64 长度 2M(而非图片大小 2M)。
另附 chrome 上的一个 issue,在2011年的时候已经有一哥们反应这个问题了,到现在这个 issue 的 status 还是 available。可以去围观一下:https://bugs.chromium.org/p/chromium/issues/detail?id=69227。没找到 chrome 官方对 data URLs 的长度 limit 申明,暂且也可认为这是一个 chrome 一个历史悠久的 bug。
回答问题2:
从问题1可以知道引发不能下载的原因就是 data URL 的长度导致图片不可下载。有两个方法来解决这个问题:
方法1:拿到 base64 编码后,把它发给后端,让后端转成 www.xxx.com/a.jpg 类似的 oss 网络图片。该方法可行,但传过长的 base64 编码需要后台去改一些一个请求大小上限之类的配置,速度体验也非常的慢,不建议用此方法。
方法2:用 canvas toBlob() 方式把 canvas 转化成 blob对象,再用 url = URL.createObjectURL(blob); 为 blob 对象生成一个指向 blob 图片对象 URL,url 的长度一般就40多(类似 blob:null/580f6db8-8a79-43c1-baef-f07e84484492)。实测只要是 canvas 能渲染出来的,通过这种方式都能下载成功。(实测用 canvas 画了幅极其复杂的图,就是文章开头的那副五颜六色怪图,通过此方法能导出成功,图片大小为 196M。)
方案一(版本2)代码:
canvas.toBlob(function(blob) {
url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.textContent = 'download image';
link.href = url;
link.download = "mypainting.jpeg";
link.click()
// no longer need to read the blob so it's revoked
URL.revokeObjectURL(url);
});
回答问题3:
canvas 画布容积会有一个上限,和宽,高,颜色复杂程度,电脑显卡性能有关,
不相关:
- 和面积(宽高)不太相关。因为例子中 820032700 画布可以被渲染,而1000*32800却不可被渲染。
- 此容积也无法用导出图片的体积来衡量,通过此方法可以导出大小为196M的图片,已经是非常大了依旧可以导出,但在全黑模式下增加canvas宽高,导出的极限尺寸在4.8M。
因为 canvas 画布容积和画面的颜色复杂程度,电脑显卡性能相关,所以上面测出的宽高并没有代表性。从我的角度,暂时找不到用具体的数值来量化。(如果你有更好的想法,非常欢迎评论交流)。
总之,在方案一图片下载失败的两个原因中,浏览器base64图片下载上限问题可以通过 blob 方式(方案一版本2)解决,但 canvas 画布绘制能力上限问题没法通过前端来解决,需采用方案二。
方案二我相信这是图片下载的一个终极实现(虽然实现是麻烦了点儿),没有任何限制,之前做过类似的项目,用 casperjs 的 capture() 函数去做这个事情,几百M的pdf导出问题都不大。在现在这个项目中,因为后台是java,所以会采用java的方案去做这个事情。
所以回到开头的那个结论:当 html 内容在 canvas 可绘制能力范围内的,用方案一(版本2:blob 方式),当 html 内容超过了 canvas 可绘制能力范围,用方案二。
最后附上 demo 代码(其中 mode、canvasWidth、canvasHeight、drawTimes 可自定义调节)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas test</title>
</head>
<body>
<button id="btn">下载 canvas</button>
<canvas id="canvas"></canvas>
</body>
<script type="text/javascript">
/**
* canvas全黑的状况下,mode = 'base64'
* 宽*高 canvas渲染状态 base64长度 图片可下载与否 图片尺寸
* 1000*32700 canvas可以被渲染 base64长度 511403 图片可下载
* 1000*32800 canvas不能被渲染
* 2400*32700 canvas可以被渲染 base64长度 1226803 图片可下载 图片尺寸1.4M
* 2600*32700 canvas可以被渲染 base64长度 2084390 图片可下载 图片尺寸1.6M
* 2700*32700 canvas可以被渲染 base64长度 2152518 图片不可下载
* 8200*32700 canvas可以被渲染 base64长度 6397386 图片不可下载
* 8300*32700 canvas不能被渲染
*
* 结论
* 1.canvas 渲染画布有一个上限。相关因素:宽,高(但与宽*高不呈现正相关关系),画面颜色复杂程度
* 2.相同尺寸的画布,增加颜色会增加图片的尺寸。==》所以上面测试出来的宽高数据没代表性。
* 3.base64模式下,可支持下载的零界点 在 2*1024*1024 = 2097152 左右
*/
/**
* 可选值 base64, blob
* @type {String}
*/
// var mode = 'base64';
var mode = 'blob';
var canvasWidth = 2400
var canvasHeight = 32700;
/**
* 更改drawTimes 可以增加画面颜色复杂度
* 可选值 0 ~ 10020000
*/
var drawTimes = 0;
// var drawTimes = 10020000;
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
/**
* [根据drawTimes随机化一些色块,增加canvas颜色复杂度]
*/
for(var i =0;i<drawTimes;i++){
var colors = ['red','yellow','grey','green','blue','white']
ctx.fillStyle = colors[i%6];
ctx.fillRect(i/drawTimes*canvasWidth, Math.random()*canvasHeight, 10, 10);
}
if (mode === 'base64') {
document.getElementById("btn").addEventListener("click", function(event) {
var base64 = canvas.toDataURL();
console.log(base64.length)
var link = document.createElement('a');
link.textContent = 'download image';
link.href = base64;
link.download = "mypainting.jpeg";
link.click()
}, false);
} else if (mode === 'blob') {
document.getElementById("btn").addEventListener("click", function(event) {
canvas.toBlob(function(blob) {
url = URL.createObjectURL(blob);
console.log(url)
console.log(url.length)
var link = document.createElement('a');
link.textContent = 'download image';
link.href = url;
link.download = "mypainting.jpeg";
link.click()
// no longer need to read the blob so it's revoked
URL.revokeObjectURL(url);
});
}, false);
}
</script>
</html>
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论