最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行!我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的,微信截图、Snipaste都可以做到的,自己实现的话,一是比较麻烦,而是性能也不会很好,没有必要,把更多的时间放在核心业务更合理!
我一听这产品小嘴巴巴的说的还挺有道理,没有办法,只能接了这个需求,从此命运的齿轮开始转动,开始了我漫长而又曲折的思考。
<!DOCTYPE html> <html> <head> <title>堆代码 duidaima.com</title> </head> <body> <canvas id="myCanvas" width="400" height="400"></canvas> <br> <button onclick="cropImage()">截取图片部分</button> <br> <img id="croppedImage" alt="截取的图片部分"> <br> <script> function cropImage() { var canvas = document.getElementById('myCanvas'); var ctx = canvas.getContext('2d'); var image = new Image(); image.onload = function () { // 在canvas上绘制整张图片 ctx.drawImage(image, 0, 0, canvas.width, canvas.height); // 截取图片的一部分,这里示例截取左上角的100x100像素区域 var startX = 0; var startY = 0; var width = 100; var height = 100; var croppedData = ctx.getImageData(startX, startY, width, height); // 创建一个新的canvas用于显示截取的部分 var croppedCanvas = document.createElement('canvas'); croppedCanvas.width = width; croppedCanvas.height = height; var croppedCtx = croppedCanvas.getContext('2d'); croppedCtx.putImageData(croppedData, 0, 0); // 将截取的部分显示在页面上 var croppedImage = document.getElementById('croppedImage'); croppedImage.src = croppedCanvas.toDataURL(); }; // 设置要加载的图片 image.src = 'your_image.jpg'; // 替换成你要截取的图片的路径 } </script> </body> </html>1、获取像素的思路
但是目前的这个需求远不止这样简单,因为它的对象是整个document,需要在整个document上截取一部分,我思考了一下,其实假设如果浏览器为我们提供了一个api,能够获取到某个位置的像素信息就好了,这样我将选定的某个区域的每个像素信息获取到,然后在一个像素一个像素绘制到canvas上就好了。
<!DOCTYPE html> <html> <head> <title>堆代码 duidaima.com</title> </head> <body> <canvas id="myCanvas" width="400" height="400"></canvas> <br> <button onclick="getPixelInfo()">获取特定像素信息</button> <br> <div id="pixelInfo"></div> <script> function getPixelInfo() { var canvas = document.getElementById('myCanvas'); var ctx = canvas.getContext('2d'); // 绘制一些内容到canvas ctx.fillStyle = 'red'; ctx.fillRect(50, 50, 100, 100); // 获取特定位置的像素信息 var x = 75; // 替换为你想要获取的像素的x坐标 var y = 75; // 替换为你想要获取的像素的y坐标 var pixelData = ctx.getImageData(x, y, 1, 1).data; // 提取像素的颜色信息 var red = pixelData[0]; var green = pixelData[1]; var blue = pixelData[2]; var alpha = pixelData[3]; // 将信息显示在页面上 var pixelInfo = document.getElementById('pixelInfo'); pixelInfo.innerHTML = '在位置 (' + x + ', ' + y + ') 的像素信息:<br>'; pixelInfo.innerHTML += '红色 (R): ' + red + '<br>'; pixelInfo.innerHTML += '绿色 (G): ' + green + '<br>'; pixelInfo.innerHTML += '蓝色 (B): ' + blue + '<br>'; pixelInfo.innerHTML += 'Alpha (透明度): ' + alpha + '<br>'; } </script> </body> </html>
浏览器之所以没有为我们提供相应的API获取像素信息,停下来想想也是有道理的,甚至是必要的,因为假设浏览器为我们提供了这个API,那么恶意程序就可以通过这个API,不断的获取你的浏览器页面像素信息,然后全部绘制出来。一旦你的浏览器运行这个段恶意程序,那么你在浏览器干的什么,它会一览无余,相当于在网络的世界里裸奔,毫无隐私可言。
既然不能走捷径直接拿取像素信息,那就得老老实实的把document转换为图片,然后调用canvas的drawImage这个方法来截取图片了。在前端领域其实99%的业务场景早已被之前的大佬们都实现过了,相应的轮子也很多。我问了一下chatGPT,它立马给我推荐了大名鼎鼎的html2canvas,这个库能够很好的将任意的dom转化为canvas。这个是它的官网。
html2canvas(document.body).then(function(canvas) { // 将 Canvas 转换为图片数据URL var src = canvas.toDataURL("image/png"); var image = new Image(); image.src = src; image.onload = ()=>{ const canvas = document.createElement("canvas") const ctx = canvas.getContext("2d"); const width = 100; const height = 100; canvas.width = width; canvas.height = height; // 截取以(10,10)为顶点,长为100,宽为100的区域 ctx.drawImage(image, 10, 10, width, height , 0 , 0 ,width , height); } });上面这段代码就可以实现截取document的特定的某个区域,需求已经实现了,但是我看了一下这个html2canvas库的资源发现并没有那么简单,有两个点并不满足我希望实现的点:
要知道整个react和react-dom的包压缩过后也才不到150kb,因此在项目只为了一个单一的功能引入一个复杂的资源可能并不划算,引入一个复杂度高的包一个是它会增加构建的时间,另一方面也会增加打包之后的体积。
如果是普通的web工程可能情有可原,但是因为我会将这需求做到插件当中,插件和普通的web不一样的一点,就是web工程如果更新之后,客户端是自动更新的。但是插件如果更新了,需要客户端手动的下载插件包,然后再在浏览器安装,因此包的大小尽可能小才好,如果一个插件好几十MB的话,那客户端肯定烦死了。
作为业内知名的html2canvas库,性能方面表现如何呢?我们可以看看它的原理,当html2canvas拿到dom结构之后,首先为了避免副作用给原dom造成了影响,它会克隆一份全新的dom,然后遍历DOM的每一个节点,将其扁平化,这个过程中会收集每个节点的样式信息,尤其是在界面上的布局的几何信息,存入一个栈中。
整个过程其实需要至少3次对整个dom树的遍历才可以绘制出来一个canvas的实例。
要想解决以上的大小的瓶颈。
因此相关的代码资源不能够动态加载。
<!DOCTYPE html> <html> <head> <title>渲染SVG</title> </head> <body> <h1>SVG示例</h1> <img src="example.svg" alt="SVG示例"> </body> </html>但是也可以是这样的方式:
<!DOCTYPE html> <html> <head> <title>渲染SVG字符串</title> </head> <body> <div id="svg-container"> <!-- 这里是将SVG内容渲染到<img>标签中 --> <img id="svg-image" src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /></svg>" alt="SVG图像"> </div> </body> </html>把svg的标签序列化之后直接放在src属性上,image也是可以成功解析的,只不过我们需要添加一个头部:data:image/svg+xml, 。
<!DOCTYPE html> <html> <head> <title>渲染SVG字符串</title> </head> <body> <div id="svg-container"> <!-- 这里是将SVG内容渲染到<img>标签中 --> <img id="svg-image" src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /><foreignObject>{ 中间可以放 dom序列化后的结果呀 }</foreignObject></svg>" alt="SVG图像" > </div> </body> </html>所以我们可以将dom序列化后的结构插到svg中,这不就天然的形成了一种dom->image的效果么?下面是演示的效果:
<!DOCTYPE html> <html> <head> <title>渲染SVG字符串</title> </head> <body> <div id="render" style="width: 100px; height: 100px; background: red"></div> <br /> <div id="svg-container"> <!-- 这里是将SVG内容渲染到<img>标签中 --> <img id="svg-image" alt="SVG图像" /> </div> <script> const perfix = "data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'>"; const surfix = "</foreignObject></svg>"; const render = document.getElementById("render"); render.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); const string = new XMLSerializer() .serializeToString(render) .replace(/#/g, "%23") .replace(/\n/g, "%0A"); const image = document.getElementById("svg-image"); const src = perfix + string + surfix; console.log(src); image.src = src; </script> </body> </html>如果你将这个字符串直接通过浏览器打开,也是可以的,说明浏览器可以直接识别这种形式的媒体资源正确解析对应的资源:
data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'><div id="render" style="width: 100px; height: 100px; background: red" xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>实不相瞒这个就是dom-to-image的核心原理,性能肯定是不错的,因为它是调用浏览器底层的渲染器。
通过这个dom-to-image我们可以很好的解决资源大小和性能这两个瓶颈的点。