1. 背景

将网页保存为图片(以下简称为快照),是用户记录和分享页面信息的有效手段,在各种兴趣测试和营销推广等形式的活动页面中尤为常见。

快照环节通常处于页面交互流程的末端,汇总了用户最终的参与结果,直接影响到用户对于活动的完整体验。因此,生成高质量的页面快照,对于活动的传播和品牌的转化具有十分重要的意义。

本文基于云音乐往期优质活动的相关实践(例如「关于你的画」、「权力的游戏」和「你的使用说明书」等),从快照的内容完整性清晰度转换效率等多个方面,讨论将网页转换为高质量图片的实践探索。

2. 适用场景

  • 适用于将页面转为图片,特别是对实时性要求较高的场景。

  • 希望在快照中展示跨域图片资源的场景。

  • 针对生成图片内容不完整、模糊或者转换过程缓慢等问题,寻求有效解决方案的场景。

3. 原理简析

3.1 方案选型

依据图片是否由设备本地生成,快照可分为前端处理和后端处理两种方式。

由于后端生成的方案依赖于网络通信,不可避免地存在通信开销和等待时延,同时对于模板和数据结构变更也有一定的维护成本。

因此,出于实时性灵活性等综合考虑,我们优先选用前端处理的方式。

3.2 基本原理

前端侧对于快照的处理过程,实质上是将 DOM 节点包含的视图信息转换为图片信息的过程。这个过程可以借助 canvas 的原生 API 实现,这也是方案可行性的基础。

theory

具体来说,转换过程是将目标 DOM 节点绘制到 canvas 画布,然后 canvas 画布以图片形式导出。可简单标记为绘制阶段和导出阶段两个步骤:

  • 绘制阶段:选择希望绘制的 DOM 节点,根据nodeType调用 canvas 对象的对应 API,将目标 DOM 节点绘制到 canvas 画布(例如对于<img>的绘制使用 drawImage 方法)。

  • 导出阶段:通过 canvas 的 toDataURL 或 getImageData 等对外接口,最终实现画布内容的导出。

3.3 原生示例

具体地,对于单个<img>元素可按如下方式生成自身的快照:

HTML:

<img id="target" src="./music-icon.png" />

JavaScript:

// 获取目标元素
const target = document.getElementById('target');// 新建canvas画布
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const ctx = canvas.getContext("2d");// 导出阶段:从canvas导出新的图片
const exportNewImage = (canvas) => {const exportImage = document.createElement('img');exportImage.src = canvas.toDataURL();document.body.appendChild(exportImage);
}// 绘制阶段:待图片内容加载完毕后绘制画布
target.onload = () => {// 将图片内容绘入画布ctx.drawImage(target, 0, 0, 100, 100);// 将画布内容导出为新的图片exportNewImage(canvas);
}

其中,drawImage是 canvas 上下文对象的实例方法,提供多种方式将 CanvasImageSource 源绘制到 canvas 画布上。exportNewImage用于将 canvas 中的视图信息导出为包含图片展示的 data URI。

4. 基础方案

在上一部分中,我们可以看到基于 canvas 提供的相关基础 API,为前端侧的页面快照处理提供了可能。

然而,具体的业务应用往往更加复杂,上面的「低配版」实例显然未能覆盖多数的实际场景,例如:

  • canvas 的drawImage方法只接受 CanvasImageSource,而CanvasImageSource并不包括文本节点、普通的div等,将非<img>的元素绘制到 canvas 需要特定处理。

  • 当有多个 DOM 元素需要绘制时,层级优先级处理较为复杂。

  • 需要关注floatz-indexposition等布局定位的处理。

  • 样式合成绘制计算较为繁琐。

因此,基于对综合业务场景的考虑,我们采用社区中认可度较高的方案:html2canvascanvas2image作为实现快照功能的基础库。

4.1 html2canvas

提供将 DOM 绘制到 canvas 的能力

