低成本打造一個帶寬無限的網站(四)
分塊處理
上一篇曾提到,我們可對資源加密存儲,然後在 SW 中進行解密。
理論上這當然可行,但事實上會出現一些問題:我們必須等整個資源下載完成後,才能開始解密操作。這對於用戶體驗,會產生很大的影響。
假如有個 1MB 的圖片,通過 100 KB/s 的速度加載,那麼要 10 秒後才能解密再展示;然而正常情況下,圖片是邊加載邊顯示的,並不會讓用戶等很久,然後一次性展示所有的。
為了解決這個問題,一個期待已久的新標準終於到來,那就是 Stream API。
有了流的支持,數據就可以漸進處理,而不必等待完整的。例如,我們使用 fetch 分塊讀取內容:
// fetch 分塊讀取演示
async function load(url) {
let res = await fetch(url);
console.log('response:', res);
let reader = res.body.getReader();
for (;;) {
let r = await reader.read();
if (r.done) {
break;
}
console.log('chunk:', r.value);
}
console.log('end');
}
load('https://raw.githubusercontent.com/EtherDream/_/master/pic.jpg');
同時,SW 也支持數據分塊輸出給下遊:
// SW 分塊輸出
let stream = new ReadableStream({
start(controller) {
...
input.ondata = function(chunk) {
controller.enqueue(chunk);
};
input.onend = function() {
controller.close();
};
...
}
});
let res = new Response(stream, ...);
...
兩者結合,我們就可以實現邊下載、邊解密、邊輸出的效果。於是對於加密的圖片、視頻等資源,也能循序漸進地展示了!
下載加速
除了解密、解壓縮等場合,數據流還可用於傳輸優化。例如,用戶下載大文件的場合。
由於免費空間單個節點的帶寬是有限的,因此下載速度不會太快。這時就可以通過 SW 做加速了 —— 我們同時從多個節點獲取相應的文件片段,然後依次輸出到響應流裏:
在用戶看來,這隻是瀏覽器默認的單線程下載,但事實上內部已通過 SW 加速,和傳統的多線程下載軟件並無本質區別!
當然,就算免費空間不支持 Range
請求也沒關係,我們可事先把大文件分成多個小文件上傳,然後分別加載即可。
動態加速
上一篇提到,通過 SW 可對故障節點「實時無縫」的切換。現在有了數據流,我們可將其發揮到極致,甚至能在傳輸的過程中進行調整。
例如,SW 默認選擇節點 1 加載資源,但發現速度沒有預期的那麼快,於是可增加節點 2 參與加速:
這樣,我們就能根據用戶的實際網絡情況,在端上動態調整,從而實現更智能的負載均衡!
插入腳本
有時候,我們希望給站點下所有頁麵的頭部插入一個 JS 腳本。
這個功能,如果沒有數據流支持的話,那麼 SW 必須得下載整個 HTML 才能修改;而現在,我們隻需改造最先返回的幾個 chunk 即可!
不過需要注意的是,chunk 是二進製層麵截斷的,因此可能把多字節字符截成兩半,導致出現亂碼。
為此,我們需要用「流模式」解碼字符串。例如:
// stream decode example
let dec = new TextDecoder();
let chunk1 = new Uint8Array([228, 189, 160, 229, 165]);
let chunk2 = new Uint8Array([189]);
dec.decode(chunk1, {stream: true}); // "你"
dec.decode(chunk2, {stream: true}); // "好"
如果 chunk 末尾的字符不完整,那麼不足的部分則被暫存在內部,下次解碼時會自動加在開頭。
這樣,我們就能用字符串方法,更方便地操作二進製數據了:
let dec = new TextDecoder();
let enc = new TextEncoder();
input.ondata = function(chunk) {
// 二進製 -> 字符串
let str = dec.decode(chunk, {stream: true});
// 插入腳本元素
str = str.replace(/<head/i, '<script ...><head');
// 字符串 -> 二進製
chunk = enc.encode(str);
...
};
當然,這裏的邏輯還有點瑕疵 —— 假如 <head
這個字符串正好跨越兩個 chunk,那就無法匹配到了。
由於 JS 不支持流模式的正則匹配,因此可以用個土辦法:如果 str 匹配不到,則截掉末尾 5 個字符,然後將尾巴暫存起來,拚到下一次的頭部。。。這樣雖然沒有流那麼嚴格,但實現簡單,並且也很高效。
此外,由於我們隻需替換一次,因此之後可跳過這步,無需解碼、匹配、編碼了。
小結
在數據流的配合下,SW 可實現非常豐富的玩法。不過目前隻有 Chrome 瀏覽器支持 Stream API,因此兼容性也是個較大的問題。相信隨著新標準的普及,今後使用前端加速的網站,一定會越來越多。
然而對於我們的「免費空間」來說,除了兼容性問題之外,還有 SW 的各種使用限製也是一個挑戰。因此如何繞過 SW 的使用限製,也是需要我們思考的。
最後更新:2017-11-09 11:34:53