探索前端黑科技——通過png圖的rgba值緩存數(shù)據(jù)
說起前端緩存,大部分人想到的無非是幾個常規(guī)的方案,比如cookie,localStorage,sessionStorage,或者加上indexedDB和webSQL,以及manifest離線緩存。除此之外,到底還有沒有別的方法可以進行前端的數(shù)據(jù)緩存呢?這篇文章將會帶你一起來探索,如何一步一步地通過png圖的rgba值來緩存數(shù)據(jù)的黑科技之旅。
原理
我們知道,通過為靜態(tài)資源設置Cache-Control和Expires響應頭,可以迫使瀏覽器對其進行緩存。瀏覽器在向后臺發(fā)起請求的時候,會先在自身的緩存里面找,如果緩存里面沒有,才會繼續(xù)向服務器請求這個靜態(tài)資源。利用這一點,我們可以把一些需要被緩存的信息通過這個靜態(tài)資源緩存機制來進行儲存。
那么我們如何把信息寫入到靜態(tài)資源中呢?canvas提供了.getImageData()方法和.createImageData()方法,可以分別用于讀取和設置圖片的rgba值。所以我們可以利用這兩個API進行信息的讀寫操作。
接下來看原理圖:
當靜態(tài)資源進入緩存,以后的任何對于該圖片的請求都會先查找本地緩存,也就是說信息其實已經(jīng)以圖片的形式被緩存到本地了。
注意,由于rgba值只能是[0, 255]之間的整數(shù),所以本文所討論的方法僅適用于純數(shù)字組成的數(shù)據(jù)。
靜態(tài)服務器
我們使用node搭建一個簡單的靜態(tài)服務器:
- const fs = require('fs')
- const http = require('http')
- const url = require('url')
- const querystring = require('querystring')
- const util = require('util')
- const server = http.createServer((req, res) => {
- let pathname = url.parse(req.url).pathname
- let realPath = 'assets' + pathname
- console.log(realPath)
- if (realPath !== 'assets/upload') {
- fs.readFile(realPath, "binary", function(err, file) {
- if (err) {
- res.writeHead(500, {'Content-Type': 'text/plain'})
- res.end(err)
- } else {
- res.writeHead(200, {
- 'Access-Control-Allow-Origin': '*',
- 'Content-Type': 'image/png',
- 'ETag': "666666",
- 'Cache-Control': 'public, max-age=31536000',
- 'Expires': 'Mon, 07 Sep 2026 09:32:27 GMT'
- })
- res.write(file, "binary")
- res.end()
- }
- })
- } else {
- let post = ''
- req.on('data', (chunk) => {
- post += chunk
- })
- req.on('end', () => {
- post = querystring.parse(post)
- console.log(post.imgData)
- res.writeHead(200, {
- 'Access-Control-Allow-Origin': '*'
- })
- let base64Data = post.imgData.replace(/^data:image\/\w+;base64,/, "")
- let dataBuffer = new Buffer(base64Data, 'base64')
- fs.writeFile('assets/out.png', dataBuffer, (err) => {
- if (err) {
- res.write(err)
- res.end()
- }
- res.write('OK')
- res.end()
- })
- })
- }
- })
- server.listen(80)
- console.log('Listening on port: 80')
這個靜態(tài)資源的功能很簡單,它提供了兩個功能:通過客戶端傳來的base64生成圖片并保存到服務器;設置圖片的緩存時間并發(fā)送到客戶端。
關鍵部分是設置響應頭:
- res.writeHead(200, {
- 'Access-Control-Allow-Origin': '*',
- 'Content-Type': 'image/png',
- 'ETag': "666666",
- 'Cache-Control': 'public, max-age=31536000',
- 'Expires': 'Mon, 07 Sep 2026 09:32:27 GMT'
- })
我們?yōu)檫@張圖片設置了一年的Content-Type和十年的Expires,理論上足夠長了。下面我們來進行客戶端的coding。
客戶端
- <!-- client.html -->
- <canvas id="canvas" width="8", height="1"></canvas>
假設我們需要存儲的是32位的數(shù)據(jù),所以我們?yōu)閏anvas設置寬度為8,高度為1。到底為什么32位數(shù)據(jù)對應長度為8,是因為每一個像素都有一個rgba,對應著red,green,blue和alpha4個數(shù)值,所以需要除以4。
- <!-- client.js -->
- let keyString = '01234567890123456789012345678901'
- let canvas = document.querySelector('#canvas')
- let ctx = canvas.getContext('2d')
- let imgData = ctx.createImageData(8, 1)
- for (let i = 0; i < imgData.data.length; i += 4) {
- imgData.data[i + 0] = parseInt(keyString[i]) + 50
- imgData.data[i + 1] = parseInt(keyString[i + 1]) + 100
- imgData.data[i + 2] = parseInt(keyString[i + 2]) + 150
- imgData.data[i + 3] = parseInt(keyString[i + 3]) + 200
- }
- ctx.putImageData(imgData, 0, 0)
首先我們假設需要被緩存的字符串為32位的01234567890123456789012345678901,然后我們使用.createImageData(8, 1)生成一個空白的imgData對象。接下來,我們對這個空對象進行賦值。為了實驗效果更加直觀,我們對rgba值都進行了放大。設置完了imgData以后,通過.putImageData()方法把它放入我們的canvas即可。
我們現(xiàn)在可以打印一下,看看這個imgData是什么:
- // console.log(imgData.data)
- [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并發(fā)送給服務器,同時接收服務器的響應,對圖片進行緩存:
- $.post('http://xx.xx.xx.xx:80/upload', { imgData: canvas.toDataURL() }, (data) => {
- if (data === 'OK') {
- let img = new Image()
- img.crossOrigin = "anonymous"
- img.src = 'http://xx.xx.xx.xx:80/out.png'
- img.onload = () => {
- console.log('完成圖片請求與緩存')
- ctx.drawImage(img, 0, 0)
- console.log(ctx.getImageData(0, 0, 8, 1).data)
- }
- }
- })
代碼很簡單,通過.toDataURL()方法把base64發(fā)送到服務器,服務器處理后生成圖片并返回,其圖片資源地址為http://xx.xx.xx.xx:80/out.png。在img.onload后,其實圖片就已經(jīng)完成了本地緩存了,我們在這個事件當中把圖片信息打印出來,作為和源數(shù)據(jù)的對比。
結果分析
開啟服務器,運行客戶端,***次加載的時候通過控制臺可以看到響應的圖片信息:
200 OK,證明是從服務端獲取的圖片。
關閉當前頁面,重新載入:
200 OK (from cache),證明是從本地緩存讀取的圖片。
接下來直接看rgba值的對比:
- 源數(shù)據(jù): [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]
- 緩存數(shù)據(jù):[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]
可以看到,源數(shù)據(jù)與緩存數(shù)據(jù)基本一致,在alpha值的誤差偏大,在rgb值內偶有誤差。通過分析,認為產生誤差的原因是服務端在進行base64轉buffer的過程中,所涉及的運算會導致數(shù)據(jù)的改變,這一點有待考證。
之前得到的結論,源數(shù)據(jù)與緩存數(shù)據(jù)存在誤差的原因,經(jīng)查證后確定為alpha值的干擾所致。如果我們把alpha值直接定為255,并且只把數(shù)據(jù)存放在rgb值內部,即可消除誤差。下面是改良后的結果:
- 源數(shù)據(jù): [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]
- 緩存數(shù)據(jù):[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而沒有把循環(huán)賦值的邏輯進行更新,所以第4n位的元數(shù)據(jù)被直接替換成了255,這個留著讀者自行修改有空再改……
綜上所述,這個利用png圖的rgba值緩存數(shù)據(jù)的黑科技,在理論上是可行的,但是在實際操作過程中可能還要考慮更多的影響因素,比如設法消除服務端的誤差,采取容錯機制等。實際上也是可行的。
值得注意的是,localhost可能默認會直接通過本地而不是服務器請求資源,所以在本地實驗中,可以通過設置header進行cors跨域,并且通過設置IP地址和80端口模擬服務器訪問。
后記
說是黑科技,其實原理非常簡單,與之類似的還有通過Etag等方法進行強緩存。研究的目的僅僅為了學習,千萬不要作為非法之用。如果讀者們發(fā)現(xiàn)這篇文章有什么錯漏之處,歡迎指正,也希望有興趣的朋友可以一起進行討論。