一文了解Service Worker

簡介

首先了解一下PWA(Progressive web apps,漸進式 Web 應用)運用現代的 Web API 以及傳統的漸進式加強策略來建立跨平臺 Web 應用程序。 PWA的優勢 PWA可被發現易安裝可連接獨立於網絡漸進式可重用響應性安全的。 PWA中能夠經過Service Worker來實現離線的應用,這個也是PWA中一個比較重要的環節,它們主要應用到Web App中,已得到更好的體驗,而且在如今也在大規模的應用。 Service Worker是一個事件驅動worker,運行在一個單獨的後臺進程,是PWA(ProgressiveWeb App)運行的基礎。主要用於代理網頁請求,可緩存請求結果;可實現離線緩存功能,也擁有單獨的做用域範圍和運行環境。咱們之後把Service Worker簡稱爲SWjavascript

本文大體分爲:css

  • SW特性
  • SW生命週期和使用
  • SW中的緩存策略
  • SW一些注意事項
  • SW幾種緩存策略
  • SW中的消息推送
  • Workerbox使用

實例代碼實例代碼地址html

SW的特性

它們的運行在一個與咱們頁面的 JavaScript 主線程獨立的線程上,而且沒有對 DOM 結構的任何訪問權限。 這引入了與傳統 Web 編程不一樣的方法 - API 是非阻塞的,而且能夠在不一樣的上下文之間發送和接收信息前端

SW使用限制

SW除了work線程的限制外,因爲可攔截頁面請求,爲了保證頁面安全,瀏覽器端對sw的使用限制也很多。java

  • 沒法直接操做DOM對象,也沒法訪問windowdocumentparent對象。能夠訪問navigatorlocationSW 經過響應 postMessage 接口發送的消息來與其控制的頁面通訊,頁面可在必要時對 DOM 執行操做。
  • 可代理的頁面做用域限制。默認是sw.js所在文件目錄及子目錄的請求可代理,可在註冊時手動設置做用域範圍;
  • 必須https 中使用,容許在開發調試的localhost使用。

SW主要做用

  • 能夠用來作緩存,以達到提高體驗、節省瀏覽等等
  • SW 是一種可編程網絡代理,讓您可以控制頁面所發送網絡請求的處理方式。
  • 離線緩存接口請求及文件,更新、清除緩存內容;
  • 可分配給 Service Worker 一些任務,並在使用基於 Promise 的方法當任務完成時收到結果。
  • Service Worker處於空閒狀態會被終止,在下一次須要時重啓。

SW兼容性

能夠經過查詢service worker能夠看到他在不一樣平臺或不一樣瀏覽器中的兼容性。git

SW生命週期和使用

SW 的生命週期徹底獨立於網頁。 SW 爲網頁添加一個相似於 App 的生命週期,它只會響應系統事件,就算瀏覽器關閉時操做系統也能夠喚醒 SW,這點很是重要,讓Web AppNative App 的能力變得相似了。因爲是離線緩存,因此在初始安裝時更新它們的所走的生命週期是不相同。下面咱們就根據這兩種場景結合代碼來分析它的執行步驟。 SW的生命週期大體分爲:註冊更新安裝成功安裝失敗激活銷燬github

使用SW前提條件web

  • 必須https 中使用,容許在開發調試的localhost使用。
  • 瀏覽器必須支持SW

初始安裝時

初始安裝時大體流程大體以下圖: 正則表達式

http-cache-serviceworker

大體能夠分爲註冊SW => 安裝SW => 激活 => 空閒 => (緩存和返回請求/終止),在初始安裝時會大體分爲這幾個步驟,下面就按照這幾個步結合代碼實現。chrome

註冊 Service Worker

用戶首次訪問SW控制的網站或頁面時,sw.js會馬上被下載和解析。咱們要在頁面中寫入JavaScript來註冊SW

// 判斷瀏覽器是否支持serviceWorker
    if ('serviceWorker' in navigator) {
        // 在頁面加載後
        window.addEventListener('load', function () {
            // 經過navigator.serviceWorker.register 註冊'./sw.js
            navigator.serviceWorker.register('./sw.js')
            .then(reg => { //註冊成功
                console.log('註冊成功', reg)
            }).catch(err => { //註冊成功
                console.log('註冊失敗', err)
            })
        });
    } else {
        console.log('當前瀏覽器不支持SW')
    }
