2种方法!带您快速实现前端屏幕截图

2025-04-08 09:03:07发布    浏览5次    信息编号:206443

平台友情提醒:凡是以各种理由向你收取费用,均有骗子嫌疑,请提高警惕,不要轻易支付。

2种方法!带您快速实现前端屏幕截图

1。背景

页面屏幕截图功能在前端开发中相对常见,尤其是在与营销方案相关的需求中。例如,与普通链接共享相比,屏幕快照共享具有更丰富的显示和更多信息携带的优点。最近,我们在需求开发中遇到了相关的功能,因此我们研究了相关的实现和原则。

2。相关技术

前端需要实现页面屏幕截图的功能。现在最常见的方法是使用开源屏幕截图NPM库。通常,有两个NPM库会更频繁地使用:

以上两个常见的NPM库对应于两个共同的实施原则。为了实现前端屏幕截图,我们通常使用图形API重新绘制页面以生成图片。基本上,我们是两个实施解决方案:SVG(DOM-to-image)和(),两个解决方案具有相同的目标,即将DOM转换为图片。让我们分别看一下这两种解决方案。

3。DOM到图像

DOM到图像库主要使用SVG实现方法。简而言之,首先将DOM转换为SVG,然后将SVG转换为图片。

(i)如何使用

首先,让我们简要了解DOM到图像提供的核心API,并且有一些方法:

如果您需要生成PNG页面的屏幕截图,则实现代码如下:

import domtoimage from "domtoimage"
const node = document.getElementById('node');domtoimage.toPng(node,options).then((dataUrl) => { const img = new Image(); img.src = dataUrl; document.body.appendChild(img);})

TOPNG方法可以通过两个参数节点总和传递。

节点是生成屏幕截图的DOM节点。它的配置为受支持的属性,如下所示:width,style,。

(ii)原理分析

DOM的源代码不多,总计不到一千行。让我们使用TOPNG方法进行简单的源代码分析并分析其实现原则。简单的过程如下:

总体实施过程中使用了几个功能:

TOPNG功能相对简单。您可以通过调用Draw方法,将其转换为图片并将其返回。

function toPng(node, options) {  return draw(node, options || {})    .then((canvas) => canvas.toDataURL());}

绘制函数首先调用TOSVG方法以获取DOM转换后的SVG,然后将所获得的URL形式SVG处理为图像,创建一个新节点,然后在()方法的帮助下将生成的图像放在画布上。

