探索前端黑科技——通过png图的rgba值缓存数据

简介:

说起前端缓存,大部分人想到的无非是几个常规的方案,比如cookie,localStorage,sessionStorage,或者加上indexedDB和webSQL,以及manifest离线缓存。除此之外,到底还有没有别的方法可以进行前端的数据缓存呢?这篇文章将会带你一起来探索,如何一步一步地通过png图的rgba值来缓存数据的黑科技之旅。

原理

我们知道,通过为静态资源设置Cache-Control和Expires响应头,可以迫使浏览器对其进行缓存。浏览器在向后台发起请求的时候,会先在自身的缓存里面找,如果缓存里面没有,才会继续向服务器请求这个静态资源。利用这一点,我们可以把一些需要被缓存的信息通过这个静态资源缓存机制来进行储存。

那么我们如何把信息写入到静态资源中呢?canvas提供了.getImageData()方法和.createImageData()方法,可以分别用于读取和设置图片的rgba值。所以我们可以利用这两个API进行信息的读写操作。

接下来看原理图:

当静态资源进入缓存,以后的任何对于该图片的请求都会先查找本地缓存,也就是说信息其实已经以图片的形式被缓存到本地了。

注意,由于rgba值只能是[0, 255]之间的整数,所以本文所讨论的方法仅适用于纯数字组成的数据。

静态服务器

