计算机系统应用教程网站

网站首页 > 技术文章 正文

前端实现最佳截图方案(上) 前端截取图片

btikc 2024-10-11 11:17:45 技术文章 2 ℃ 0 评论

作者:蜀中亮子

转发链接:https://mp.weixin.qq.com/s/ghXm-dySERTFsXEWw79afA

旧闻重发,由于上一次的图片有些糊,这次分为上下两篇发送,不至于阅读压力太大

开篇

平时很多时候,需要把当前页面或者页面某一部分内容保存为图片分享出去,也或者有其他的业务用途,这种在很多的营销场景和裂变的过程都会使用到,那我们要把一个页面的内容转化为图片的这个过程,就是比较需要探讨的了。

首先这种情况,想到的实现方案就是使用canvas来实现,我们探索一下基本实现步骤:

  1. 把需要分享或者记录的内容绘制到canvas上;
  2. 把绘制之后的canvas转换为图片;

这里需要明确的一点就是,只要把数据绘制到canvas上,这就在canvas画布上形成了被保存在内存中的像素点信息,所以可以直接调用canvas的api方法toDataURL、toBlob,把已经形成的像素信息转化为可以被访问的资源uri,同时保存在服务器当中。这就很轻松的解决了第二步(把canvas转为图片链接),下面是代码的实现:


在实现了第二步的情况之下,需要关注的就是第一步的内容,怎么把内容绘制到canvas上,我们知道canvas的绘图环境有一个方法是ctx.drawImage,可以绘制部分元素到canvas上,包含图片元素Image、svg元素、视频元素Video、canvas元素、ImageBitmap数据等,但是对于一般的其他div或者列表li元素它是不可以被绘制的。

所以,这不是直接调用绘图的api就可以办到的,我们就需要思考其他的方法。在一般的实现上,比较常见的就是使用html2canvas,那么我们先来聊聊html2canvas的使用和实现。

html2canvas的使用及实现html2canvas使用实现

使用

首先看一下html2cavas的使用方法:



调用html2canvas方法传入想要截取的dom,执行之后,返回一个Promise,接收到的canvas上,就绘制了我们想要截取的dom元素。到这一步之后,我们再调取canvas转图片的方法,就可以对其做其他的处理。

这里它的html2canvas方法还支持第二个选项传入一些用户的配置参数,比如是否启用缓存、整个绘图canvas的宽高值等。

在这个转换的过程,在html2canvas的内部,是怎么把dom元素绘制到canvas上的,这是咱们需要思考的问题!

实现

首先咱们先献上一个内部的大致流程图:


对比着内部的流程图,就可以理一下整体的思路,整体的思路就是遍历目标节点和子节点,收集样式,计算节点本身的层级关系和根据不同的优先级绘制到画布中,下面基于这个思路,咱们深入一下整个过程:

  1. 调用html2canvs函数,直接返回一个执行函数,这一步没有什么;
  2. 在执行函数的内部第一步是构建配置项defaultOptions,在合并默认配置的过程中,有一个缓存的配置,它会生成处理缓存的方法;
    • 处理缓存类,对于一个页面中的多个不同的地方渲染调用多次的情况做优化,避免同一个资源被多次加载;
    • 缓存类里面控制了所有图片的加载和处理,包括使用proxy代理和使用cors跨域资源共享这两种情况资源的处理,同时也对base64和blob这两种形式资源的处理。比如如果渲染dom里面包含一个图片的链接类型是blob,使用的方式就是如下处理,然后添加到缓存类中,下次使用就不需要再重新请求。



  1. 在上一步生成了默认配置的情况之下,传入需要绘制的目标节点element和配置到DocumentCloner里面,这个过程会克隆目标节点所在的文档节点document,同时把目标节点也克隆出来。这个过程中,只是克隆了开发者定义的对应节点样式,并不是结合浏览器渲染生成特定视图最后的样式



如上这个.box的元素节点,定义的样式只有高度,但是在浏览器渲染之下,会对它设置默认的文字样式等等

  1. 基于上一步的情况,就需要把克隆出来的目标节点所在的文档节点document进行一次浏览器的渲染,然后在收集最终目标节点的样式。于此,把克隆出来的目标节点的document装载到一个iframe里面,进行一次渲染,然后就可以获取到经过浏览器视图真实呈现的节点样式。