function draw(domNode, options) {  return toSvg(domNode, options)  // 拿到的svg是image data URL, 进一步创建svg图片    .then(util.makeImage)    .then(util.delay(100))    .then((image) => {      // 创建canvas,在画布上绘制图像并返回      const canvas = newCanvas(domNode);      canvas.getContext("2d").drawImage(image, 0, 0);      return canvas;    });  // 新建canvas节点,设置一些样式的options参数  function newCanvas(domNode) {    const canvas = document.createElement("canvas");    canvas.width = options.width || util.width(domNode);    canvas.height = options.height || util.height(domNode);    if (options.bgcolor) {      const ctx = canvas.getContext("2d");      ctx.fillStyle = options.bgcolor;      ctx.fillRect(0, 0, canvas.width, canvas.height);    }    return canvas;  }}

function toSvg(node, options) {  options = options || {};  // 处理imagePlaceholder、cacheBust值  copyOptions(options);  return Promise.resolve(node)    .then((node) =>      // 递归克隆dom节点      cloneNode(node, options.filter, true))  // 把字体相关的csstext放入style    .then(embedFonts)  // clone处理图片,将图片链接转换为dataUrl    .then(inlineImages)  // 添加options里的style放入style    .then(applyOptions)    .then((clone) =>      // node节点转化成svg      makeSvgDataUri(clone,        options.width || util.width(node),        options.height || util.height(node)));  // 处理一些options的样式  function applyOptions(clone) {    ...    return clone;  }}

该函数主要涉及DOM节点,其中很多内容,简单的摘要如下:

function cloneNode(node, filter, root) {  if (!root && filter && !filter(node)) return Promise.resolve();  return Promise.resolve(node)    .then(makeNodeCopy)    .then((clone) => cloneChildren(node, clone, filter))    .then((clone) => processClone(node, clone));  function makeNodeCopy(node) {  // 将canvas转为image对象    if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL());    return node.cloneNode(false);  }  // 递归clone子节点  function cloneChildren(original, clone, filter) {    const children = original.childNodes;    if (children.length === 0) return Promise.resolve(clone);    return cloneChildrenInOrder(clone, util.asArray(children), filter)      .then(() => clone);    function cloneChildrenInOrder(parent, children, filter) {      let done = Promise.resolve();      children.forEach((child) => {        done = done          .then(() => cloneNode(child, filter))          .then((childClone) => {            if (childClone) parent.appendChild(childClone);          });      });      return done;    }  }  function processClone(original, clone) {    if (!(clone instanceof Element)) return clone;    return Promise.resolve()      .then(cloneStyle)      .then(clonePseudoElements)      .then(copyUserInput)      .then(fixSvg)      .then(() => clone);    // 克隆节点上的样式。    function cloneStyle() {       ...    }    // 提取伪类样式,放到css    function clonePseudoElements() {       ...    }    // 处理Input、TextArea标签    function copyUserInput() {       ...    }    // 处理svg    function fixSvg() {       ...    }  }}

首先,我们需要了解两个特征:

<svg xmlns="http://www.w3.org/2000/svg"><foreignObject width="120" height="50"><body xmlns="http://www.w3.org/1999/xhtml"><p>文字。p>body>foreignObject>svg>

您可以看到标签中设置了XMLNS =“”名称空间的标签。目前,标签及其子标签将根据XHTML标准渲染,并意识到SVG和XHTML的混合使用。

基于上述特征,让我们再次查看功能。该方法实现节点节点转换为SVG,并使用现在提到的两个重要功能。首先,通过()。()将DOM节点序列化为字符串,然后将转换后的字符串嵌入标签中。 XHTML可以嵌入SVG中,然后处理SVG作为数据返回。具体实现如下:

function makeSvgDataUri(node, width, height) {  return Promise.resolve(node)    .then((node) => {      // 将dom转换为字符串      node.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");      return new XMLSerializer().serializeToString(node);    })    .then(util.escapeXhtml)    .then((xhtml) => `${xhtml}`)  // 转化为svg    .then((foreignObject) =>    // 不指定xmlns命名空间是不会渲染的      ``)  // 转化为data:url    .then((svg) => `data:image/svg+xml;charset=utf-8,${svg}`);}

四个,

该库主要使用实现方法,主要过程是手动重新粉刷DOM,因此它只能正确渲染可理解的属性,并且有许多CSS属性无法正确渲染。

支持的CSS属性的完整列表:

浏览器兼容性:

3.5+ Opera 12+ IE9+ Edge 6+

官方文件地址:

(i)如何使用

// dom即是需要绘制的节点, option为一些可配置的选项import html2canvas from 'html2canvas'  html2canvas(dom, option).then(canvas=>{  canvas.toDataURL()})

常用配置:

所有配置文档:

(ii)原理分析

内部实现比DOM到图像更复杂。基本原理是阅读DOM元素的信息,根据此信息构建屏幕截图,并将其显示在画布中。关键点是重新绘制DOM。该过程的总体想法是:遍历目标节点的目标节点和子节点,记录遍历过程中所有节点的结构,内容和样式,然后计算节点本身的层次结构关系,最后根据不同的优先级将其绘制到画布中。

由于源代码量相对较大,因此可能无法像DOM-to-Image这样的详细分析它,但是您仍然可以理解整个过程。首先,您可以查看源代码中SRC文件夹中的代码结构,如下图所示:

简短分析:

基于上述核心文件,让我们简要了解解析过程。一般过程如下:

在此步骤中,将与传入和一些结合生成用于渲染的配置数据。在此过程中,将对配置项进行分类,例如(资源跨域相关),(缓存,日志相关),(窗口宽度和高度,滚动配置),(指定DOM的配置),(结果相关的配置,包括用于生成图片的各种属性)等,然后将各种配置项传递到下一步的步骤。

在此步骤中,目标节点将转移到指定的DOM分析方法。此过程将克隆目标节点及其子女,以获取节点的内容信息和样式信息。克隆DOM分析方法也相对复杂,因此我们不会在此处详细扩展它。获得目标节点后,您需要将克隆的目标节点DOM加载到一个,将其渲染一次,然后可以获取通过浏览器视图真正显示的节点样式。

在获得目标节点的样式和内容之后,其携带的数据信息需要转换为可以使用的数据类型。在目标节点的分析方法中,复发了整个DOM树,并获得了每一层节点的数据。对于每个节点,需要绘制的零件包括边框,背景,阴影和内容,而对于内容,它包括图片,文本,视频等。在整个分析过程中,目标节点的所有属性都会对其进行解析,并构造并转换为指定的数据格式。以下代码可以以基本数据格式看到:

class ElementContainer {  // 所有节点上的样式经过转换计算之后的信息  readonly styles: CSSParsedDeclaration;  // 节点的文本节点信息, 包括文本内容和其他属性  readonly textNodes: TextContainer[] = [];  // 当前节点的子节点  readonly elements: ElementContainer[] = [];  // 当前节点的位置信息(宽/高、横/纵坐标)  bounds: Bounds;  flags = 0;  ...}

图片,SVG,输入等特定元素也将具有自己的特定数据结构,并且不会在此处详细发布。

将目标节点处理为特定的数据结构后,必须将渲染方法组合起来。图纸需要计算上层应绘制哪些元素,并根据样式在下层绘制哪些元素。那么这是什么规则?在这里,我们涉及一些与CSS布局有关的知识。默认情况下,CSS是流式传输的,并且不会在元素之间重叠。但是,在某些情况下,这种流布局将被打破,例如使用float和()。因此,有必要确定与正常文档流分离的元素,并记住其级联信息以正确渲染它们。与普通文档流分离的元素形成堆叠的上下文。

根据W3C标准,在浏览器中渲染元素时,所有节点层次结构布局都需要遵循堆叠上下文和堆叠顺序的规则。具体规则如下:

在了解元素的渲染需要遵循此标准之后,在绘制节点时,您需要生成指定的级联数据。您需要首先计算整个目标节点中渲染子节点时显示的不同级别,并构建由与所有节点相对应的级联上下文表达的数据结构。特定的数据结构如下:

// 当前元素element: ElementPaint;// z-index为负, 形成层叠上下文negativeZIndex: StackingContext[];// z-index为0、auto、transform或opacity, 形成层叠上下文zeroOrAutoZIndexOrTransformedOrOpacity: StackingContext[];// 定位和z-index形成的层叠上下文positiveZIndex: StackingContext[];// 没有定位和float形成的层叠上下文nonPositionedFloats: StackingContext[];// 没有定位和内联形成的层叠上下文nonPositionedInlineLevel: StackingContext[];// 内联节点inlineLevel: ElementPaint[];// 不是内联的节点nonInlineLevel: ElementPaint[];

基于上述数据结构,将元素子节点分类并添加到指定的数组中。分析级联信息的方法类似于分析节点信息的方法。两者都递归整整棵树,收集树的每一层信息,形成包含级联信息的级联树。

基于上述两个步骤中构建的数据,您可以开始调用内部绘图方法来执行数据处理和绘图。使用节点的级联数据,根据浏览器渲染级联数据的规则,将DOM元素逐层渲染。核心源代码如下:

async renderStackContent(stack: StackingContext): Promise<void> {  if (contains(stack.element.container.flags, FLAGS.DEBUG_RENDER)) {  debugger;  }  // 1. the background and borders of the element forming the stacking context.  await this.renderNodeBackgroundAndBorders(stack.element);  // 2. the child stacking contexts with negative stack levels (most negative first).  for (const child of stack.negativeZIndex) {  await this.renderStack(child);  }  // 3. For all its in-flow, non-positioned, block-level descendants in tree order:  await this.renderNodeContent(stack.element);  for (const child of stack.nonInlineLevel) {  await this.renderNode(child);  }  // 4. All non-positioned floating descendants, in tree order. For each one of these,  // treat the element as if it created a new stacking context, but any positioned descendants and descendants  // which actually create a new stacking context should be considered part of the parent stacking context,  // not this new one.  for (const child of stack.nonPositionedFloats) {  await this.renderStack(child);  }  // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.  for (const child of stack.nonPositionedInlineLevel) {  await this.renderStack(child);  }  for (const child of stack.inlineLevel) {  await this.renderNode(child);  }  // 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:  // All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.  // For those with 'z-index: auto', treat the element as if it created a new stacking context,  // but any positioned descendants and descendants which actually create a new stacking context should be  // considered part of the parent stacking context, not this new one. For those with 'z-index: 0',  // treat the stacking context generated atomically.  //  // All opacity descendants with opacity less than 1  //  // All transform descendants with transform other than none  for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {  await this.renderStack(child);  }  // 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index  // order (smallest first) then tree order.  for (const child of stack.positiveZIndex) {  await this.renderStack(child);  }}

在该方法中,元素本身首先被调用并渲染。

然后处理每个分类的子元素。如果子元素形成堆叠上下文,则调用该方法,并且该方法继续在内部调用,这形成了整个堆叠环境树的递归。如果子元素是正常元素,并且不形成堆叠上下文,则将直接调用它,包括内容的两个部分,渲染节点内容和渲染节点边框背景颜色。

async renderNode(paint: ElementPaint): Promise<void> {  if (paint.container.styles.isVisible()) {    // 渲染节点的边框和背景色    await this.renderNodeBackgroundAndBorders(paint);    // 渲染节点内容    await this.renderNodeContent(paint);  }}

该方法是在元素节点中渲染内容,这可能是正常的元素,文本,图片,SVG,输入和不同的内容也将以不同的方式处理。

以上过程是整体内部过程。在理解一般原则之后,让我们看一下更详细的源代码流程图,以简单地摘要上述过程。

5。常见问题摘要

在使用过程中,将会有一些常见的问题和陷阱,这些问题总结如下:

(i)不完整的屏幕截图

要解决此问题,请在屏幕截图之前将页面滚动到顶部:

document.documentElement.scrollTop = 0;document.body.scrollTop = 0;

(ii)图片跨域

当插件请求图片时,将出现跨域情况。这是因为如果借助跨域资源,并且不使用CORS请求资源,则将被视为污染并可以正常显示,但是没有办法使用()或()来导出数据。有关详细信息,请参考:

解决方案:在IMG标签上设置,可以启用属性值,并且可以启用CROS请求。当然,此方法的前提是服务器的响应标头(允许)已设置为允许交叉域。如果图像本身的服务器端不支持跨域,则可以使用统一的转换来格式,如下所示。

function getUrlBase64_pro( len,url ) {  //图片转成base64  var canvas = document.createElement("canvas"); //创建canvas DOM元素  var ctx = canvas.getContext("2d");  return new Promise((reslove, reject) => {  var img = new Image();  img.crossOrigin = "Anonymous";  img.onload = function() {  canvas.height = len;  canvas.width = len;  ctx.drawImage(img, 0, 0, len, len);  var dataURL = canvas.toDataURL("image/");  canvas = null;  reslove(dataURL);  };  img.onerror = function(err){  reject(err)  }  img.src = url;  });}

(iii)屏幕截图与当前页面不同

方法1:如果要排除一些渲染,则可以将数据添加到这些元素中,从而将它们排除在渲染之外。例如,如果您不想进行屏幕截图,则可以如下:

html2canvas(ele,{useCORS: true,ignoreElements: (element: any) => {  if (element.tagName.toLowerCase() === 'iframe') {  return element;  }  return false;  },})

方法2:您可以将需要转换为一个节点的图片的零件,然后将整个节点的透明度设置为0,然后将其他部分设置为更高级别以实现屏幕截图指定的区域。

6。摘要

本文详细介绍并分析了两个开源库的使用和原理,并根据前端屏幕快照实现的实现方法详细详细介绍了dom-to-image和实现原则。

参考:

1.二像原则

2。原则的简要描述

3。浏览器端网页屏幕截图方案的详细说明

4。

5。实施浏览器屏幕截图的原理(包括用于源代码分析的一般方法)

Node 社群


本文链接:https://chahouw.com/detail/id/206443.html

提醒:请联系我时一定说明是从茶后生活网上看到的!