这款来自社区的神器,为开发者简化了将逐个 DOM 绘制到 canvas 的过程。简单来说,其基本原理为:

  • 递归遍历目标节点及其子节点,收集节点的样式信息;

  • 计算节点本身的层级关系,根据一定优先级策略将节点逐一绘制到 canvas 画布中;

  • 重复这一过程,最终实现目标节点内容的全部绘制。

在使用方面,html2canvas对外暴露了一个可执行函数,它的第一个参数用于接收待绘制的目标节点(必选);第二个参数是可选的配置项,用于设置涉及 canvas 导出的各个参数:

// element 为目标绘制节点,options为可选参数
html2canvas(element[,options]);

简易调用示例如下:

import html2canvas from 'html2canvas';const options = {};// 输入body节点,返回包含body视图内容的canvas对象
html2canvas(document.body, options).then(function(canvas) {document.body.appendChild(canvas);
});

4.2 canvas2image

提供由 canvas 导出图片信息的多种方法

相比于html2canvas承担的复杂绘制流程,canvas2image 所要做的事情简单的多。

canvas2image仅用于将输入的 canvas 对象按特定格式转换和存储操作,其中这两类操作均支持 PNG,JPEG,GIF,BMP 四种图片类型:

// 格式转换
Canvas2Image.convertToPNG(canvasObj, width, height);
Canvas2Image.convertToJPEG(canvasObj, width, height);
Canvas2Image.convertToGIF(canvasObj, width, height);
Canvas2Image.convertToBMP(canvasObj, width, height);// 另存为指定格式图片
Canvas2Image.saveAsPNG(canvasObj, width, height);
Canvas2Image.saveAsJPEG(canvasObj, width, height);
Canvas2Image.saveAsGIF(canvasObj, width, height);
Canvas2Image.saveAsBMP(canvasObj, width, height);

实质上,canvas2image只是提供了针对 canvas 基础 API 的二次封装(例如 getImageData、toDataURL),而本身并不依赖html2canvas

在使用方面,由于目前作者并未提供 ES6 版本的canvas2image(v1.0.5),暂时不能直接以 import 方式引入该模块。

对于支持现代化构建的工程中(例如 webpack),开发者可以自助 clone 源码并手动添加 export 获得 ESM 支持:

支持 ESM 导出

// canvas2Image.js
const Canvas2Image = function () {...
}();// 以下为定制添加的内容
export default Canvas2Image;

调用示例

import Canvas2Image from './canvas2Image.js';// 其中,canvas代表传入的canvas对象,width, height分别为导出图片的宽高数值
Canvas2Image.convertToPNG(canvas, width, height)

4.3 组合技

接下来,我们基于以上两个工具库,实现一个基础版的快照生成方案。同样是分为两个阶段,对应 3.2 节的基本原理:

  • 第一步,通过html2canvas实现 DOM 节点绘制到 canvas 对象中;

  • 第二步,将上一步返回的 canvas 对象传入canvas2image,进而按需导出快照图片信息。

具体地,我们封装一个convertToImage的函数,用于输入目标节点以及配置项参数,输出快照图片信息。

JavaScript

// convertToImage.js
import html2canvas from 'html2canvas';
import Canvas2Image from './canvas2Image.js';/*** 基础版快照方案* @param {HTMLElement} container* @param {object} options html2canvas相关配置*/
function convertToImage(container, options = {}) {return html2canvas(container, options).then(canvas => {const imageEl = Canvas2Image.convertToPNG(canvas, canvas.width, canvas.height);return imageEl;});
}

5. 进阶优化

通过上一节的实例,我们基于html2canvascanvas2image,实现了相比原生方案通用性更佳的基础页面快照方案。然而面对实际复杂的应用场景,以上基础方案生成的快照效果往往不尽如人意。

快照效果的差异性,一方面是由于html2canvas导出的视图信息是通过各种 DOM 和 canvas 的 API 复合计算二次绘制的结果(并非一键栅格化)。因此不同宿主环境的相关 API 实现差异,可能导致生成的图片效果存在多端不一致性或者显示异常的情况。

另一方面,业务层面的因素,例如对于开发者html2canvas的配置有误或者是页面布局不当等原因,也会对生成快照的结果带来偏差。

