資源合併
前端開發者都知道,過多的請求對性能影響很大。而且有些 CDN 不僅按流量收費,請求數也收費,如果網頁裏有大量小文件,顯然不划算。
爲此不少開發者將零碎的小文件進行合併優化,例如 JS/CSS 合併在一起,圖片合併成精靈圖等。
不過傳統的合併方式有一定的侷限性,只能合併同類型的文件。例如 JS、CSS 等文本格式的數據可以合併,但 JS 和圖片顯然無法合併,畢竟一個是文本格式,一個是二進制格式。而且合併過程需對現有資源進行修改,最終發佈的文件與原始文件差異很大。
有沒有什麼辦法,可將任何類型的資源併成在一起,並且不改變原始文件?
類似方案
Google 推出了一個 Web Bundles
方案,可將任意類型的資源打包成一個文件:
細節可參考:https://web.dev/web-bundles/
不過 Web Bundles
注重的是離線分享。如文中所提到,在沒有網絡的飛機上,可將網頁小遊戲通過單個文件的方式分享給旁邊的人一起玩。
演示可見,通過本地文件打開的網站仍保留原始 URL。
由於 Web Bundles
目前仍未正式啓用,需在 flags 中手動開啓,因此該方案仍無法解決本文提出的問題。
通用方案
事實上,我們大可不必關心資源類型,將所有文件都當做二進制文件合併在一起,運行時再通過 JS 提取。
但是,網頁裏的資源引用的仍是原始 URL,例如 <img src="a.gif">
。怎樣才能讓網頁使用 JS 提供的數據,而不是從原始 URL 加載?
這就需要藉助 HTML5 的一個黑科技 —— Service Worker。它能攔截網頁產生的 HTTP 請求,並能控制返回內容。這樣即可實現所有資源都從單個文件中提取!
初始化
既然要調用 Service Worker,那麼是否得修改現有的 HTML 文件,在其中添加腳本?
事實上不需要!用戶首次訪問時,無論訪問哪個路徑,後端都返回 Service Worker 安裝頁;安裝完成後頁面自動刷新,這時請求即可被 Service Worker 攔截,從而使用資源包中的 HTML 文件。
至於實現其實很簡單,使用 404.html
即可!
免費空間
雖然我們將資源請求數降低到只有 1 個,但流量仍然是存在的。並且任何一個資源更新都得重新下載整個資源包,導致流量成本進一步增長。
有沒有什麼辦法,可大幅降低流量成本?很簡單,使用免費 CDN 即可。你可將資源包發佈 GitHub、NPM 等空間,然後通過 jsdelivr、unpkg 等免費 CDN 加速。
這樣,你的網站只需提供 404.html
和 sw.js 兩個極小的文件即可,其他所有內容都可從免費空間獲取!
演示站點:https://fanhtml5.github.io/
原始文件:https://github.com/fanhtml5/test-site (多個文件,總共數 MB)
發佈文件:https://github.com/fanhtml5/fanhtml5.github.io (只有兩個,壓縮後不到 2kB)
圖片空間
類似 jsdelivr、unpkg 這麼好用的免費 CDN 並不多,用在這裏太過浪費,作爲開發者也不建議過度使用它們。
我們可使用更低廉更廣泛的免費空間 —— 各大網站的貼圖相冊,例如知乎、B 站、簡書等文章的貼圖,它們不僅支持 CORS,而且允許空 referrer,完全可用於存儲數據。
參照之前寫的《利用 canvas 實現數據壓縮》文章,我們可將原始數據編碼成圖片像素,從而可將任意文件寫入圖片並上傳到相冊;運行時再解碼還原,將原始文件寫入 Storage Cache 供 Service Worker 使用。
至於穩定性,可將圖片上傳到多個站點作冗餘。如果加載失敗或 Hash 不正確則使用下一個備用圖片,從而大幅提升穩定性和安全性。
隱私保護
爲了儘可能減少隱私泄露,同時防止外鏈限制,我們可通過 referrer-policy
對 referrer 進行隱藏。例如:
var img = new Image()
img.crossOrigin = true
img.referrerPolicy = 'no-referrer'
img.src = 'https://pic3.zhimg.com/80/v2-a492fc0204ad0275e0b609ceac2dab10.png'
這樣請求中就沒有 Referer
頭了。
不過需注意的是,爲了能讀取圖片中的像素數據,必須使用 CORS 模式,即設置 crossOrigin
屬性。這種模式下請求會出現 Origin
頭。雖然大部分網站不會使用該頭限制外鏈,但它會泄露你的站點域名。這仍不完美。
爲了能隱藏 Origin
請求頭,這裏使用一種簡單古老但有效的黑科技 —— 使用無源的頁面加載圖片,例如通過 Data URI 創建的 iframe:
var iframe = document.createElement('iframe')
iframe.src = `data:text/html,
<script>
var img = new Image()
img.crossOrigin = true
img.referrerPolicy = 'no-referrer'
img.src = 'https://pic3.zhimg.com/80/v2-a492fc0204ad0275e0b609ceac2dab10.png'
</script>
`
document.body.appendChild(iframe)
這個方案雖然無法讓 Origin
請求頭消失,但可將其設置爲 null
,從而保護你的站點域名不被泄露。
演示
基於上述思路,這裏實現了一個簡單的工具,暫且稱之 web2img
。
GitHub: https://github.com/EtherDream/web2img
工具演示: