探索前端黑科技——通過 png 圖的 rgba 值快取資料

Jrain發表於2018-12-27

寫於 2016.09.09

本文系原創,歡迎轉載,轉載請註明作者資訊
專案地址:SphinxJS
線上體驗地址:jrainlau.github.io/sphinx/

說起前端快取,大部分人想到的無非是幾個常規的方案,比如cookielocalStoragesessionStorage,或者加上indexedDBwebSQL,以及manifest離線快取。除此之外,到底還有沒有別的方法可以進行前端的資料快取呢?這篇文章將會帶你一起來探索,如何一步一步地通過png圖的rgba值來快取資料的黑科技之旅。

PS:本文所研究的內容已經整合成一個開源的JS庫,名字叫SphinxJS,感興趣的同學可以移步到這篇文章SphinxJS——把字串編碼成png圖片的超輕量級開源庫去看相關的文件,歡迎STAR!

原理

我們知道,通過為靜態資源設定Cache-ControlExpires響應頭,可以迫使瀏覽器對其進行快取。瀏覽器在向後臺發起請求的時候,會先在自身的快取裡面找,如果快取裡面沒有,才會繼續向伺服器請求這個靜態資源。利用這一點,我們可以把一些需要被快取的資訊通過這個靜態資源快取機制來進行儲存。

那麼我們如何把資訊寫入到靜態資源中呢?canvas提供了.getImageData()方法和.createImageData()方法,可以分別用於讀取設定圖片的rgba值。所以我們可以利用這兩個API進行資訊的讀寫操作。

接下來看原理圖:

探索前端黑科技——通過 png 圖的 rgba 值快取資料

當靜態資源進入快取,以後的任何對於該圖片的請求都會先查詢本地快取,也就是說資訊其實已經以圖片的形式被快取到本地了。

注意,由於rgba值只能是[0, 255]之間的整數,所以本文所討論的方法僅適用於純數字組成的資料。

靜態伺服器

我們使用node搭建一個簡單的靜態伺服器:

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`)
複製程式碼

這個靜態資源的功能很簡單,它提供了兩個功能:通過客戶端傳來的base64生成圖片並儲存到伺服器;設定圖片的快取時間併傳送到客戶端。

關鍵部分是設定響應頭:

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`
})
複製程式碼

我們為這張圖片設定了一年的Content-Type和十年的Expires,理論上足夠長了。下面我們來進行客戶端的coding。

客戶端

<!-- client.html -->

<canvas id="canvas" width="8", height="1"></canvas>
複製程式碼

假設我們需要儲存的是32位的資料,所以我們為canvas設定寬度為8,高度為1。到底為什麼32位資料對應長度為8,是因為每一個畫素都有一個rgba,對應著redgreenbluealpha4個數值,所以需要除以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即可。

我們現在可以列印一下,看看這個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併傳送給伺服器,同時接收伺服器的響應,對圖片進行快取:

$.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傳送到伺服器,伺服器處理後生成圖片並返回,其圖片資源地址為http://xx.xx.xx.xx:80/out.png。在img.onload後,其實圖片就已經完成了本地快取了,我們在這個事件當中把圖片資訊列印出來,作為和源資料的對比。

結果分析

開啟伺服器,執行客戶端,第一次載入的時候通過控制檯可以看到響應的圖片資訊:

探索前端黑科技——通過 png 圖的 rgba 值快取資料

200 OK,證明是從服務端獲取的圖片。

關閉當前頁面,重新載入:

探索前端黑科技——通過 png 圖的 rgba 值快取資料

200 OK (from cache),證明是從本地快取讀取的圖片。

接下來直接看rgba值的對比:

源資料:  [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]

快取資料:[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值內部,即可消除誤差。下面是改良後的結果:

源資料:  [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]

快取資料:[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等方法進行強快取。研究的目的僅僅為了學習,千萬不要作為非法之用。如果讀者們發現這篇文章有什麼錯漏之處,歡迎指正,也希望有興趣的朋友可以一起進行討論。

感謝你的閱讀。我是Jrain,歡迎關注我的專欄,將不定期分享自己的學習體驗,開發心得,搬運牆外的乾貨。下次見啦!

相關文章