複製代碼

首先檢瀏覽器是否支持SW,若是支持就在瀏覽器加載後經過register().then註冊sw.js,而且設置註冊成功或者失敗的回調函數。

注意:register() 方法的精妙之處在於服務工做線程文件的位置。SW降接收此網域上全部的事項的featch事件。 若是是Chrome瀏覽器能夠經過chrome://inspect/#service-workers或者console => application => Service Worker查看是否註冊成功

由於如今sw.js中咱們的代碼是空的,因此在瀏覽器中的cache stoage是空的,運行效果以下:

安裝 Service Worker

在受控頁面啓動註冊流程後,下面就是SW獲取的第一個事件install,而且只發生一次。傳遞到 installEvent.waitUntil() 的一個 promise 可代表安裝的持續時間以及安裝是否成功。

install中要作三件事打開緩存緩存文件確認全部須要的資產是否已緩存

// 在sw.js中監聽對應的安裝事件,並預處理須要緩存的文件
    // 該部份內容涉及到cacheStorage API

    // 定義緩存空間名稱
    const CACHE_NAME = 'sw_cache_v1';
    // 定義須要緩存的文件目錄
    let cachelist = ['./app.js', './index.css'];
    // 監聽安裝事件,返回installEvent對象
    self.addEventListener('install', function (installEvent) {
        // waitUntil方法執行緩存方法
        installEvent.waitUntil(
            // cacheStorage API 可直接用caches來替代
            // open方法建立/打開緩存空間,並會返回promise實例
            // then來接收返回的cache對象索引
            caches.open(CACHE_NAME)
             // cache對象addAll方法解析(同fetch)並緩存全部的文件
            .then(function(cache) {
                console.log('Opened cache');
                return cache.addAll(cachelist);
            })
        );
    });

複製代碼