社区中也可以常见到一些对于生成快照质量的讨论,例如:

  • 为什么有些内容显示不完整、残缺、白屏或黑屏?

  • 明明原页面清晰可辨,为什么生成的图片模糊如毛玻璃?

  • 将页面转换为图片的过程十分缓慢,影响后续相关操作,有什么好办法么?

  • ...

下面我们从内容完整性清晰度优化转换效率,进一步探究高质量的快照解决方案。

5.1 内容完整性

首要问题:保证目标节点视图信息完整导出

由于真机环境的兼容性和业务实现方式的不同,在一些使用html2canvas过程中常会出现快照内容与原视图不一致的情况。内容不完整的常见自检checklist如下:

  • 跨域问题:存在跨域图片污染 canvas 画布。

  • 资源加载:生成快照时,相关资源还未加载完毕。

  • 滚动问题:页面中滚动元素存在偏移量,导致生成的快照顶部出现空白。

5.1.1 跨域问题

常见于引入的图片素材相对于部署工程跨域的场景。例如部署在https://st.music.163.com/上面的页面中引入了来源为https://p1.music.126.net的图片,这类图片即是属于跨域的图片资源。

由于 canvas 对于图片资源的同源限制,如果画布中包含跨域的图片资源则会污染画布( Tainted canvases ),造成生成图片内容混乱或者html2canvas方法不执行等异常问题。

对于跨域图片资源处理,可以从以下几方面着手:

(1)useCORS 配置

开启html2canvasuseCORS配置项,示例如下:

// doc: http://html2canvas.hertzen.com/configuration/
const opts = {useCORS   : true,   // 允许使用跨域图片allowTaint: false   // 不允许跨域图片污染画布
};html2canvas(element, opts);

html2canvas的源码中对于useCORS配置项置为true的处理,实质上是将目标节点中的<img>标签注入 crossOrigin 为anonymous的属性,从而允许载入符合 CORS 规范的图片资源。

其中,allowTaint默认为false,也可以不作显式设置。即使该项置为true,也不能绕过 canvas 对于跨域图片的限制,因为在调用 canvas 的toDataURL时依然会被浏览器禁止。

(2)CORS 配置

上一步的useCORS的配置,只是允许<img>接收跨域的图片资源,而对于解锁跨域图片在 canvas 上的绘制并导出,需要图片资源本身需要提供 CORS 支持。

这里介绍下跨域图片使用 CDN 资源时的注意事项:

验证图片资源是否支持 CORS 跨域,通过 Chrome 开发者工具可以看到图片请求响应头中应含有Access-Control-Allow-Origin的字段,即坊间常提到的跨域头。

例如,某个来自 CDN 图片资源的响应头示例:

// Response Headers
access-control-allow-credentials: true
access-control-allow-headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type
access-control-allow-methods: GET,POST,OPTIONS
access-control-allow-origin: *

不同的 CDN 服务商配置资源跨域头的方式不同,具体应咨询 CDN 服务商。

特殊情况下,部分 CDN 提供方可能会存在图片缓存不含 CORS 跨域头的情况。为保证快照显示正常,建议优先联系 CDN 寻求技术支持,不推荐通过图片链接后缀时间戳等方式强制回源,避免影响源站性能和 CDN 计费。

(3)服务端转发

在微信等第三方 APP 中,平台的用户头像等图片资源是不直接提供 CORS 支持的。此时需要借助服务端作代理转发,从而绕过跨域限制。

即通过服务端代为请求平台用户的头像地址并转发给客户端(浏览器),当然这个服务端接口本身要与页面同源或者支持 CORS。

为简洁表述,假设前端与后端针对跨域图片转发作如下约定,且该接口与前端工程部署在相同域名下:

请求地址 请求方式 传入参数 返回信息
/api/redirect/image GET redirect,表示原图地址 Content-Typeimage/png的图片资源

页面中的<img>通过拼接/api/redirect/image与代表原图地址的查询参数redirect,发出一个 GET 请求图片资源。由于接口与页面同源,因此不会触发跨域限制:

<img src="/api/redirect/image?redirect=thirdwx.qlogo.cn/somebody/avatar" alt="user-pic" class="avatar" crossorigin="anonymous">

对于服务端接口的实现,这里基于 koa 提供了一则简易示例:

const Koa = require('koa');
const router = require('koa-router')();
const querystring = require('querystring');const app = new Koa();/*** 图片转发接口* - 接收 redirect 入参,即需要代为请求的图片URL* - 返回图片资源*/
router.get('/api/redirect/image', async function(ctx) {const querys = ctx.querystring;if (!querys) return;const { redirect } = querystring.parse(querys);const res = await proxyFetchImage(redirect);ctx.set('Content-Type', 'image/png');ctx.set('Cache-Control', 'max-age=2592000');ctx.response.body = res;})/*** 请求并返回图片资源* @param {String} url 图片地址*/
async function proxyFetchImage(url) {const res = await fetch(url);return res.body;
}const res = await proxyFetchImage(redirect);app.use(router.routes());

在浏览器看来,页面请求的图片资源仍是相同域名下的资源,转发过程对前端透明。建议在需求开发前了解图片资源的来源情况,明确是否需要服务端支持。

在云音乐早期的活动「权力的游戏」中,使用了同类方案,实现了微信平台中用户头像的完整绘制和快照导出。

5.1.2 资源加载

资源加载不全,是造成快照不完整的一个常见因素。在生成快照时,如果部分资源没有加载完毕,那么生成的内容自然也谈不上完整。

除了设置一定的延迟外,如果要确保资源加载完毕,可以基于 Promise.all 实现。

加载图片

const preloadImg = (src) => {return new Promise((resolve, reject) => {const img = new Image();img.onload = () => {resolve();}img.src = src;});
}

确保在全部加载后生成快照

