Service Worker 實現(xiàn)網(wǎng)頁離線可用

離線可用:

  • 在無網(wǎng)的情況下可以訪問叹谁, 甚至使用部分功能饲梭, 而不是展示"無網(wǎng)絡(luò)連接"的錯誤頁面。
  • 在弱網(wǎng)的情況下焰檩, 能使用緩存快速訪問我們的頁面憔涉,提升體驗。
  • 在正常網(wǎng)絡(luò)的情況下析苫, 可以通過自發(fā)各種控制的緩存來節(jié)省請求寬帶兜叨。
  • ......
    而這一切,其實都要歸功于PWA背后的英雄 —— Service Worker衩侥。

Service Worker可以簡單的理解為一個獨立于前端頁面国旷,在后臺運行的進(jìn)程(Worker)。因此茫死, 它不會阻塞瀏覽器腳本運行跪但, 同事也無法直接訪問瀏覽器相關(guān)的API(例如DOM、localStorage等)璧榄。此外,即便是離開頁面吧雹, 甚至是關(guān)閉瀏覽器后骨杂, 它仍然可以運行。 它就是web應(yīng)用后默默工作的小蜜蜂雄卷, 處理著緩存搓蚪、推送、通知與同步等工作丁鹉,如果學(xué)習(xí)PWA妒潭,也繞不開Service Worker蒜胖。

Service Worker是如何實現(xiàn)離線可用的风题?

當(dāng)訪問一個web網(wǎng)站時, 我們實際上做了些什么? 總體上來說纠亚, 我們通過與服務(wù)器建立連接, 獲取資源胃夏, 然后獲取到的部分資源還會去請求新的資源(例如html中使用的css,js等)蹬铺。粗粒度來說, 我們訪問一個網(wǎng)站匈庭,就是在獲取/訪問這些資源夫凸。
可想而知, 當(dāng)處于離線或弱網(wǎng)環(huán)境時阱持, 我們無法有效訪問這些資源夭拌, 這就是制約我們的關(guān)鍵因素。因此一個直觀的思路就是: 如果我們把這些資源緩存起來衷咽,在某些情況下鸽扁,將網(wǎng)絡(luò)請求變?yōu)楸镜卦L問,是否能解決這一問題兵罢?這時就需要有個本地cache, 可以靈活的將各類資源進(jìn)行本地存取献烦。


有了本地緩存的cache還不夠, 還需要有效的使用緩存卖词, 更新緩存與清除緩存巩那, 進(jìn)一步應(yīng)用各種個性化的緩存策略。 而這就需要有個能夠控制緩存的woker,也就是 Service Worker的部分工作之一此蜈。
Service Worker有一個非常重要的特性: 你可以在Service Worker中監(jiān)聽所有的客戶端(Web)發(fā)出的請求即横, 然后通過Service Worker來代理, 向服務(wù)器發(fā)起請求裆赵。 通過監(jiān)聽用戶請求信息东囚, Service Worker可以決定是都使用緩存來作為Web請求的返回。
普通 網(wǎng)頁與 添加了Service Worker的網(wǎng)頁對比圖如下:

  • 普通網(wǎng)頁


  • 添加Service Worker


注意: 圖中雖然將瀏覽器战授、SW(Service Worker)與后端服務(wù)三者并列放置了页藻, 但實際上瀏覽器和SW都是運行在本機上的, 所以這個場景下的SW類似一個"客戶端代理"植兰。

如何使用Service Worker實現(xiàn)離線可用

1. 注冊Service Worker
在index.js來注冊Service Worker(sw.js)

//注冊Service Worker份帐,腳本為sw.js
    if('serviceWorker' in navigator) {
        navigator.serviceWorker.register('./sw.js').then(function () {
            console.log("service Worker 注冊成功");
        });
    }

Service Worker的各類操作都被設(shè)計為異步, 用以避免一些長時間的阻塞操作楣导。 這些API都是以Promise 的形式來調(diào)用的废境。 所以在接下來的各段代碼中不斷會看到Promise的使用。
2. Service Worker的聲明周期
當(dāng)我們注冊Service Worker后, 它會經(jīng)歷生命周期的各個階段噩凹, 同時會觸發(fā)響應(yīng)的事件巴元。 整個生命周期包括了: installing -> installed -> activated -> redundant。 當(dāng)Service Worker安裝(installed)完畢后驮宴, 會觸發(fā)install事件逮刨;而激活(activated)后, 則會觸發(fā)activate事件幻赚。


寫個例子監(jiān)聽install事件:

//監(jiān)聽install
self.addEventListener('install', function () {
    console.log("Service Worker當(dāng)前狀態(tài):install");
});

self是Service Worker中一個特的全局變量禀忆,類似于我們常見的Window對象。 self引用了當(dāng)前這個Service Worker落恼。

3. 緩存靜態(tài)資源
要使網(wǎng)頁離線可用箩退, 就需要將所需資源緩存下來。我們需要一個資源列表佳谦, 當(dāng)Service Worker被激活時戴涝, 會將該列表內(nèi)的資源存進(jìn)cache。在sw.js中:

//創(chuàng)建一個cacheName
const cacheName = 'cache-0-1-2';
//需要緩存的資源列表
const cacheFiles = [
    '/',
    './index.html',
    './index.js',
    './style.css',
    './img/wang.jpeg',
    './img/loading.svg'
];
//監(jiān)聽install事件, 完成安裝時钻蔑, 進(jìn)行文件緩存
self.addEventListener('install', function (e) {
    console.log("Service Worker當(dāng)前狀態(tài):install");
    const cacheOpenPromise = caches.open(cacheName).then(function (cache) {
        return cache.addAll(cacheFiles);
    });
    e.waitUntil(cacheOpenPromise);
});

可以看到啥刻, 首先cacheFiles中我們列出了所有的靜態(tài)資源依賴。注意 '/',由于根路徑也可以訪問頁面咪笑, 因此不要忘記將其也緩存下來可帽。 當(dāng)Service Worker install時, 我們就會通過caches.open()caches.addAll()方法將資源緩存起來窗怒。這里給緩存起了個名字cacheName映跟,這個值成為這些緩存的key。
caches是一個全局變量扬虚, 通過它我們可以操作Cache相關(guān)接口努隙。

Cache接口提供緩存的Request/Response對象的存儲機制。Cache接口向wokers一樣辜昵, 是保留在window作用域下的荸镊。盡管被定義在Service Worker中, 但是它不必一定要配合Service Worker使用堪置。 ——MDN躬存。

