低成本打造一個帶寬無限的網站 —— No.4 數據流優化

分塊處理

上一篇曾提到,我們可對資源加密存儲,然後在 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');

演示:codepen.io/anon/pen/zPKrGX

同時,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 的使用限制,也是需要我們思考的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章