第一個事件爲install,該事件在Worker執行時當即觸發。在install的回調函數中,咱們經過caches.open(CACHE_NAME)打開緩存,以後調用cache.addAll()並傳入路徑數組。這是一個promise鏈(caches.open()chaches.addAll()installEvent.waitUntill()放大帶有promise並使用它來判斷安裝所花時間,以及是否安裝成功。

注意: 第一個事件install,它只能被每一個 SW 調用一次。若是您更改您的 SW 腳本,則瀏覽器將其視爲一個不一樣SW,而且它將得到本身的 install 事件。 若有任何文件沒法下載,則安裝步驟將失敗。 當前的狀態是在等待狀態。 咱們能夠直接經過self.skipwaiting()讓當前sw當即將狀態提高到active

當咱們安裝成功時,效果以下圖所示:

http-cache-serviceworker

會多了一個skipWaiting,還有在cache stroage中的當前域名下的service worker對應的緩存文件列表。這個時候咱們即便刷新也不會走service worker的緩存的。

激活

SW 準備控制客戶端並處理 pushsync 等功能事件時,您將得到一個 activate 事件。但這不意味着調用 .register() 的頁面將受控制。若是第二次加載此演示(換言之,刷新頁面),該頁面將受控制。改寫代碼sw.js以下:

self.addEventListener('install', () => {
        // 通常註冊之後,激活須要等到再次刷新頁面後再激活
        // 可防止出現等待的狀況,這意味着服務工做線程在安裝完後當即激活
        self.skipWaiting();
    })
    self.addEventListener('activate', function (event) {
        event.waitUntil(
            // cacheStorage API 可直接用caches來替代
            // open方法建立/打開緩存空間,並會返回promise實例
            // then來接收返回的cache對象索引
            caches.open(CACHE_NAME)
            // cache對象addAll方法解析(同fetch)並緩存全部的文件
            .then(function(cache) {
                console.log('Opened cache');
                return cache.addAll(cachelist);
            })
        );
    })
複製代碼

通常在書寫的時候,會在install()註冊以後直接經過self.skipWaiting();激活當前的SW,在activate中書寫打開緩存等等的邏輯,就不會出現上面還要刷新或者手動激活的問題。效果圖以下:

http-cache-serviceworker

可是若是出現更新SW,而且更新了緩存列表或者出現異步資源時,咱們能夠經過clients.claim()更新緩存列表。

clients.claim

激活 SW 後,您能夠經過在其中調用 clients.claim() 控制未受控制的客戶端。google developer中的一個異步加載圖片的實例。下面修改代碼以下:

self.addEventListener('install', (event) => {
        event.waitUntil(
            // cacheStorage API 可直接用caches來替代
            // open方法建立/打開緩存空間,並會返回promise實例
            // then來接收返回的cache對象索引
            caches.open(CACHE_NAME)
            // cache對象addAll方法解析(同fetch)並緩存全部的文件
            .then(function(cache) {
                console.log('Opened cache');
                return cache.addAll(cachelist);
            })
        );
        // 通常註冊之後,激活須要等到再次刷新頁面後再激活
        // 可防止出現等待的狀況,這意味着服務工做線程在安裝完後當即激活
        self.skipWaiting();
    })
    self.addEventListener('activate', function (event) {
        // 若緩存數據更改,則在這裏更新緩存
        var cacheDeletePromise = caches.keys()
        .then(keyList => {
            Promise.all(keyList.map(key => {
                if (key !== CACHE_NAME) {
                    var deletePromise = caches.delete(key)
                    return deletePromise
                } else {
                    Promise.resolve()
                }
            }));
        });
        event.waitUntil(
            Promise.all([cacheDeletePromise]).then(res => {
                this.clients.claim()
            })
        );
    })
複製代碼

用於處理更新緩存,新的文件。到如今咱們仍是沒有用到serviceWorker的緩存,下面重頭戲來了緩存和返回請求

緩存和返回請求

上咱們已經安裝而且激活了SW,如今咱們要返回一個緩存的響應。SW用戶轉至其餘頁面或刷新當前頁面後,將開始接受fetch事件。

self.addEventListener('fetch', function(event) {
        event.respondWith(
            caches.match(event.request)
            .then(function(response) {
                // Cache hit - return response
                if (response) {
                    return response;
                }
                return fetch(event.request);
            })
        );
    });
複製代碼

在定義的fetch事件中,咱們在event.respondWith()中傳入來自caches.match()的一個promisecaches.match()這個方法檢視該請求,並從服務工做線程所建立的任何緩存中查找緩存的結果。若是命中返回緩存值,不然,將調用fetch以發出網絡請求。運行效果以下:

http-cache-serviceworker

若是咱們想把新的請求也緩存掉,修改代碼以下:

self.addEventListener('fetch', function(event) {
        event.respondWith(
            caches.match(event.request)
            .then(function(response) {
                // Cache hit - return response
                if (response) {
                    return response;
                }
                // return fetch(event.request);
                var requestClone = event.request.clone();
                return fetch(requestClone).then(response => {
                    if(!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }
                    var responseToCache = response.clone();
                    caches.open(CACHE_NAME)
                    .then(function(cache) {
                        cache.put(event.request, responseToCache);
                    });
                    return response;
                });
            })
        );
    });
複製代碼

執行操做以下:

  1. fetch 請求中添加對 .then() 的回調。
  2. 得到響應後,確保響應有效。檢查並確保響應的狀態爲 200。確保響應類型爲 basic,亦即由自身發起的請求。 這意味着,對第三方資產的請求也不會添加到緩存。
  3. 若是經過檢查,則克隆響應。

沒有緩存新請求時效果以下:

http-cache-serviceworker

當使用咱們下面的代碼時,效果圖以下:

http-cache-serviceworker

即便異步請求的png圖片也被加入了緩存中。

更新SW

在如下狀況下會觸發更新:

  • 導航到一個做用域內的頁面。
  • 更新 pushsync 等功能事件,除非在前 24 小時內已進行更新檢查。
  • 調用 .register(),僅在 SW 網址已發生變化時。

當觸發更新時,會通過大體以下步驟:

  1. 更新您的服務工做線程 JavaScript 文件。 用戶導航至您的站點時,瀏覽器會嘗試在後臺從新下載定義 SW 的腳本文件。 若是 SW 文件與其當前所用文件存在字節差別,則將其視爲新 SW
  2. 更新SW現有 SW 一塊兒啓動,並獲取本身的 install 事件。
  3. 此時, SW控制着當前頁面,所以 SW 將進入 waiting 狀態。
  4. 若是新 Worker 出現不正常狀態代碼(例如,404)、解析失敗,在執行中引起錯誤在安裝期間被拒,則系統將捨棄新 Worker,但當前 Worker 仍處於活動狀態
  5. 安裝成功後,更新的 Worker 將 wait,直到現有 Worker 控制零個客戶端。(注意,在刷新期間客戶端會重疊。)
  6. self.skipWaiting() 可防止出現等待狀況,這意味着 Service Worker 在安裝完後當即激活。

更新SW代碼

更新一個叫作sw_cache_v2的新的SW緩存,代碼以下:

const CACHE_NAME = 'sw_cache_v2';

    self.addEventListener('install', (event) => {
        event.waitUntil(
            // cacheStorage API 可直接用caches來替代
            // open方法建立/打開緩存空間,並會返回promise實例
            // then來接收返回的cache對象索引
            caches.open(CACHE_NAME)
            // cache對象addAll方法解析(同fetch)並緩存全部的文件
            .then(function(cache) {
                return cache.add('index_copy.png')
            })
        );
        // 通常註冊之後,激活須要等到再次刷新頁面後再激活
        // 可防止出現等待的狀況,這意味着服務工做線程在安裝完後當即激活
        self.skipWaiting();
    })
    self.addEventListener('activate', function (event) {
        // 若緩存數據更改,則在這裏更新緩存
        var cacheDeletePromise = caches.keys()
        .then(keyList => {
            Promise.all(keyList.map(key => {
                if (key !== CACHE_NAME) {
                    var deletePromise = caches.delete(key)
                    return deletePromise
                } else {
                    Promise.resolve()
                }
            }));
        });
        event.waitUntil(
            Promise.all([cacheDeletePromise]).then(res => {
                this.clients.claim()
            })
        );
    });
    self.addEventListener('fetch', function(event) {
        event.respondWith(
            caches.match(event.request)
            .then(function(response) {
                // Cache hit - return response
                if (response) {
                return response;
                }
                return fetch(event.request);
            }
            )
        );
    });
複製代碼

代碼執行效果以下圖所示:

http-cache-serviceworker

整個過程咱們大體通過了install => waiting => activate三個過程。

更新Install

咱們在代碼中把sw_cache_v1更改成sw_cache_v2,咱們從新Install了一個新的緩存sw_cache_v2,而且經過添加了一個緩存進去cache.add('index_copy.png')

更新Waiting

若是新的緩存安裝成功SW後,更新的SW將延遲激活,直到現有SW再也不控制任何客戶端。此狀態爲waiting,這是瀏覽器確保每次只運行一個SW版本的樣式。

激活Activate

SW 退出時將觸發 Activate,新 SW 將可以控制客戶端。此時,您能夠執行在仍使用舊 Worker 時沒法執行的操做,如遷移數據庫和清除緩存。 在上面的演示中,我維護了一個指望保存的緩存列表,而且在 activate事件中,我刪除了全部其餘緩存,從而也移除了舊的 sw_cache_v1 緩存。

不要更新之前的版本。它多是許多舊版本的 SW

若是您將一個 promise 傳遞到 event.waitUntil(),它將緩衝功能事件(fetch、push、sync 等),直到 promise 進行解析。所以,當您的 fetch 事件觸發時,激活已所有完成。

Cache storage API 屬於「源存儲」(如 localStorageIndexedDB)。若是您在同源上運行許多網站(例如,yourname.github.io/myapp),請注意,不要刪除其餘網站的緩存。爲避免此問題,能夠爲您的緩存名稱提供一個在當前網站上具備惟一性的前綴(例如,myapp-static-v1),而且不要刪除緩存,除非它們以 myapp- 開頭。

跳過等待階段skipWaiting

等待階段表示您每次只能運行一個網站版本,但若是您不須要該功能,您能夠經過調用 self.skipWaiting() 儘快將新 SW 激活。 這會致使您的 SW 將當前活動的 Worker 逐出,並在進入等待階段時儘快激活本身(或當即激活,前提是已經處於等待階段)。這不能讓您的 Worker 跳過安裝,只是跳過等待階段skipWaiting() 在等待期間調用仍是在以前調用並無什麼不一樣。通常狀況下是在 install 事件中調用它:

self.addEventListener('install', event => {
        self.skipWaiting();
        event.waitUntil(
            // caching etc
        );
    });
複製代碼

clients.claim() 同樣,它是一個競態。

skipWaiting() 意味着新 Service Worker 可能會控制使用較舊 Worker 加載的頁面。這意味着頁面提取的部分數據將由舊 Service Worker 處理,而新 Service Worker 處理後來提取的數據。若是這會致使問題,則不要使用 skipWaiting()

手動更新update

當頁面刷新或者執行功能性事件時,瀏覽器會自動檢查更新,其實咱們也能夠手動的來觸發更新:

navigator.serviceWorker.register("/sw.js").then(reg => {
        // sometime later…
        reg.update();
    });
複製代碼

若是你但願你的用戶訪問頁面很長時間並且不用刷新,那麼你能夠每一個一段時間調用一次update()

避免改變 SW 的 URL

你可能會考慮給每一個 SW 不一樣的 URL。**千萬不要這麼作!**在 SW 中這麼作是「最差實踐」,要在原地址上修改 SW

舉個例子來講明爲何:

  1. index.html註冊了sw-v1.js做爲SW

  2. sw-v1.jsindex.html作了緩存,也就是緩存優先(offline-first)。

  3. 你更新了index.html從新註冊了在新地址的 SW sw-v2.js.

若是你像上面那麼作,用戶永遠也拿不到sw-v2.js,由於index.htmlsw-v1.js緩存中,這樣的話,若是你想更新爲sw-v2.js,還須要更改原來的sw-v1.js

SW一些注意事項

這裏主要分爲:

  • 更新的小技巧
  • sync事件

更新小技巧

SW 生命週期是專爲用戶構建的,這就給開發工做帶來必定的困難。幸運的是,咱們可經過如下幾個工具解決這個問題:

Update on reload

http-cache-serviceworker

這可以使生命週期變得對開發者友好。每次瀏覽時都將:

  1. 從新提取 SW
  2. 即便字節徹底相同,也將其做爲新版本安裝,這表示運行 install 事件並更新緩存
  3. 跳過等待階段,以激活SW
  4. 瀏覽頁面。這意味着每次瀏覽時(包括刷新)都將進行更新,無需從新加載兩次或關閉標籤。

Skip waiting

http-cache-serviceworker

若是您有一個 Worker 在等待,您能夠按 DevTools 中的「skip waiting」以當即將其提高到「active」。同時也能夠經過self.skipWaiting()來實現。

Shift-reload

若是您強制從新加載頁面 (shift-reload),則將徹底繞過 SW。頁面將變得不受控制。此功能已列入規範,所以,它在其餘支持 SW 的瀏覽器中也適用。

處理更新週期

爲支持儘量多的模式,整個更新週期都是可觀察的:

navigator.serviceWorker.register('/sw.js').then(reg => {
        reg.installing; // the installing worker, or undefined
        reg.waiting; // the waiting worker, or undefined
        reg.active; // the active worker, or undefined

        reg.addEventListener('updatefound', () => {
            // A wild service worker has appeared in reg.installing!
            const newWorker = reg.installing;

            newWorker.state;
            // "installing" - the install event has fired, but not yet complete
            // "installed" - install complete
            // "activating" - the activate event has fired, but not yet complete
            // "activated" - fully active
            // "redundant" - discarded. Either failed install, or it's been
            // replaced by a newer version

            newWorker.addEventListener('statechange', () => {
            // newWorker.state has changed
            });
        });
    });

    navigator.serviceWorker.addEventListener('controllerchange', () => {
        // This fires when the service worker controlling this page
        // changes, eg a new worker has skipped waiting and become
        // the new active worker.
    });
複製代碼

sync事件

Sync事件讓你能夠先將網絡相關任務延遲到用戶有網絡的時候再執行。這個功能常被稱做「背景同步」。這功能能夠用於保證任何用戶在離線的時候所產生對於網絡有依賴的操做,最終能夠在網絡再次可用的時候抵達它們的目標。

代碼示例以下

navigator.serviceWorker.ready.then(registration => {
        document.getElementById('submit').addEventListener('click', () => {
            registration.sync.register('submit').then(() => {
                console.log('sync registered!');
            });
        });
    });
複製代碼

咱們指定在一個按鈕的點擊事件裏,在一個全局的 ServiceWorkerRegistration 對象身上調用 sync.register

簡單地講,任何你須要確保在有網絡時馬上執行或者等到有網再執行的操做,都須要註冊爲一個sync事件

這操做能夠是發送一個評論,或者獲取用戶信息,在SW的事件監聽器裏會以下定義:

// sw.js
    self.addEventListener('sync', event => {  
        if (event.tag === 'submit') {
            console.log('sync!');
        }
    });
複製代碼

咱們監聽一個 sync 事件,而後在 SyncEvent 對象上檢查 tag 是否匹配咱們在點擊事件裏所設定的 'submit'

若是多個 tag 標記爲 submitsync事件被註冊了,sync 事件處理器只會運行一次。

因此在這個例子裏,若是用戶離線了,而後點擊按鈕7次,當網絡再次連上,全部的sync註冊都會合而爲一,sync事件只會觸發一次。

Sync事件是何時觸發

若是用戶的網絡時聯通的,那麼sync事件馬上觸發而且馬上執行你所定義的任務。

而若是用戶離線了,sync 事件會在網絡恢復後第一時間觸發

SW幾種緩存策略

  • 漸進式緩存
  • 僅使用緩存
  • 僅使用網絡
  • 緩存優先
  • 網絡優先
  • 速度優先

漸進式緩存

對於在install中發現沒有緩存,頁面又依賴但又不常常變化的資源,能夠在頁面打開或發生用戶交互時觸發fetch而後使用fetch api再去網絡拉取,將返回正常的response緩存起來以便下次使用。

progressive-cache

self.addEventListener('fetch', function(event) {
        event.respondWith(
            caches.match(event.request)
            .then(function(response) {
                // Cache hit - return response
                if (response) {
                    return response;
                }
                // return fetch(event.request);
                var requestClone = event.request.clone();
                return fetch(requestClone).then(response => {
                    if(!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }
                    var responseToCache = response.clone();
                    caches.open(CACHE_NAME)
                    .then(function(cache) {
                        cache.put(event.request, responseToCache);
                    });
                    return response;
                });
            })
        );
    });
複製代碼

僅使用緩存

fetch事件中,僅去匹配資源,若匹配失敗,表現出來的就是前端頁面對於該 資源加載失敗。這裏容錯性比較差,適用於頁面資源都是靜態資源的,且不能使用不影響安裝的資源預緩存

cache-only

// SW請求攔截事件
    self.addEventListener('fetch', (event) => {
        event.respondWith(
            caches.open(OFFLINE_CACHE_NAME).then((cache) => {
                // 匹配資源若是命中返回緩存
                return cache.macth(event.request.url);
            });
        );
    });
複製代碼

僅使用網絡

fetch事件中,僅將request從新抽出用fetch去網絡加載並返回給前端頁面。適用於資源大可能是動態資源實時性要求高的場景。

network-only

// SW請求攔截事件
    self.addEventListener('fetch', (event) => {
        // 僅使用網絡加載
        event.respondWith(fetch(event.request));
    });
複製代碼

緩存優先

簡單的資源緩存中使用的就是緩存優先策略先去緩存匹配匹配失敗折回網絡,這算是最經常使用、容錯性能好的一種策略。

firstCache

function firstCache (cacheName, request) {
        // 打開SW
        return caches.open(cacheName).then(cache => {
            // 匹配請求路徑
            return cache.match(request).then(response => {
                // fetch請求
                var fetchServer = function() {
                    return fetch(request).then(newResponse => {
                        cache.put(request, newResponse.clone());
                        return newResponse;
                    });
                }
                // 若是緩存中有數據則返回,不然請求網絡數據
                if (response) {
                    return response;
                } else {
                    return fetchServer();
                }
            });
        });
    }
複製代碼

網絡優先

fetch事件中先去網絡fetch,當出現服務器故障或者網絡不良時折回本地緩存,目的是爲了展現最新的數據,對實時性要求比較高但又可以帶來良好體驗的應用,好比天氣類型應用。

firstNet

function firstNet(cacheName, request) {
        // 請求網絡數據並緩存
        return fetch(request).then(response => {
            // 響應clone
            var responseCopy = response.clone();
            caches.open(cacheName).then(cache => {
                // fetch請求
                cache.put(request, responseCopy);
            });
            return response;
        }).catch(() => {
            // fetch失敗走本地緩存
            return caches.open(cacheName).then(cache => {
                return cache.match(request);
            });
        });
    }
複製代碼

速度優先

fetch事件同時發起本地緩存匹配及網絡請求誰先返回使用誰的,該方案適用於對性能要求比較高的站點,縮短了緩存優先策略中有可能緩存中沒有資源再折回網絡的時間消耗。

function networkCacheRace(cacheName, request) {
    var timer, TIMEOUT = 500;
    /** * 網絡好的狀況下給網絡請求500ms, 若超時則從緩存中取數據 * 若網絡較差且沒有緩存, 因爲第一個 Promise 會一直處於 pending, 故此時等待網絡請求響應 */
    return Promise.race([new Promise((resolve, reject) => {
        // 緩存請求
        timer = setTimeout(() => {
            caches.open(cacheName).then( cache => {
                cache.match(request).then( response => {
                    if (response) {
                        resolve(response);
                    }
                });
            });
        }, TIMEOUT);
    }), fetch(request).then( response => {
        // 網絡請求
        clearTimeout(timer);
        var responseCopy = response.clone();
        caches.open(cacheName).then( cache => {
            cache.put(request, responseCopy);
        });
        return response;
    }).catch(() => {
        clearTimeout(timer);
        return caches.open(cacheName).then( cache => {
            return cache.match(request);
        });
    })]);
}
複製代碼

如今咱們能夠在 sw.js 中更改一下緩存策略,從而達到最理想的效果。

// sw.js
    self.addEventListener('fetch', event => {
        // ...
        if ( /\.(js|css)$/.test(url) ) {
            (cacheName = cacheMaps.cache_file) 
            && e.respondWith(networkCacheRace(cacheName, request));
        }
        // ...
    })
複製代碼

SW中的消息推送

Push消息

SW裏,經過 push 事件以及瀏覽器的 Push API,能夠實現push消息的功能。 在說道web push消息的時候,其實涉及到兩個正在完善中的技術:消息提醒信息推送

消息提醒

SW實現消息提醒挺簡單直接:

// app.js
    // ask for permission
    Notification.requestPermission(permission => {  
        console.log('permission:', permission);
    });

    // display notification
    function displayNotification() {  
        if (Notification.permission == 'granted') {
            navigator.serviceWorker.getRegistration()
            .then(registration => {
                registration.showNotification('this is a notification!');
            });
        }
    }
複製代碼
// sw.js
    self.addEventListener('notificationclick', event => {  
        // 消息提醒被點擊的事件
    });

    self.addEventListener('notificationclose', event => {  
        // 消息提醒被關閉的事件
    });
複製代碼

你須要先向用戶尋求讓你的網頁產生消息提醒的權限。以後,你就能夠彈出提示信息,而後處理某些事件,好比用戶把消息關掉的事件。

信息推送

信息推送涉及到利用瀏覽器提供的Push API以及後端的配合實現。要講解如何使用Push API徹底能夠再寫一篇文章,不過基本的套路以下:

http-cache-serviceworker

這是個略微複雜難懂的過程,已經超出這篇文章的討論範圍。

更好的方案 - Workbox

什麼是 Workbox ?

Workbox is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers.

其大概意思是它對常見的 SW 操做進行了一層封裝, 根據最佳實踐方便了開發者的使用。所以在咱們快速開發本身的 PWA 應用時使用 Workbox 是最合適不過的了。

它主要有如下幾大功能 :

  • Precaching ~ 預緩存
  • Runtime caching ~ 運行時緩存
  • Strategies ~ 緩存策略
  • Request routing ~ 請求路由控制
  • Background sync ~ 後臺同步
  • Helpful debugge

簡單應用

直接修改sw.js的代碼,以下:

// sw.js
    // 導入谷歌提供的 Workbox 庫
    importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.2.0/workbox-sw.js');

    if ( !workbox ) { 
        console.log(`Workbox didn't load.`);
        return;
    }

    // Workbox 註冊成功, 能夠進行下一步的操做

    // 當即激活, 跳過等待
    workbox.skipWaiting();
    workbox.clientsClaim();

    // workbox.routing.registerRoute()...
複製代碼

若是瀏覽器支持,能夠直接引用API接口:

  1. precaching能夠在註冊成功後直接緩存的文件;
  2. routing匹配符合規則的urlstrategies合做來完成文件的緩存。

代碼以下:

// 註冊完成後,即緩存對應的文件列表
workbox.precaching.precacheAndRoute([
    '/src/static/js/index.js',
    '/src/static/css/index/css'
])

// routing方法匹配請求文件路徑,strategies用來存儲對應文件
workbox.routing.registerRoute(
    matchFunction,  // 字符串或者是正則表達式
    handler // 可使用workbox.strategies緩存策略來緩存
)
複製代碼

workbox緩存策略

workbox.strategies緩存策略有:

  1. staleWhileRevalidate 使用已有的緩存,而後發起請求,用請求結果來更新緩存;
  2. networkFirst 先發起請求,請求成功後會緩存結果。若是失敗,則使用最新的緩存;
  3. cacheFirst 老是先使用緩存,若是無匹配的緩存,則發起網絡請求並緩存結果;
  4. networkOnly 強制發起請求;
  5. cacheOnly 強制使用緩存。

官方也有給了實現邏輯以下。

Cache Only

只從緩存中讀取,當緩存中沒有數據時,讀取失敗。

http-cache-serviceworker

NetWork Only

只經過網絡請求進行資源請求,若請求失敗,則返回失敗響應。

http-cache-serviceworker

NetWork First

  • 優先網絡請求
  • 網絡請求成功時,將結果寫入緩存,並將結果直接返回。
  • 網絡請求失敗時,從緩存中讀取結果,若讀取結果,則返回,若未讀取到,則請求失敗。

不難看出,這種策略是爲了保證在第一次請求成功以後,後面屢次的請求始終都能返回結果。

http-cache-serviceworker

workbox.routing.registerRoute(/\.(js|css)$/,
        workbox.strategies.networkFirst({
            // 給網絡請求0.5秒,若仍未返回則從緩存中取數據
            networkTimetoutSeconds: 0.5,
            cacheName: 'css.js',
        }),
    );
複製代碼

Cache First

  • 優先從緩存中讀取結果
  • 若緩存中不存在結果,則進行網絡請求
  • 網絡請求成功時,將結果寫入緩存並返回
  • 網絡請求失敗時,返回失敗響應

http-cache-serviceworker

workbox.routing.registerRoute(/\.(png|jpg|jpeg|gif|webp)$/,
        // 對於圖片資源使用緩存優先
        workbox.strategies.cacheFirst({
            cacheName: 'images',
            // 設置最大緩存數量以及過時時間
            plugins: [
            new workbox.expiration.Plugin({
                maxEntries: 60,
                maxAgeSeconds: 7 * 24 * 60 * 60,
            }),
            ],
        }),
    );
複製代碼

Stale-While-Revalidate

  • 優先查緩存,並同時發起網絡請求
  • 若緩存命中且網絡請求成功,返回緩存結果,並更新緩存(下次從緩存中讀取的數據就是最新的了)
  • 若緩存未命中,則看網絡請求是否成功,成功則更新緩存並返回結果,失敗則返回失敗響應。

http-cache-serviceworker

workbox.routing.registerRoute(/\.(js|css)$/,
        workbox.strategies.staleWhileRevalidate({
            cacheName: 'css.js',
        }),
    );
複製代碼

總結

在本篇文章中詳細的記錄了有關SW的主要功能和能給咱們帶來的好處。通常在開發中仍是推薦使用workbox,最後若是我的有興趣的能夠本身編寫示例來驗證文章中的代碼示例。

SW主要做用

  • 能夠用來作緩存,以達到提高體驗、節省瀏覽等等
  • SW 是一種可編程網絡代理,讓您可以控制頁面所發送網絡請求的處理方式。
  • 離線緩存接口請求及文件,更新、清除緩存內容;
  • 可分配給 Service Worker 一些任務,並在使用基於 Promise 的方法當任務完成時收到結果。
  • Service Worker處於空閒狀態會被終止,在下一次須要時重啓。

SW幾種策略

  • 漸進式緩存
  • 僅使用緩存
  • 僅使用網絡
  • 緩存優先
  • 網絡優先
  • 速度優先

感受寫的不錯請點一下贊,據說長的帥的人都會點贊,而且點贊後走上人生巔峯

參考

Service Worker:簡介  |  Web Fundamentals  |  Google Developers

Service Worker 生命週期  |  Web Fundamentals  |  Google Developers

用 Service Worker 實現前端性能優化

service worker 實現離線緩存

【前-workbox-網絡摘要】WorkBox緩存策略

相關文章
相關標籤/搜索