我们使用node搭建一个简单的静态服务器:

 
  1. const fs = require('fs'
  2. const http = require('http'
  3. const url = require('url'
  4. const querystring = require('querystring'
  5. const util = require('util'
  6.  
  7. const server = http.createServer((req, res) => { 
  8.   let pathname = url.parse(req.url).pathname 
  9.   let realPath = 'assets' + pathname 
  10.   console.log(realPath) 
  11.   if (realPath !== 'assets/upload') { 
  12.      fs.readFile(realPath, "binary"function(err, file) { 
  13.       if (err) { 
  14.         res.writeHead(500, {'Content-Type''text/plain'}) 
  15.         res.end(err) 
  16.       } else { 
  17.         res.writeHead(200, { 
  18.           'Access-Control-Allow-Origin''*'
  19.           'Content-Type''image/png'
  20.           'ETag'"666666"
  21.           'Cache-Control''public, max-age=31536000'
  22.           'Expires''Mon, 07 Sep 2026 09:32:27 GMT' 
  23.         }) 
  24.         res.write(file, "binary"
  25.         res.end() 
  26.       } 
  27.    }) 
  28.   } else { 
  29.     let post = '' 
  30.     req.on('data', (chunk) => { 
  31.       post += chunk 
  32.     }) 
  33.     req.on('end', () => { 
  34.       post = querystring.parse(post) 
  35.       console.log(post.imgData) 
  36.       res.writeHead(200, { 
  37.         'Access-Control-Allow-Origin''*' 
  38.       }) 
  39.       let base64Data = post.imgData.replace(/^data:image\/\w+;base64,/, ""
  40.       let dataBuffer = new Buffer(base64Data, 'base64'
  41.       fs.writeFile('assets/out.png', dataBuffer, (err) => { 
  42.         if (err) { 
  43.           res.write(err) 
  44.           res.end() 
  45.         } 
  46.         res.write('OK'
  47.         res.end() 
  48.       }) 
  49.     }) 
  50.   } 
  51. }) 
  52.  
  53. server.listen(80) 
  54.  
  55. console.log('Listening on port: 80')  

这个静态资源的功能很简单,它提供了两个功能:通过客户端传来的base64生成图片并保存到服务器;设置图片的缓存时间并发送到客户端。

关键部分是设置响应头:

 
  1. res.writeHead(200, { 
  2.   'Access-Control-Allow-Origin''*'
  3.   'Content-Type''image/png'
  4.   'ETag'"666666"
  5.   'Cache-Control''public, max-age=31536000'
  6.   'Expires''Mon, 07 Sep 2026 09:32:27 GMT' 
  7. })  

我们为这张图片设置了一年的Content-Type和十年的Expires,理论上足够长了。下面我们来进行客户端的coding。

客户端

 
  1. <!-- client.html --> 
  2.  
  3. <canvas id="canvas" width="8", height="1"></canvas>  

假设我们需要存储的是32位的数据,所以我们为canvas设置宽度为8,高度为1。到底为什么32位数据对应长度为8,是因为每一个像素都有一个rgba,对应着red,green,blue和alpha4个数值,所以需要除以4。

 
  1. <!-- client.js --> 
  2.  
  3. let keyString = '01234567890123456789012345678901' 
  4.          
  5. let canvas = document.querySelector('#canvas'
  6. let ctx = canvas.getContext('2d'
  7.  
  8. let imgData = ctx.createImageData(8, 1) 
  9.  
  10. for (let i = 0; i < imgData.data.length; i += 4) { 
  11.     imgData.data[i + 0] = parseInt(keyString[i]) + 50 
  12.     imgData.data[i + 1] = parseInt(keyString[i + 1]) + 100 
  13.     imgData.data[i + 2] = parseInt(keyString[i + 2]) + 150 
  14.     imgData.data[i + 3] = parseInt(keyString[i + 3]) + 200 
  15.  
  16. ctx.putImageData(imgData, 0, 0)  

首先我们假设需要被缓存的字符串为32位的01234567890123456789012345678901,然后我们使用.createImageData(8, 1)生成一个空白的imgData对象。接下来,我们对这个空对象进行赋值。为了实验效果更加直观,我们对rgba值都进行了放大。设置完了imgData以后,通过.putImageData()方法把它放入我们的canvas即可。

我们现在可以打印一下,看看这个imgData是什么:

 
  1. // console.log(imgData.data) 
  2.  
  3. [50, 101, 152, 203, 54, 105, 156, 207, 58, 109, 150, 201, 52, 103, 154, 205, 56, 107, 158, 209, 50, 101, 152, 203, 54, 105, 156, 207, 58, 109, 150, 201] 

接下来,我们要把这个canvas编译为一张图片的base64并发送给服务器,同时接收服务器的响应,对图片进行缓存:

 
  1. $.post('http://xx.xx.xx.xx:80/upload', { imgData: canvas.toDataURL() }, (data) => { 
  2.     if (data === 'OK') { 
  3.         let img = new Image() 
  4.         img.crossOrigin = "anonymous" 
  5.         img.src = 'http://xx.xx.xx.xx:80/out.png' 
  6.         img.onload = () => { 
  7.             console.log('完成图片请求与缓存'
  8.             ctx.drawImage(img, 0, 0) 
  9.             console.log(ctx.getImageData(0, 0, 8, 1).data) 
  10.         } 
  11.     } 
  12. })  

代码很简单,通过.toDataURL()方法把base64发送到服务器,服务器处理后生成图片并返回,其图片资源地址为http://xx.xx.xx.xx:80/out.png。在img.onload后,其实图片就已经完成了本地缓存了,我们在这个事件当中把图片信息打印出来,作为和源数据的对比。

结果分析

开启服务器,运行客户端,第一次加载的时候通过控制台可以看到响应的图片信息:

200 OK,证明是从服务端获取的图片。

关闭当前页面,重新载入:

200 OK (from cache),证明是从本地缓存读取的图片。

接下来直接看rgba值的对比:

 
  1. 源数据:  [50, 101, 152, 203, 54, 105, 156, 207, 58, 109, 150, 201, 52, 103, 154, 205, 56, 107, 158, 209, 50, 101, 152, 203, 54, 105, 156, 207, 58, 109, 150, 201] 
  2.  
  3. 缓存数据:[50, 100, 152, 245, 54, 105, 157, 246, 57, 109, 149, 244, 52, 103, 154, 245, 56, 107, 157, 247, 50, 100, 152, 245, 54, 105, 157, 246, 57, 109, 149, 244]  

可以看到,源数据与缓存数据基本一致,在alpha值的误差偏大,在rgb值内偶有误差。通过分析,认为产生误差的原因是服务端在进行base64转buffer的过程中,所涉及的运算会导致数据的改变,这一点有待考证。

之前得到的结论,源数据与缓存数据存在误差的原因,经查证后确定为alpha值的干扰所致。如果我们把alpha值直接定为255,并且只把数据存放在rgb值内部,即可消除误差。下面是改良后的结果:

 
  1. 源数据: [0, 1, 2, 255, 4, 5, 6, 255, 8, 9, 0, 255, 2, 3, 4, 255, 6, 7, 8, 255, 0, 1, 2, 255, 4, 5, 6, 255, 8, 9, 0, 255] 
  2.  
  3. 缓存数据:[0, 1, 2, 255, 4, 5, 6, 255, 8, 9, 0, 255, 2, 3, 4, 255, 6, 7, 8, 255, 0, 1, 2, 255, 4, 5, 6, 255, 8, 9, 0, 255]  

因为我懒,只是把alpha值给定为255而没有把循环赋值的逻辑进行更新,所以第4n位的元数据被直接替换成了255,这个留着读者自行修改有空再改……

综上所述,这个利用png图的rgba值缓存数据的黑科技,在理论上是可行的,但是在实际操作过程中可能还要考虑更多的影响因素,比如设法消除服务端的误差,采取容错机制等。实际上也是可行的。

值得注意的是,localhost可能默认会直接通过本地而不是服务器请求资源,所以在本地实验中,可以通过设置header进行cors跨域,并且通过设置IP地址和80端口模拟服务器访问。

后记

说是黑科技,其实原理非常简单,与之类似的还有通过Etag等方法进行强缓存。研究的目的仅仅为了学习,千万不要作为非法之用。如果读者们发现这篇文章有什么错漏之处,欢迎指正,也希望有兴趣的朋友可以一起进行讨论。


作者:jrainlau

来源:51CTO

相关文章
|
4月前
|
XML 算法 Java
Android App开发之位图加工Bitmap中转换位图的像素色彩、裁剪内部区域、利用矩阵变换位图的讲解及实战(附源码和演示)
Android App开发之位图加工Bitmap中转换位图的像素色彩、裁剪内部区域、利用矩阵变换位图的讲解及实战(附源码和演示)
29 0
|
12月前
|
小程序 容器
【微信小程序】image组件的4种缩放模式与9种裁剪模式
假设有一个容器(这个容器的宽高就是设置的样式),要将图片放进去。而aspectFit的特点就是保持图片不变形,且容器要“刚好”将这个图片装进去。如果原始图片比容器大,就要被等比例缩小;如果原始图片比容器小,就会被等比例放大。一直放大或缩小到图片的某一条边刚好和容器的一条边重合。
1059 0
|
存储 Swift
Swift - Cell自适应+代码约束(SnapKit)横竖屏支持平铺+根据URL获取图片size
Swift - Cell自适应+代码约束(SnapKit)横竖屏支持平铺+根据URL获取图片size
179 0
wx.chooseImage 的sizeType 拿到压缩图和原图数据
我的理解应该是:如果sizeType为:['original', 'compressed'] 时,那 tempFilePaths 就应该是返回 2个路径才对,一个是原图的路径,一个是压缩图的路径,但实际上只返回了一个路径,那请问,这个返回的路径是原图的,还是压缩图的?
171 0
wx.chooseImage 的sizeType 拿到压缩图和原图数据
|
测试技术 Android开发 数据格式
|
算法 图形学
Unity图片优化-Dither算法(RGBA16优化)
在unity开发过程中,游戏图片占用了很大一部分的手机内存。所以在游戏开发中,对图片的优化也至关重要。 在Unity中常用的的图片格式有RGBA32,RGBA16,ETC,PVRTC等。这里我们主要讨论带透明通道的RGBA32和RGBA16这两种格式的图片在Unity占用的内存空间已经优化方案。
1861 0
|
iOS开发
使用AutoLayout约束, 为啥图片的大小(Image size)却还以实际大小显示?
问题 给一个 UIImageView 设置一张图片时,使用 AutoLayout 给 UIImageView 约束宽高,但是实际显示的大小,图片以实际的大小显示出来,代码也没有设置 frame,设置contentMode为UIViewContentModeScaleAspectFit 也不起作用。
1049 0
|
数据采集 存储 传感器
【Android RTMP】x264 图像数据编码 ( NV21 格式中的 YUV 数据排列 | Y 灰度数据拷贝 | U 色彩值数据拷贝 | V 饱和度数据拷贝 | 图像编码操作 )
【Android RTMP】x264 图像数据编码 ( NV21 格式中的 YUV 数据排列 | Y 灰度数据拷贝 | U 色彩值数据拷贝 | V 饱和度数据拷贝 | 图像编码操作 )
327 0
|
前端开发 Android开发
【Android 应用开发】Paint 渲染 之 BitmapShader 位图渲染 ( 渲染流程 | CLAMP 拉伸最后像素 | REPEAT 重复绘制图片 | MIRROR 绘制反向图片 )(一)
【Android 应用开发】Paint 渲染 之 BitmapShader 位图渲染 ( 渲染流程 | CLAMP 拉伸最后像素 | REPEAT 重复绘制图片 | MIRROR 绘制反向图片 )(一)
362 0
|
Android开发
【Android 应用开发】Paint 渲染 之 BitmapShader 位图渲染 ( 渲染流程 | CLAMP 拉伸最后像素 | REPEAT 重复绘制图片 | MIRROR 绘制反向图片 )(二)
【Android 应用开发】Paint 渲染 之 BitmapShader 位图渲染 ( 渲染流程 | CLAMP 拉伸最后像素 | REPEAT 重复绘制图片 | MIRROR 绘制反向图片 )(二)
247 0
【Android 应用开发】Paint 渲染 之 BitmapShader 位图渲染 ( 渲染流程 | CLAMP 拉伸最后像素 | REPEAT 重复绘制图片 | MIRROR 绘制反向图片 )(二)