在这个过程中,就可以通过`window.getComputedStyle`这个API拿到要克隆的目标节点上所有的样式了(包含自定义和浏览器默认的结合最终的样式);

  1. 目标节点的样式和内容都获取到了之后,就需要把它所承载的数据信息转化为canvas可以使用的数据类型,比如某一个子节点的宽度设置为50%或者2rem,在这个过程中,就需要根据父级的宽度把它计算成为像素级别的单位。同时对于每一个节点而言需要绘制的包括了边框、背景、阴影、内容,而对于内容就包含图片、文字、视频等。这个过程就需要对目标节点的所有属性进行解析构造,分析成为可以理解的数据形式。




如上图片这种数据结构和我注释一样,在它内部把每一个节点处理成为了一个container,它的上面有一个styles字段,这个字段是所有节点上的样式经过转换计算之后的数据,还有一个textNodes属性,它表示当前节点下的文本节点,如上,每一个文本的点的内容使用text来表示,位置和大小信息放置在textBounds中。对于elements字段存放的就是当前节点下除了文本节点外,其他节点转换成为的container,最后一个就是bounds字段,存放的是当前节点的位置和大小信息。可以看一下container这个类的代码:


基于这种情况,每一个container数据结构的elements属性都是子节点,整个节点就够构造成一个container tree。

  1. 在通过解析器把目标节点处理成特定的数据结构container之后,就需要结合canvas调用渲染方法了,我们在浏览器里面创建多个元素的时候,不同的元素设置不同的样式,最后展示的结果就可能不一样,比如下面代码:


这个代码的展示结果如下:



此时,如果修改了代码中.sta1元素节点的opacity属性为0.999,此时整个布局的层级就会发生大变化,结果如下:



这个是什么原因?因为canvas绘图需要根据样式计算哪些元素应该绘制在上层,哪些在下层。元素在浏览器中渲染时,根据W3C的标准,所有的节点层级布局,需要遵循层叠上下文和层叠顺序的标准。当某一些属性发生变化,层叠上下文的顺序就可能发生变化,比如上列中透明度默认为1和不为1的情况(对于如何形成一个层叠上下文此处不做深入讲解,可以自行研究)。


更加直白的理解就是一部分属性会使一些元素形成一个单独的层级,不同属性的层级有一定的排列顺序。如下就是我们对应的顺序:


  • 形成层叠上下文环境的元素的背景与边框(相当于整个文档的背景和边框)
  • 拥有负 z-index 的子层叠上下文元素 (负的越高越层叠上下文层级越低)
  • 正常流式布局,非 inline-block,无 position 定位(static除外)的子元素
  • 无 position 定位(static除外)的 float 浮动元素
  • 正常流式布局, inline-block元素,无 position 定位(static除外)的子元素(包括 display:table 和 display:inline )
  • 拥有 z-index:0 或者auto的子堆叠上下文元素
  • 拥有正 z-index: 的子堆叠上下文元素(正的越低层叠上下文层级越低)
  • 在正常的元素情况下,没有形成层叠上下文的时候,显示顺序准守以上规则,在设置了一些属性,形成了层叠上下文之后,准守谁大谁上(z-index比较)、后来居上(后写的元素后渲染在上面)



此处,在清楚了元素的渲染需要遵循这个标准的情况之下,canvas绘制节点的时候,就需要先计算出整个目标节点里子节点渲染时所展现的不同层级。先给出来内部模拟层叠上下文的数据结构StackingContext:



以上就是某一个节点对应的层叠上下文在内部所表现出来的数据结构。很多属性都会形成层叠上下文,不同的属性形成的上下文,有不同的顺序,所以需要对目标节点的子节点解析,根据不同的样式属性分配到不同的数组中归类,比如遍历子节点的container上的styles,发现opacity为0.5,此时会形成层叠上下文,然后就把它构造成为上下文的数据结构StackContext。添加到zeroOrAutoZIndexOrTransformedOrOpacity这个数组中,这样一个递归查看子节点的过程,最后会形成一个层叠上下文的树。


  1. 基于上面构造出的数据结构,就开始调用内部的绘图方法了,一下代码是渲染某一个层叠上下文的代码:



如上绘图函数中,如果子元素形成了层叠上下文,就调用renderStack,这个方法内部继续调用了renderStackContent,这就形成了对于层叠上下文整个树的递归。



如果子元素没有形成层叠上下文,而是正常元素,就直接调用renderNode或者renderNodeContent。这两个的区别是renderNodeContent只负责渲染内容,不会渲染节点的边框和背景色。

