最近丁香医生的产品大佬又双叒叕搞事情,想要在 H5 中作一个医生邀请提问的功能,能够将医生的二维码名片分享出去,支持移动、PC 和微信。以前的图片是由后端生成的,而且会缓存三天,致使信息更新不及时。由前端来作就能避免这些问题。css
我一听,这好说,不就是个保存图片的功能么,简单看看需求:html
分析下来就两点前端
由于以前已经据说过有个库能将 HTML 转为 canvas,而后又据说 canvas 能转为图片,而后又据说图片能下载....(开发基本靠据说(搜索),这是废话)node
那个人基本方案就是:
html -> canvas -> image -> a[download]git
既然方案定下来了,下面就开始踩坑表演了,👏程序员
官方是这样介绍的:github
js将遍历加载页面的 DOM 节点,收集全部元素的信息,而后用这些信息来呈现页面。换句话说,实际上这个库并非真的对页面进行截图,而是基于从 DOM 读取的元素及属性来一点点的绘制 canvas。 所以,它只能正确地呈现它理解的元素和属性,这意味着有许多 CSS 属性不起做用。chrome
// v0.4.1
html2canvas(element, {
onrendered: function(canvas) {
// 如今你已经拿到了canvas DOM元素
}
});
// v0.5.0
html2canvas(element, options).then(canvas => {
// 如今你已经拿到了canvas DOM元素
});复制代码
因此基本能够猜到整个工做流程应该是:canvas
目前官方提供的版本有不少,正式版本是v0.4.1 - 7.9.2013
,最新版本是v0.5.0-beta4
,那对于咱们开发来讲若是不是玩新特性什么的通常仍是会选择正式版,结果第一个坑就掉进去爬了半天。后端
由于开发的时候是用 chrome 模拟器生成 canvas 后没有发现有模糊的地方,可是用 PC 代理手机请求开发资源时,发现画面的模糊感很是明显。
如图:
容易想到,多是移动端像素密度计算的问题。
设备像素比 (简称 dpr) 定义了物理像素和设备独立像素的对应关系,它的值能够按以下的公式的获得:
设备像素比 = 物理像素 / 设备独立像素 // 在某一方向上,x方向或者y方向
知道了这个也没用,由于文档中根本没有给出可以配置像素比的地方。。
然而经过研究发现,官方文档其实仍是 0.4.1
的,从 0.5.0
版本开始,其实已经偷偷摸摸支持自定义 canvas 做为配置项传入了,它会根据咱们传入的 canvas 为基础开始绘制。因此咱们在调用 html2canvas 的时候,能够先建立好一个尺寸合适的 canvas,再传进去。
话很少说,首先将库升级到 0.5.0
,而后:
/** * 根据window.devicePixelRatio获取像素比 */
function DPR() {
if (window.devicePixelRatio && window.devicePixelRatio > 1) {
return window.devicePixelRatio;
}
return 1;
}
/** * 将传入值转为整数 */
function parseValue(value) {
return parseInt(value, 10);
};
/** * 绘制canvas */
async function drawCanvas(selector) {
// 获取想要转换的 DOM 节点
const dom = document.querySelector(selector);
const box = window.getComputedStyle(dom);
// DOM 节点计算后宽高
const width = parseValue(box.width);
const height = parseValue(box.height);
// 获取像素比
const scaleBy = DPR();
// 建立自定义 canvas 元素
const canvas = document.createElement('canvas');
// 设定 canvas 元素属性宽高为 DOM 节点宽高 * 像素比
canvas.width = width * scaleBy;
canvas.height = height * scaleBy;
// 设定 canvas css宽高为 DOM 节点宽高
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
// 获取画笔
const context = canvas.getContext('2d');
// 将全部绘制内容放大像素比倍
context.scale(scaleBy, scaleBy);
// 将自定义 canvas 做为配置项传入,开始绘制
return await html2canvas(dom, {canvas});
}复制代码
以上代码先获取设备像素比,并根据比例建立尺寸更大的 canvas。如二倍屏就是二倍,三倍屏就是三倍,八倍镜就是八倍···
手机端截图,和html展现效果一致,基本看不出来差异。
PC端截图:
可能有多种缘由,排查后发现是由于 canvas 内的图片跨域了 这里有解释
总而言之,就是:能够在 canvas 中绘制跨域的图片,但此时的 canvas 处于被 「污染」 的状态,而污染状态的 canvas 使用 toDataUrl() 等 API 是会出现问题的。
因此,如今咱们须要作两件事:
crossOrigin
属性,值为 anonymous
第一件事好办,由于 html2canvas 自己支持配置useCORS: true
;
可是第二件事就要分状况。当图片放在本身服务器时,仅仅是让后端小哥改个配置的事儿。可是当图片放在 CDN 上时······嗯, 为了更快的响应,不少 CDN 会缓存图片的返回值,而缓存的值是不带 CORS 头的。由于没有 CORS 头,因此 js 请求会被拦截。这个时候,咱们可使用服务器转发,在转发时带上 CORS 头。(前端撸一个 node 中间层来进行服务器转发是个很好的方案,这个下回再单独说)
OK。使用以上方案,咱们测试一下。
PC 端打开,完美。
微信端,咦,仍是不行。
后期发现,使用 html2canvas 0.5.0
版本是没有问题的,可是开发时使用 0.4.1
绘制 canvas 仍是会致使图片丢失。猜想是由于 html2canvas 在预载图片和绘制图片时多了什么不可描述的东西。为了解决这个问题,咱们使用了一个很是暴力的解决方案:用 js 去获取图片,得到其 base64,放回 img 的 src 中再进行绘制。
/** * 图片转base64格式 */
img2base64(url, crossOrigin) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
const c = document.createElement('canvas');
c.width = img.naturalWidth;
c.height = img.naturalHeight;
const cxt = c.getContext('2d');
cxt.drawImage(img, 0, 0);
// 获得图片的base64编码数据
resolve(c.toDataURL('image/png'));
};
crossOrigin && img.setAttribute('crossOrigin', crossOrigin);
img.src = url;
});
}复制代码
这个坑总算是磕磕碰碰趟过去了。
border-radius
必须 ≤ 短边长度的一半,而且是具体数值,不然可能会出现奇妙的效果。
另外使用伪元素实现 0.5px 边框也可能会出现奇妙效果,建议直接使用 border
属性
0.4.1
版本中须要作圆形图片只能置为背景图,img 不支持绘制 border-radius,0.5.0
中则无此限制
前面说的, html2canvas 并不支持全部 css 属性。使用 border-style: dashed/dotted
无效,仍是大实线。切图在 PC 端有效,可是在微信中,尝试使用切图渲染虚线时有可能还会报 SecurityError, The operation is insecure.
错误,致使转 base64 失败
理想:
/** * 在本地进行文件保存 * @param {String} data 要保存到本地的图片数据 * @param {String} filename 文件名 */
saveFile(data, filename) {
const save_link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
save_link.href = data;
save_link.download = filename;
const event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
save_link.dispatchEvent(event);
}复制代码
现实:
PC端: 完美。微信大佬:很差意思,你说什么?我听不见?!
好嘛,微信中根本没有任何反应。查看 微信sdk 后发现:
downloadImage
仅支持 uploadImage
接口上传的图片。uploadImage
接口仅支持 chooseImage
接口相册选择的图片。chooseImage
接口是从本地相册选择图片。最终实现的方案是:
html
canvas
html
为 img
,src
为 base64也就是说,用户刚进入页面时,显示的是 html。js 执行完后,将原有 html 删掉,替换为图片。
再回头看咱们的需求:
- html 展现实时用户信息
- 点击保存将当前页面保存成图片至本地
其实最终只实现了第一点,而第二点实际上是实现了一半,图片虽然生成了,但保存功能仍是须要用户长按图片,调起微信内置菜单来完成。在进行 H5 开发时,一旦考虑到微信,就有可能出现一些以前考虑不到的问题和限制,对此,产品经理和程序员都要尽量地多多了解。知道在微信中,能干什么,不能干什么,下降开发和反复沟通的成本。
但愿以上内容可以对你们之后的开发有所帮助。
做者: 丁香园 f2e - 顾重喜