专业编程基础技术教程

网站首页 > 基础教程 正文

前端实现最佳截图方案(下)

ccvgpt 2024-11-22 11:30:34 基础教程 1 ℃

作者: 蜀中亮子

转发链接:https://mp.weixin.qq.com/s/ugrBaCIWYzGn8nuStp7ohw

前端实现最佳截图方案(下)

前言

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

上一篇:‘前端实现最佳截图方案(上)

换个思路

基于我们对于上篇html2canvas整个流程的实现,会发现中间换算会出现很多不精准的问题,那么怎么做一个可以精准的绘制呢?能不能把所有内部绘制的换算过程全部交给浏览器?

基本思路

上文提到canvas还可以绘制image、svg等等,此处就可以把html处理成svg的结果,然后再绘制到canvas上。

对于svg是一种可扩展标记语言,再转化的过程中,就需要使用到<foreignObject>这个svg元素。<foreignObject>允许包含不同的XML命名空间,在浏览器的上下文中,很可能是XHTML\HTML,如下是使用方式:



这样只需要指定对应的命名空间,就可以把它嵌套到foreignObject中,然后结合SVG,直接渲染。

什么是命名空间,相当于是元素名和属性名的一种集合,元素和属性可以有多种不同的集合,为了解决冲突,就需要有命名空间的指派,对于带有属性xmlns=""就是一个命名空间的表现形式。以下是多种命名空间:

  • HTML — http://www.w3.org/1999/xhtml
  • SVG — http://www.w3.org/2000/svg
  • MathML— http://www.w3.org/1998/math/MathML
  • 对于不同的命名空间,浏览器解析的方式也不一样,所以在SVG中嵌套HTML,解析SVG的时候遇到 http://www.w3.org/2000/svg 转化SVG的解析方式,当遇到了http://www.w3.org/1999/xhtml 就使用html的解析方式。

这是为什么SVG中可以嵌套HTML,并且浏览器能够正常渲染。

实现

但是这个过程中,会存在一些问题:

  1. SVG 是不允许连接到外部的资源,比如html中图片链接、css link方式的资源链接等,在SVG中都会有限制;
  2. html中会有脚本执行的情况,比如Vue的SPA单页项目,需要先执行js的逻辑才能够渲染出dom节点。但是SVG中,是不支持js执行的情况。
  3. SVG的位置大小和foreignObject标签的位置大小不能够确定,需要计算。

基于以上的情况,需要做一些其他的处理,以下为这个方案渲染的整个流程,看看如何解决存在的问题:



对于这种方案需要处理以上几个流程:

  1. 初始化不同类型的截图需要,比如DrawHTML(截取部分文档片段)、DrawDocument(截取完整document节点)、DrawURL(截取一个html资源链接)这几种形式,最后都会处理成截取整个document文档节点,以下是流程第一步的处理,。
  • DrawHTML 转换部分文档片段为一个完整的document文档节点,然后使用DrawDocument的方式处理。



DrawURL 转换一个html资源链接为截取一个完整的document文档节点,再使用DrawDocument的方式处理。

可以看到最后的方式都是处理成一个document文档,实现到drawDocument这个方法里面,使用绘制document的形式来渲染。

基于上面的思路,把document文档转为SVG,但是document文档里面包含了外部链接的图片资源、外部样式资源和脚本资源。这种情况在SVG是不支持的,所以这一步的处理方式是把所有的外部资源,处理为内联形式的,改造为新的document,比如:


以上这种文档结构中,所有的资源都是属于外部资源,如果要转变为SVG,就需要处理成内联的形式,构造新的document文档,如下:



所以上一步把所有截图形式都处理成为了渲染一个document文档之后,就需要对文档进行重构转换,处理文档内部所有外部资源,不同的资源对应不同的处理方式,这里需要处理的资源情况分为以下几点:

在html文档中存在img图片标签的链接为外部资源,需要处理为base64资源,通过loadAndInlineIages函数进行处理,以下是loadAndInlineIages函数。

loadAndInlineImages函数的处理流程是获取到所有和图片有关的标签,在通过ajajx请求下来,然后处理成base64的资源类型,对原有的图片标签进行替换,这样就把所有的标签图片,处理成为了内联资源类型。以下是encodeImageAsDataURI方法内部请求图片资源且转义base64的逻辑:


通过了以上步骤之后,此时的document文档里面的图片标签元素的资源已经全部为内联形式了

在html中同时也存在着脚本为外部资源的情况,对于脚本的处理逻辑,整体就比较简单了,获取到脚本的链接,请求脚本内容,之后用请求的内容替换原有的外部链接的<script>,以下为脚本处理函数loadAndInlineScript的实现方式:



以上处理脚本资源的方法整体比较简单。

  • 在处理完成了脚本和图片的情况之后,目前剩余需要处理成为内联资源的情况还剩下外部样式表。但是此处还需要注意一点,对于本来存在的内联样式也需要处理,因为可能会出现使用外链背景图的情况、通过@import导入样式表的情况。

所以对于外部样式表请求下来的内容会存在同样的问题,所以对于外部样式表而言,整体的流程就是通过ajax请求外部样式内容,然后对内容存在背景图片和@import的情况做处理。先供上对于css处理不同情况的流程处理:



通过上面的架构流程图,可以看出来远端请求的样式表需要和内联样式做同样的处理,把内部的远端图片资源和字体资源处理为内联形式。

  • 对外部样式表的请求逻辑,大致逻辑如下:

通过以上代码,可以看见请求和处理逻辑全部在requestStylesheetAndInlineResources方法中,以下为代码方法:



从以上的代码逻辑中,可以清楚,有几个promise的处理流程,每一个流程处理的内容主要做了以下几件事情:

    1. 请求远端样式资源表,通过封装的ajax方法;
    2. 处理请求下来的样式表中可能使用到的远端图片或者字体资源链接,使用inlineCss.adjustPathsOfCssResources方法,把使用到资源的相对地址,处理成为绝对地址;
    3. 通过inlineCss.loadCSSImportsForRule方法处理@import资源引入的情况
    4. 请求样式表中使用到的图片和文字资源,并且处理成内联,这一步的逻辑在inlineCss.loadAndInlineCSSResourcesForRules这个方法中
    5. 基于原有样式表构造新的样式表


现在我们来看一下,对应每一种处理情况具体所做的事情:

  • ajax请求资源,这一步不做深入,简单的ajax封装
  • 对于adjustPathsOfCssResources方法处理链接相对路劲变为绝对路劲,整体的实现思路是遍历查找所有的CSSRule,查找到background、font-face、@import等对应的Rule,解析属性设置的值,判断引用的地址是否是外部url,处理路劲变换为绝对路劲。构建新的CSSRule。

通过上面的逻辑处理之后,此时所有的css中包含的外部资源的链接已经处理为绝对路劲,对于整个资源css中的资源内联处理,第一步就已经完成了。

  • 对于处理完成路劲之后,对于上面整个资源处理的大流程loadCSSImportsForRule方法就是把import的外部css请求回来,然后重新构建新的css。大体的思路为搜集当前css中所有的import资源地址,下载下来之后,构建为新的css,在分析新的css是否包含import,递归写入到最后的CSSRule中。 对于以上代码处理@import的函数中,loadAndInlineCSSImport方法就是核心的逻辑了,结合上面讲的整体处理流程,看看以下代码:



这样就把所有的css中的@import的资源,也处理进来了。


  • 对于css资源,处理到这一步之后,结合我们上面的流程图,就只剩下把所有的资源诸如背景图、font-face等引用的外部链接变为内联资源。这一步的实现和上面css中转换资源相对路劲到绝对路劲,整个思路是一致的。区别在于对于最后一步替换相对路劲为绝对路劲的url不一致,这里需要替换的是资源请求下来之后处理成为base64的data数据之后的链接。
    1. 首先遍历所有CSSRule,找出需要替换的所有Rule
    2. 获取对应Rule中包含的外部链接
    3. 请求资源回来之后,处理为base64类型的data链接
    4. 替换原有Rule中资源的地址,改为内联类型,构造成为新的CSSRule;


这样整个流程中的资源就已经处理完成,目前构造出来的文档,全是内联文档,符合构造SVG的要求;

  1. 在处理完成内容之后,就需要计算整个文档需要展示的大小,这是在SVG构建的时候需要使用到的;因为在用户截图的时候会传入对应想要的大小,这个时候,怎么去控制。大致的思路如下:
    • 根据用户传入宽高大小创建iframe,把上面处理过的内联文档装载到iframe中执行
    • 获取到执行之后文档的clientWidth和clientHeight,同时根据zoom计算缩放的大小来作为最后SVG需要渲染的结果
    • 获取装载之后iframe中的文档的font-size来设置SVG的内容字体大小

经过上面这些步骤,我们计算出来了大小,剩下最后一步,序列化处理之后的文档节点构建SVG;

  1. 序列化文档节点的过程,就是把文档节点处理成为整个字符串的过程,在大多数浏览器中都是有序列化api的支持,不过有少数兼容问题,所以最优方法为自己实现序列化的过程,整个过程逻辑主要为递归遍历文档节点,处理节点名称大小写、文本内容中包含<、>、&这几个符号的转义处理及对整个文档添加指定的命名空间。
  2. 在序列化文档文档之后,就需要使用序列化之后的内容和计算出来的展示文档大小值来构建SVG,整个构建的过程代码大致流程: 至此,SVG构建已经构建完成,剩下最后一步就是把SVG处理成图片可以显示的资源;


6.处理图片显示的资源这个过程,其实有两种实现:

  • 第一种是通过createObjectURL把图片资源处理为blob数据,img使用时直接使用blob数据;
  • 第二种是直接encode对应的SVG资源,构建data资源链接 这两种生成的连接都可以对应添加到图片的src中;当然,此时也可以拿到对应的SVG调用canvas绘图的api来绘制SVG,做二次加工;

至此,这个思路的实现全部完成;

思路缺点

基于以上两个思路的对比,明显会发现,使用html通过foreignObject构建SVG的方法要简单清晰,但是对于一些浏览器也会有一些小问题,不过已经有一个比较不错的库通过hack的方式,处理了这些问题。rasterizeHTML.js是一个比较不错的截图库,实现的逻辑就是基于上面的思路。

不过这两种方式都会涉及到一个问题,就是图片资源跨域问题,如果图片为跨域图片,就需要通过CORS来处理。由于在 `canvas` 位图中的像素可能来自多种来源,包括从其他主机检索的图像或视频,因此不可避免的会出现安全问题,所以对于除CORS以外的跨域图片,canvas都会被处理成污染的情况,此时getImageData、toBlob、toDataURL都会被禁止调用,这种机制也可以避免未经许可拉取远程网站信息而导致的用户隐私泄露,这对于webgl的贴图也是同样的处理,不能使用除CORS以外的跨域图片。

总结

以上总结了html2canvas的整体思路及优缺点,目前html2canvas源码里面也已经开始融合第二种思路,这说明了第二种截图思路的优点。但是第二种思路的过程中自己手动处理的序列化性能相比浏览器处理而言略微慢一点,等到浏览器序列化都支持的特别好的时候,就可以替代这一部分。当然,咱们也可以打开思路,结合webassembly来重写序列化的部分,打开整个BS架构大门。

推荐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/ugrBaCIWYzGn8nuStp7ohw

Tags:

最近发表
标签列表