const preloadList = ['./pic-1.png','./pic-2.png','./pic-3.png',
];Promise.all(preloadList.map(src => preloadImg(src))).then(async () => {convertToImage(container).then(canvas => {// ...})
});

实际上,以上方法只是解决页面图片的显示问题。在真实场景中,即使页面上的图片显示完整,保存快照后依然可能出现内容空白的情况。原因是 html2canvas 库内部处理时,对图片资源仍会做一次加载请求;如果此时加载失败,那么该部分保存快照后即是空白的。

下面介绍图片资源转 Blob 的方案,保证图片的地址来自本地,避免在快照转化时加载失败的情况。这里提到的 Blob 对象表示一个不可变、代表二进制原始数据的类文件对象,在特定的使用场景会使用到。

图片资源转 Blob:

 // 返回图片Blob地址
const toBlobURL = (function () {const urlMap = {};// @param {string} url 传入图片资源地址return function (url) {// 过滤重复值if (urlMap[url]) return Promise.resolve(urlMap[url]);return new Promise((resolve, reject) => {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');const img = document.createElement('img');img.src = url;img.onload = () => {canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0);// 关键												

高质量前端快照方案:来自页面的「自拍」相关推荐

  1. [FFMpeg开发]视频转高质量GIF优化方案(接近ps生成效果),从原理剖析

    摘要 虽然此前有人发过了,但是这个博主没有分析原理并且没有提炼出来.不适合开发者学习. 所以我只是进行二次优化,原文高质量视频转gif 此前,做产品的时候,产品用到了ffmpeg框架,手上几个ffmp ...

  2. 推荐几个高质量前端公众号

    技术日新月异,发展迅速,作为一个与时俱进的互联网人,需要不断地学习扩宽视野. 今天为大家推荐几个技术领域中出类拔萃的公众号,它们的每一篇推文都值得你点开! 1 前端之神 模拟面试 Vue面试 300w ...

  3. 这家高精定位巨头,为何拼命盘活「北斗+」生态?

    迈向2023年,"抢占市场份额"或是低速无人驾驶产业的关键词之一. 一方面,在相关智能驾驶技术.资方和相关政策的推动下,部分特定场景的低速无人驾驶商业化正在加速,尤其是在智慧矿区. ...

  4. app.vue 跳转页面_「案例分析」APP关键页面UX优化拆解—以珍爱网APP为例

    珍爱网APP一共有五个主要的关键页面.第一个是推荐页面,第二个是直播页面,第三个动态页面,第三个是消息页面,第五个是个人设置. (一)APP页面结构梳理 珍爱网APP关键页面结构 (二)各部分关键页面 ...

  5. 技术人如何做高质量方案汇报

    汇报不是目的,是沟通手段.用正确的方法组织汇报,沟通更顺畅,事半功倍.一切的美好,都是精心准备的结果. 技术人员做汇报,经常会出现"讲粗讲细领导都听不懂"."讲多了时间不 ...

  6. 伪原创文章特点(高质量的伪原创文章有哪些特点)

    不论是原创还是伪原创都希望被搜索引擎收录,页面被收录对网站具有重要意义,除了积累网站的权重,还使得网站的长尾关键词能够有效发挥作用.当然,我们在写的伪原创文章中不但要符合搜索引擎的要求,也要给用户一个 ...

  7. 「划线高亮」和「插入笔记」—— 不止是前端知识点

    如今前端领域:serverless,low code,全栈化等概念遍布漫天.开发者们热衷于讨论「如何把前端格局做大」,「如何将高高在上的概念落地」.此时,你有没有感受到「还不知道发展方向到底是什么,就 ...

  8. 读书笔记:编写高质量代码--web前端开发修炼之道(二:5章)

    读书笔记:编写高质量代码--web前端开发修炼之道 这本书看得断断续续,不连贯,笔记也是有些马虎了,想了解这本书内容的童鞋可以借鉴我的这篇笔记,希望对大家有帮助. 笔记有点长,所以分为一,二两个部分: ...

  9. 【转】前端进阶之路:如何高质量完成产品需求开发

    有时候好的文章不是光收藏一下就可以的,要研究为什么人家那么思考,你为什么不行?要多想. 前言 看到这篇的时候,想起前几周的周末参加的一个工作坊,讲师有提到一个问题,作为程序员你们写了解你们写代码的目的 ...

最新文章

  1. Android线程管理(一)
  2. 如何判断两个单向链表是否有相交,并找出交点
  3. 超百家金融机构争相出席,只因飞贷宣布输出全球领先的移动信贷整体技术
  4. 【论文解读】腾讯FAT | 未来感知的多样化趋势推荐框架
  5. 计算机科学与技术专业导向ppt,计算机科学与技术专业导向讲座 第讲.ppt
  6. cms java垃圾回收_java cms垃圾回收器总结
  7. 【超详细】一文学会链表解题(建议收藏!)
  8. 20180601]函数与标量子查询2.txt
  9. springboot 添加拦截器之后中文乱码_spring boot 2.x 添加拦截器配置未生效的问题
  10. PID参数整定法(1)
  11. 计算机的组成结构6,计算机组成及结构.6.ppt
  12. Apollo灰度发布
  13. Struts2学习笔记(十六) 文件上传(File Upload)
  14. JMX Java Management Extensions
  15. 【SDK编程】LRC歌词制作工具V1.0
  16. Mock工具介绍,为什么使用Mock?
  17. MongoDB集群和安全
  18. Android4.4蓝牙耳机HFP流程分析-1
  19. 大二第二次月赛--买水果
  20. 靶机渗透练习91-Grotesque:2

热门文章

  1. 消息称:华为将官宣为全国老款手机内存扩容
  2. 根据IMSI区别运营商
  3. 零跑股价再度上涨的原因到底是什么呢?
  4. 哈尔滨工业大学计算机考研专业课,2020考研哈尔滨工业大学计算机考研考试科目...
  5. 无线通信中 RSRP RSRQ RSSI SINR的定义和区别
  6. Mac和Win7双系统 + 完美文件共享
  7. android pcm文件大小_Android中的PCM设备
  8. mac下使用ipv6观看电视
  9. 建模知识2: ROC、AUC、K-S曲线
  10. 步进电机--S 曲线的C算法