4. 使用緩存的靜態(tài)資源
到目前為止, 我們僅僅注冊了一個Service Worker, 并在其install時緩存了一些靜態(tài)資源舀锨。 然而岭洲, 如果這是運行網(wǎng)頁時會發(fā)現(xiàn)無法使用我們的緩存。因為我們僅僅緩存了這些資源雁竞, 然而瀏覽器并不知道需要如何使用它們钦椭,換句話說,瀏覽器仍然會通過向服務(wù)器發(fā)送請求來等待并使用這些資源碑诉。
在文章的前半部分提到了Service Worker 可以做 ”客戶端代理“ ——用Service Worker 來幫助如何使用緩存彪腔。
下圖是一個簡單的策略:

  • 瀏覽器有cache時:


  • 瀏覽器無cache時:


  1. 瀏覽器發(fā)起請求, 請求各類靜態(tài)資源(html,css,img
  2. Service Worker攔截瀏覽器請求进栽, 并查詢當(dāng)前cache
  3. 若存在cache則直接返回德挣,結(jié)束
  4. 若不存在cache,則通過 fetch方法向服務(wù)器端發(fā)起請求快毛, 并返回請求結(jié)果給瀏覽器
//cache存在則使用cache格嗅,無cache則fetch服務(wù)器端請求資源
self.addEventListener('fetch', function (e) {
    e.respondWith(
      caches.match(e.request).then(function (cache) {
          return cache || fetch(e.request);
      }).catch(function (err) {
            console.log(err);
            return fetch(e.request);
      })
    );
});

fetch事件會監(jiān)聽所有瀏覽器請求。 e.responedWith()方法接受Promise作為參數(shù)唠帝,通過它讓Service Worker向瀏覽器返回數(shù)據(jù)屯掖。caches.match(e.request)則可以查看當(dāng)前的請求是否有一份本地緩存, 如果有緩存襟衰,則直接向瀏覽器返回cache贴铜。如果沒有,則Service Worker 會向后端服務(wù)發(fā)起一個fetch(e.request)的請求瀑晒, 并將請求返回給瀏覽器绍坝。

到目前為止, 我們的網(wǎng)頁靜態(tài)資源將會被緩存到本地苔悦;以后再訪問時轩褐, 就會使用這些緩存而不發(fā)起網(wǎng)絡(luò)請求,因此無網(wǎng)的情況下玖详,依舊能訪問該網(wǎng)頁把介。

5. 更新靜態(tài)緩存資源
當(dāng)緩存可以使用時,細(xì)心的話會發(fā)現(xiàn)一個小問題竹宋, 當(dāng)我們將資源緩存后劳澄,除非注銷(unregister)sw.js、手動清除緩存蜈七、否則新的靜態(tài)資源將無法緩存秒拔。
解決這個問題的一個簡單方法就是修改cacheName。由于瀏覽器判斷sw.js是否更新是通過字節(jié)方式飒硅, 因此修改cacheName會重新出發(fā)install并緩存資源砂缩。 此外,在activate事件中三娩, 我們需要檢查cacheName是否變化庵芭,如果變化則表示有了新的緩存,原有緩存需要刪除雀监。

//監(jiān)聽activite事件双吆, 激活后通過cache的key來判斷是否需要更新cache中的靜態(tài)資源
self.addEventListener('activate',function (e) {
    console.log('Service Worker 當(dāng)前狀態(tài): activate');
    const cachePromise = caches.keys().then(function (keys) {
        return Promise.all(keys.map(function (key) {
            if(key !== cacheName) {
                return caches.delete(key);
            }
        }))
    });
    e.waitUntil(cachePromise);
    return self.clients.claim(); //激活觸發(fā)Service Worker
});

6.緩存離線接口##

離線時眨唬,我們發(fā)現(xiàn)只展示了靜態(tài)資源,而接口數(shù)據(jù)渲染的地方還是空白一片好乐。
所以我們把XHR也緩存一份匾竿, 然后再請求時, 會優(yōu)先使用本地緩存蔚万, 然后再向服務(wù)器端請求數(shù)據(jù)岭妖。大致過程如下:



首先需要改造一下sw.js的fetch事件進(jìn)行API數(shù)據(jù)的緩存:

//定義api緩存name
const apiCacheName = 'api-0-1-1';
// cache存在則使用cache,無cache則fetch服務(wù)器端請求資源
self.addEventListener('fetch', function (e) {
    // 需要緩存的XHR請求
    const cacheRequestUrls = [
        '/book?'
    ];
    console.log('當(dāng)前請求接口:' + e.request.url);
    //判斷當(dāng)前請求是否需要緩存
    const needCacheXhr = cacheRequestUrls.some(function (url) {
        return e.request.url.indexOf(url) > -1;
    });
    if(needCacheXhr) {
        // 使用fetch請求數(shù)據(jù)反璃, 并將請求結(jié)果clone一份緩存到cache
        // 緩存后在browser中使用全局變量caches獲取
        caches.open(apiCacheName).then(function (cache) {
            return fetch(e.request).then(function (res) {
                cache.put(e.request.url, res.clone());
                return res;
            })
        })
    }else {
        // 非api請求昵慌, 直接查詢cache
        // 如果有cache直接返回, 否則通過fetch請求
        e.respondWith(
            caches.match(e.request).then(function (cache) {
                return cache || fetch(e.request);
            }).catch(function (err) {
                console.log(err);
                return fetch(e.request);
            })
        );
    }
});

這里也為API緩存數(shù)據(jù)創(chuàng)建了一個專門的緩存位置淮蜈, key值為變量apiCacheName斋攀。在fetch事件中, 我們首先通過對比當(dāng)前請求與cacheRequestUrls來判斷是否需要緩存XHR請求的數(shù)據(jù)梧田, 如果是的話就使用fetch方法向后端發(fā)起請求蜻韭。

fetch.then中我們以請求的url 為key, 向cache中更新了一份當(dāng)前請求所返回數(shù)據(jù)的緩存,cache.put(e.request.url, res.clone())柿扣。這里使用.clone()方法拷貝一份響應(yīng)數(shù)據(jù)肖方, 這樣就可以對響應(yīng)緩存進(jìn)行各類操作而不用擔(dān)心響應(yīng)原數(shù)據(jù)被修改了。

7.使用離線XHR數(shù)據(jù)未状, 完成離線渲染俯画,提升響應(yīng)速度

目前為止, 我們已經(jīng)對Service Worker(sw.js) XHR進(jìn)行了緩存改造司草, 最后只剩如何在XHR請求時有策略的使用緩存了艰垂, 這一部分的改造幾種在index.js, 也就是我們的前端腳本埋虹。
先看看XHR緩存的這張圖:



和普通情況不同猜憎, 前端瀏覽器會首先嘗試獲取緩存數(shù)據(jù)并使用其渲染頁面。同時搔课, 瀏覽器也會發(fā)起一個XHR請求胰柑,Service Worker 通過將請求返回的數(shù)據(jù)更新到緩存中的同事向前端頁面返回數(shù)據(jù)(這一部分主要就是緩存策略);最終爬泥,如果判斷返回的數(shù)據(jù)與最開始取到的cache不一致柬讨,則重新渲染界面, 否則忽略袍啡。

為了使代碼更加清晰踩官,我們將原來XHR請求部分單獨剝離出來,作為一個方法 getApiDataRemote()使用境输, 同事將其改造為Promise蔗牡。

我們知道颖系,在Service Worker 中是可以通過caches變量來訪問到緩存對象的。 而在前端應(yīng)用中辩越,也仍然可以通過caches來訪問緩存集晚。 為了統(tǒng)一代碼, 將獲取該請求的緩存數(shù)據(jù)也封裝為Promise方法getApiDataFromCache()区匣。

    // 前端頁面中
    function getApiDataRemote(url) {
        if('caches' in window) {
            return caches.match(url).then(function (cache) {
                if(!cache) {
                    return;
                }
                return cache.json();
            })
        }else {
            return Promise.resolve();
        }
    }

而原本在請求接口queryData()方法中, 我們會請求后端數(shù)據(jù)蒋院, 然后再渲染頁面亏钩;而現(xiàn)在則加上緩存的渲染:
···

 function queryBook(value) {
        var input = document.querySelector('#js-search-input');
        var query = value || input.value;
        var url = '/book?q=' + query;
        //請求緩存
        var remotePromise = getApiDataRemote(url);
        var cacheData;
        //先使用緩存數(shù)據(jù)渲染
        getApiDataFromCache(url).then(data => {
            if(data) {
                loading(false);
                input.blur();
                fillList(data.data.songs);
                document.querySelector('#js-thanks').style = 'display: block';
            }
            cacheData = data || {};
            return remotePromise;
        }).then(function (data) {
            if(JSON.stringify(data) !== JSON.stringify(cacheData)) {
                loading(false);
                input.blur();
                fillList(data.data.songs);
                document.querySelector('#js-thanks').style = 'display: block';
            }
        })
    }

如果getApiDataFromCache(url).then返回緩存數(shù)據(jù), 則使用它進(jìn)行先渲染欺旧。 當(dāng)getApiDataRemote()返回的數(shù)據(jù)時姑丑, 與cacheData進(jìn)行比對, 只有數(shù)據(jù)不一致的時候重新渲染頁面即可辞友。 這里使用 JSON.stringify()進(jìn)行粗略的比較栅哀,這么做有兩個優(yōu)勢:

  1. 離線可用。 如果我們之前訪問過某些URL,那么及時在離線的情況下称龙, 重復(fù)響應(yīng)的操作依然可以正常顯示頁面留拾。
  2. 優(yōu)化體驗,提高訪問速度讀取本地cache耗時相對對網(wǎng)絡(luò)請求時間很低鲫尊,因此就會有一種 "秒開"痴柔, "秒響應(yīng)"的感覺。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疫向,一起剝皮案震驚了整個濱河市咳蔚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搔驼,老刑警劉巖谈火,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異舌涨,居然都是意外死亡糯耍,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門囊嘉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谍肤,“玉大人,你說我怎么就攤上這事哗伯』拇В” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵焊刹,是天一觀的道長系任。 經(jīng)常有香客問我恳蹲,道長,這世上最難降的妖魔是什么俩滥? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任嘉蕾,我火速辦了婚禮,結(jié)果婚禮上霜旧,老公的妹妹穿的比我還像新娘错忱。我一直安慰自己,他們只是感情好挂据,可當(dāng)我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布以清。 她就那樣靜靜地躺著,像睡著了一般崎逃。 火紅的嫁衣襯著肌膚如雪掷倔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天个绍,我揣著相機與錄音勒葱,去河邊找鬼。 笑死巴柿,一個胖子當(dāng)著我的面吹牛凛虽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播广恢,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼涩维,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了袁波?” 一聲冷哼從身側(cè)響起瓦阐,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎篷牌,沒想到半個月后睡蟋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡枷颊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年戳杀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夭苗。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡信卡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出题造,到底是詐尸還是另有隱情傍菇,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布界赔,位于F島的核電站丢习,受9級特大地震影響牵触,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咐低,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一揽思、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧见擦,春花似錦钉汗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至执俩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間癌刽,已是汗流浹背役首。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留显拜,地道東北人衡奥。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像远荠,于是被迫代替她去往敵國和親矮固。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容