对于renderNodeContent这个方法就是渲染一个元素节点里面的内容,可能是正常元素、图片、文字、svg、canvas、视频、input、iframe。对于图片、svg、视频、canvas这几种元素,直接通过调用前文提到的api,对于input需要根据样式计算出绘图数据来模拟完成,文字就直接根据提供的样式来绘制。重点需要提一下的是iframe,如果需要绘制的元素中包含了iframe,就相当于我们需要重新绘制一个新的文档document,处理方法是在内部调用html2canvas的api,绘制整个文档。


以下为多个不同类型的元素的绘制方式:

对于文字的绘制方式


对于图片、SVG、canvas元素的绘制:


对于代码中调用renderReplacedElement方法内部的处理逻辑,就是调用canvas的drawImage方法绘制以上三种数据形式;


对于需要绘制的元素是iframe的时候,做的处理逻辑就如同重新调用整个绘制方法,重新渲染页面的过程:


对于单选或者多选框的处理情况,就是根据是否选中,来绘制对应状态的样式:


对于input输入框的情况,首先需要绘制边框,然后把内部的文字绘制到输入框中,超出部分需要剪切掉,所以需要使用到canvas的clip绘图API:


对于最后一种需要考虑的就是列表,对于li、ol这两种列表,都可以设置不同类型的list-style,所以需要区分绘制。


以上整个过程,就是html2canvas的整体内部流程,最后的操作都是不同的线条、图片、文字等等的绘制,概括起来就是遍历目标节点,收集样式信息,转化为绘制数据,并且根据一定的优先级策略递归绘制节点到canvas画布上。

实现

在捋顺了整个大流程的情况之下,咱们来看看html2canvas的一些缺点

不支持的一些场景

  1. box-shadow属性,支持的不好,因为对于canvas的阴影API没有扩散半径。所以对于样式的阴影支持不是特别好;
  2. 边框虚线的情况也不支持,这一点源码里面没有使用setLineDash,是因为大多数浏览器原本不支持这个属性,chrome也是64版本之后才支持这个属性;
  3. css中元素的zoom属性支持也不是也特别好,因为换算会出现问题;
  4. 计算问题是最大的问题!!!因为每一次计算都会有精确度的省略问题,比如父元素的宽度是100像素,子元素是父元素的30%,这个时候转化为canvas绘图单位像素的时候,就会有省略的过程,在有多次省略的情况之下,精确度就会变得不精确。并且还涉及到一些圆弧的情况,这种弧度的计算,最后模仿出来,都会有失去精确度的问题。对于正常的浏览器渲染节点,渲染的内部逻辑,直接是由浏览器处理,但是对于html2canvas的方案,需要先计算为像素单位,然后绘制到canvas上,最后canvas元素还要经过浏览器的一次处理,才能够渲染出来。这个过程不止是换算单位失去精度,渲染也会失去精度。

解决方案,详见下一篇

推荐JavaScript学习相关文章

Node.js 实现抢票小工具&短信通知提醒(上)「干货」

Node.js 实现抢票小工具&短信通知提醒(下)「干货」

那些不常见,但却非常实用的JS知识点(上)

那些不常见,但却非常实用的JS知识点(下)

如何为团队定制自己的 Node.js 框架?

细品原生JS从初级到高级知识点汇总(一)

细品原生JS从初级到高级知识点汇总(二)

细品原生JS从初级到高级知识点汇总(三)

细品原生JS从初级到高级知识点汇总(四)

学习 jQuery 源码整体架构,打造属于自己的 js 类库

细说RTSP实现前端直播流探索记「干货」

一文带你搞懂前端登陆设计

使用 Node.js 将图片中的苹果变成橘子「实践」

基于Canvas实现的高斯模糊(上)「JS篇」

基于Canvas实现的高斯模糊(下)「JS篇」

由浅入深,66条JavaScript面试知识点(一)

由浅入深,66条JavaScript面试知识点(二)

由浅入深,66条JavaScript面试知识点(三)

由浅入深,66条JavaScript面试知识点(四)

由浅入深,66条JavaScript面试知识点(五)

由浅入深,66条JavaScript面试知识点(六)

由浅入深,66条JavaScript面试知识点(七)

为什么 setTimeout 有最小时延 4ms ?

如何处理 Node.js 中出现的未捕获异常?

Angular v10.0.0 正式发布,不再支持 IE9/10

基于 Docker 的 SSR 持续开发集成环境实践

细聊图解webpack 指南手册

一文带你彻底搞懂 NPM 知识点「进阶篇」

细聊webpack性能优化面面观

JS实现各种日期操作方法汇总

「实践」细聊前端性能优化总结

「实践」浏览器中的画中画(Picture-in-Picture)模式及其 API

「多图」一文带你彻底搞懂 Web Workers (上)

「多图」一文带你彻底搞懂 Web Workers (中)

深入细聊前端下载总结「干货」

细品西瓜播放器功能分析(上)「实践」

细品西瓜播放器功能分析(下)「实践」

细聊50道JavaScript基础面试题「附答案」

webpack4主流程源码解说以及动手实现一个简单的webpack(上)

webpack4主流程源码解说以及动手实现一个简单的webpack(下)

细聊前端架构师的视野

细聊应用场景再谈防抖和节流「进阶篇」

前端埋点统一接入方案实践

细聊微内核架构在前端的应用「干货」

一种高性能的Tree组件实现方案「干货」

进击的JAMStack

前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)上

前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)中

前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)下

一文带你搞懂 babel-plugin-import 插件(上)「源码解析」

一文带你搞懂 babel-plugin-import 插件(下)「源码解析」

JavaScript常用API合集汇总「值得收藏」

推荐10个常用的图片处理小帮手(上)「值得收藏」

推荐10个常用的图片处理小帮手(下)「值得收藏」

JavaScript 中ES6代理的实际用例

12 个实用的前端开发技巧总结

一文带你搞懂搭建企业级的 npm 私有仓库

教你如何使用内联框架元素 IFrames 的沙箱属性提高安全性?

细说前端开发UI公共组件的新认识「实践」

细说DOM API中append和appendChild的三个不同点

细品淘系大佬讲前端新人如何上王者「干货」

一文带你彻底解决背景跟随弹窗滚动问题「干货」

推荐常用的5款代码比较工具「值得收藏」

Node.js实现将文字与图片合成技巧

爱奇艺云剪辑Web端的技术实现

我再也不敢说我会写前端 Button组件「实践」

NodeX Component - 滴滴集团 Node.js 生态组件体系「实践」

Node Buffers 完整指南

推荐18个webpack精美插件「干货」

前端开发需要了解常用7种JavaScript设计模式

浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务

了不起的 Webpack HMR 学习指南(上)「含源码讲解」

了不起的 Webpack HMR 学习指南(下)「含源码讲解」

10个打开了我新世界大门的 WebAPI(上)「实践」

10个打开了我新世界大门的 WebAPI(中)「实践」

10个打开了我新世界大门的 WebAPI(下)「实践」

「图文」ESLint 在中大型团队的应用实践

Deno是代码的浏览器,你认同吗?

前端存储除了 localStorage 还有啥?

Javascript 多线程编程?的前世今生

微前端方案 qiankun(实践及总结)

「图文」V8 垃圾回收原来这么简单?

Webpack 5模块联邦引发微前端的革命?

基于 Web 端的人脸识别身份验证「实践」

「前端进阶」高性能渲染十万条数据(时间分片)

「前端进阶」高性能渲染十万条数据(虚拟列表)

图解 Promise 实现原理(一):基础实现

图解 Promise 实现原理(二):Promise 链式调用

图解 Promise 实现原理(三):Promise 原型方法实现

图解 Promise 实现原理(四):Promise 静态方法实现

实践教你从零构建前端 Lint 工作流「干货」

高性能多级多选级联组件开发「JS篇」

深入浅出讲解Node.js CLI 工具最佳实战

延迟加载图像以提高Web网站性能的五种方法「实践」

比较 JavaScript 对象的四种方式「实践」

使用Service Worker让你的 Web 应用如虎添翼(上)「干货」

使用Service Worker让你的 Web 应用如虎添翼(中)「干货」

使用Service Worker让你的 Web 应用如虎添翼(下)「干货」

前端如何一次性处理10万条数据「进阶篇」

推荐三款正则可视化工具「JS篇」

如何让用户选择是否离开当前页面?「JS篇」

JavaScript开发人员更喜欢Deno的五大原因

仅用18行JavaScript实现一个倒数计时器

图文细说JavaScript 的运行机制

一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索

推荐Web程序员常用的15个源代码编辑器

10个实用的JS技巧「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」

深入JavaScript教你内存泄漏如何防范

手把手教你7个有趣的JavaScript 项目-上「附源码」

手把手教你7个有趣的JavaScript 项目-下「附源码」

JavaScript 使用 mediaDevices API 访问摄像头自拍

手把手教你前端代码如何做错误上报「JS篇」

一文让你彻底搞懂移动前端和Web 前端区别在哪里

63个JavaScript 正则大礼包「值得收藏」

提高你的 JavaScript 技能10 个问答题

JavaScript图表库的5个首选

一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法

可视化的 JS:动态图演示 - 事件循环 Event Loop的过程

教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」

可视化的 js:动态图演示 Promises & Async/Await 的过程

原生JS封装拖动验证滑块你会吗?「实践」

如何实现高性能的在线 PDF 预览

细说使用字体库加密数据-仿58同城

Node.js要完了吗?

Pug 3.0.0正式发布,不再支持 Node.js 6/8

纯JS手写轮播图(代码逻辑清晰,通俗易懂)

JavaScript 20 年 中文版之创立标准

值得收藏的前端常用60余种工具方法「JS篇」

箭头函数和常规函数之间的 5 个区别

通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events

「前端篇」不再为正则烦恼

「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能

深入细品浏览器原理「流程图」

JavaScript 已进入第三个时代,未来将何去何从?

前端上传前预览文件 image、text、json、video、audio「实践」

深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系

推荐13个有用的JavaScript数组技巧「值得收藏」

前端必备基础知识:window.location 详解

不要再依赖CommonJS了

犀牛书作者:最该忘记的JavaScript特性

36个工作中常用的JavaScript函数片段「值得收藏」

Node + H5 实现大文件分片上传、断点续传

一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」

【实践总结】关于小程序挣脱枷锁实现批量上传

手把手教你前端的各种文件上传攻略和大文件断点续传

字节跳动面试官:请你实现一个大文件上传和断点续传

谈谈前端关于文件上传下载那些事【实践】

手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件

最全的 JavaScript 模块化方案和工具

「前端进阶」JS中的内存管理

JavaScript正则深入以及10个非常有意思的正则实战

前端面试者经常忽视的一道JavaScript 面试题

一行JS代码实现一个简单的模板字符串替换「实践」

JS代码是如何被压缩的「前端高级进阶」

前端开发规范:命名规范、html规范、css规范、js规范

【规范篇】前端团队代码规范最佳实践

100个原生JavaScript代码片段知识点详细汇总【实践】

关于前端174道 JavaScript知识点汇总(一)

关于前端174道 JavaScript知识点汇总(二)

关于前端174道 JavaScript知识点汇总(三)

几个非常有意思的javascript知识点总结【实践】

都2020年了,你还不会JavaScript 装饰器?

JavaScript实现图片合成下载

70个JavaScript知识点详细总结(上)【实践】

70个JavaScript知识点详细总结(下)【实践】

开源了一个 JavaScript 版敏感词过滤库

送你 43 道 JavaScript 面试题

3个很棒的小众JavaScript库,你值得拥有

手把手教你深入巩固JavaScript知识体系【思维导图】

推荐7个很棒的JavaScript产品步骤引导库

Echa哥教你彻底弄懂 JavaScript 执行机制

一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧

深入解析高频项目中运用到的知识点汇总【JS篇】

JavaScript 工具函数大全【新】

从JavaScript中看设计模式(总结)

身份证号码的正则表达式及验证详解(JavaScript,Regex)

浏览器中实现JavaScript计时器的4种创新方式

Three.js 动效方案

手把手教你常用的59个JS类方法

127个常用的JS代码片段,每段代码花30秒就能看懂-【上】

深入浅出讲解 js 深拷贝 vs 浅拷贝

手把手教你JS开发H5游戏【消灭星星】

深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】

手把手教你全方位解读JS中this真正含义【实践】

书到用时方恨少,一大波JS开发工具函数来了

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

手把手教你JS 异步编程六种方案【实践】

让你减少加班的15条高效JS技巧知识点汇总【实践】

手把手教你JS开发H5游戏【黄金矿工】

手把手教你JS实现监控浏览器上下左右滚动

JS 经典实例知识点整理汇总【实践】

2.6万字JS干货分享,带你领略前端魅力【基础篇】

2.6万字JS干货分享,带你领略前端魅力【实践篇】

简单几步让你的 JS 写得更漂亮

恭喜你获得治疗JS this的详细药方

谈谈前端关于文件上传下载那些事【实践】

面试中教你绕过关于 JavaScript 作用域的 5 个坑

Jquery插件(常用的插件库)

【JS】如何防止重复发送ajax请求

JavaScript+Canvas实现自定义画板

Continuation 在 JS 中的应用「前端篇」

作者:蜀中亮子

转发链接:https://mp.weixin.qq.com/s/ghXm-dySERTFsXEWw79afA

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表