Service Workers 與離線緩存

系列文章:css

  1. Service Workers 和離線緩存 (本文)git

  2. Notification with Service Workers push eventsgithub

  3. PWA:添加應用至桌面及分享web

第一次聽到 Service Workers 這個詞仍是在去年 Google 來安利 Angular 2 的時候,那時就以爲很驚豔,想搞一搞,可是由於沒把網站升級成 https 一直拖到如今。不久前,把網站升級成了 https,終於能夠搞一發了。chrome

本篇主要包含如下內容:docker

固然,仍是先來看看 Service Workers 到底是什麼?

What's Service Workers?

Service Workers 是谷歌 chrome 團隊提出並大力推廣的一項 web 技術。在 2015 年,它加入到 W3C 標準,進入草案階段。W3C 標準中對 Service Workers 的解釋太細緻,相對而言,我更喜歡 MDN 上的解釋,更簡練,更易於理解。

Service workers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs. - MDN

簡單翻譯一下:Service workers 基本上充當應用同服務器之間的代理服務器,能夠用於攔截請求,也就意味着能夠在離線環境下響應請求,從而提供更好的離線體驗。同時,它還能夠接收服務器推送和後臺同步 API。

那麼,這項技術的瀏覽器支持狀況是什麼樣,仍是來看一眼 Can I use?

能夠從看到,Chrome 和 Firefox, Opera 都已經支持 Service Workers,底下的備註也寫到 Edge 在開發中,Safari 也考慮支持。至於 IE,船長都跳船了。看了 PC 端,再來看看移動端。移動端的支持率並不盡如人意,不過在安卓 4.4 以後,安卓原生瀏覽器,以及安卓版的 Chrome 都已經開始支持 Service Workers。

說句題外話,忽然發如今 Can I use 中選擇導入我國數據時,竟出現了 UC 和 QQ 瀏覽器的支持狀況,口以口以?...

言歸正傳,在真正開始使用 Service Workers 以前,還有幾點要注意:

  1. Service Workers 基於 Https,這是硬性條件(如何升級 https 能夠參考上一篇文章

  2. 每一個 Service Worker 都有本身的做用域,它只會處理本身做用域下的請求,而 Service Worker 的存放位置就是它的最大做用域

  3. Service Workder 是 Web Worker 的一種,它不可以直接操做 DOM

Github 上有一個很是棒的資源,它用圖片的方式展現了 Servic Workers 的一些核心要點。

搞定這些基礎就能夠正式開搞了...

小試 Service Workers

和其餘 worker 同樣,service worker 有一個獨自的文件。因爲以前所提到的 service worker 只能做用在本身存放位置之下的文件,因此,通常在應用根目錄下存放 service worker 文件。

首先,先寫一個最簡單的來看看瀏覽器是否是支持,以及可否正確地安裝並運行 service worker。

// service-worker.js
const _self = this;

console.log('In service worker.');

_self.addEventListener('install', function () {
    console.log('Install success');
});

_self.addEventListener('activate', function () {
    console.log('Activated');
});

雖然,service worker 是 web worker 其中的一種,但它有些不一樣,它有本身的註冊方式。

// ServiceWorkerService.js
const SERVICE_WORKER_API = 'serviceWorker';
const SERVICE_WORKER_FILE_PATH = 'service-worker.js';

const isSupportServiceWorker = () => SERVICE_WORKER_API in navigator;

if (isSupportServiceWorker()) {
    navigator
        .serviceWorker
        .register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'));
} else {
    console.info('Browser not support Service Worker.');
}

重啓程序以後,你應該就能在控制檯中看到 Load service worker Success.。然而,卻沒有另兩句的輸出,難道加載失敗了?可是,控制檯不是顯示加載成功了麼?不要擔憂,程序沒有出錯,只是 service worker 中的日誌信息有它本身的輸出位置,而並不是輸出在主日誌之中。

接下去,先來看看如何調試 service worker。

調試 Service Workers

在 Chrome 中,service worker 的信息顯示在 Application -> Service Workers 中,就像這樣

裏面會顯示註冊的 service worker,以及它當前的狀態。還能經過切換最上面的選項來模擬不一樣的網絡環境,測試在不一樣環境下 service worker 的響應,它們分別是:

  • Offline: 離線

  • Update on reload: 加載時更新

  • Bypass for network: 使用網絡內容

回到以前的問題,如何查看 service worker 之中的日誌哪?只需點擊圖中的 inspect 連接,它會彈出另外一個開發者窗口,在裏面能夠查看 service worker 的日誌。是否是以爲須要那麼多步有點麻煩,別擔憂,Chrome 已經替咱們解決了這個煩惱。從新刷新頁面後,Chrome 的開發者工具中已經可以查看 service workers 的信息了,好比:在 console 選項卡勾選 Show all messages 就能顯示 service workers 中控制檯的信息;在 source 選項卡也能看到 service workers 的代碼,固然也能夠打斷點啦~

在 firefox 中,默認會將 service worker 中的日誌輸出到主控制檯中,但要打開 service worker 的調試器就有點麻煩了。有兩種方法查看,一個是在地址欄中輸入 about:debugging#workers,另外一種就是經過菜單欄中選擇 Tools -> Web Developer -> Service Workers

更多關於在 firefox 中調試 service workers 的信息能夠點此查看

雖然,已經將日誌輸出到主控制檯了,可這裏就有個疑問了,主頁能不能獲取 service workers 中的信息哪?答案是確定的,那就是經過 postMessage

經過 postMessage 與主窗口通訊

和 web worker 同樣,service worker 與主窗口通信也須要經過 postMessage,但它的語法又有些許不一樣。

首先,是主頁面給 service worker 發消息。

// ServiceWorkerService.js
const sendMessageToSW = msg => navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg);

if (isSupportServiceWorker()) {
    const sw = navigator.serviceWorker;

    sw.register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'))
        .then(() => sendMessageToSW('Hello, service worker.'))
        .catch(() => console.error('Send message error.'));
} else {
    console.info('Browser not support Service Worker.');
}

能夠看到,postMessage 方法並不在 worker 實例下,而是在 serviceWorker 下的 controller 對象下。這裏須要注意一下,當 service worker 尚未註冊成功時,navigator.serviceWorker.controller 對象的值是 null,因此,在調用 postMessage 以前須要確保 controller 對象已經存在。在 service worker 這邊就沒有什麼區別了

// service-worker.js
_self.addEventListener('message', function(event) {
    console.log(event.data);
});

是否是很簡單?不過,反過來 service worker 給主頁面發消息就要複雜一點了。在 service worker 裏發送信息須要經過 Client 對象的 postMessage 方法。獲取 Client 的方法有不少,好比,剛從主頁面發來的消息,事件的來源就是一個 Client 對象,即 event.source。不過,這隻能向來源發消息,但若是你開了幾個網頁,或者不是經過主頁消息發來的該怎麼辦哪?方法仍是有的,在 service workers 中能夠經過 clients 來獲取全部的頁面對象或其餘的 service workers。

// service-worker.js
_self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
        client.postMessage('Service worker attached.');
    })
});

不過,若是你發出一個消息須要等到另外一方的返回的消息作處理,上述的辦法就作不到了。這時就須要創建一個通道來處理了,修改一下以前的 sendMessageToSW 方法。

// ServiceWorkerService.js
const sendMessageToSW = msg => new Promise((resolve, reject) => {
    const messageChannel = new MessageChannel();
    messageChannel.port1.onmessage = event => {
        if (event.data.error) {
            reject(event.data.error);
        } else {
            resolve(event.data);
        }
    };

    navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg, [messageChannel.port2]);
});

這樣信息發送出去後會返回一個 promise,而後就能夠優雅地鏈式調用了。

// ServiceWorkerService.js
if (isSupportServiceWorker()) {
    const sw = navigator.serviceWorker;

    sw.register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'))
        .then(() => sendMessageToSW('Hello, service worker.'))
        .then(console.log)
        .catch(() => console.error('Send message error.'));
} else {
    console.info('Browser not support Service Worker.');
}

瞭解瞭如何在瀏覽器中調試 service workers 和與主頁面通訊這些基礎以後,就能夠搞一些正真功能性的東西,好比創造 service workers 最初的動機——提供更好的離線體驗。

爲應用添加離線緩存

爲應用添加緩存的方式有不少,但可以提供離線緩存的,據我所知,那就只有 service workers 一家了。這就比如已經安裝了的應用,不管是否有網絡鏈接均可以隨時打開使用(google 所推的 PWA 最終目的就是這個)。你可能會懷疑,聽起來這麼高大上實現起來會不會很複雜?然而並無,使用 service workers 爲應用添加離線緩存仍是至關簡單的。

就如同文章開頭 MDN 中所提到的,service workers 能夠充當應用與服務器以前的代理服務器,它經過監聽 fetch 事件來捕捉本身做用域下發出的網絡請求,並經過 event.respondWith 來返回請求結果,過程當中能夠對返回結果作任何的修改(因此必須 https 啊)。

// service-worker.js
const handleFetchRequest = function(request) {
    return fetch(request);
};

const onFetch = function(event) {
    event.respondWith(handleFetchRequest(event.request));
};

_self.addEventListener('fetch', onFetch);

上面這段代碼就是捕獲請求最基本的方式,而後直接將請求發送出去,並將請求的結果返回,沒有作其餘額外的操做。若是,你這時觀察控制檯的網絡請求,會發現全部請求的 size 都再也不是原先的文件大小或來自緩存,而是 from ServiceWorker

接下去,就來給應用添加離線緩存。既然,全部的請求都是手動發出的,並且可以拿到返回的結果,那麼,緩存這些結果就變得垂手可得了。

不過,這裏要先講另外一個知識點——Cache Storage。它做爲 service worker 的一部分寫在草案中。經過它,咱們能夠方便地把請求,以及請求結果一同緩存起來。瞭解了 Cache Storage,那就把上面的代碼改一下,讓它可以緩存請求。

// service-worker.js
const handleFetchRequest = function(request) {
    return caches.match(request)
        .then(function(response) {
            return response || fetch(request)
                    .then(function(response) {
                        const clonedResponse = response.clone();

                        caches.open(CACHE_NAME)
                            .then(function(cache) {
                                cache.put(request, clonedResponse);
                            });

                        return response;
                    });
        });
};

這裏主要修改瞭如何處理請求的方法,先判斷這個請求是否已經被緩存過了,緩存過了就直接返回結果,沒有的話就去請求,並把結果添加到緩存中,以便下次請求來時能夠直接返回。

離線緩存就這樣添加好了,來看看效果怎麼樣。這就要用到以前調試時所提到的模擬不一樣環境,不記得的童鞋能夠往上翻一翻。(提示關鍵詞:控制檯, Application, Service Workers, Offline)這裏模擬離線環境,設置好後再刷新頁面。

Awesome~?

雖然已實現了離線緩存,可是,使用 Cache Storage 還須要注意如下幾點:

  1. 它只能緩存 GET 請求;

  2. 每一個站點只能緩存屬於本身域下的請求,同時也能緩存跨域的請求,好比 CDN,不過沒法對跨域請求的請求頭和內容進行修改

  3. 緩存的更新須要自行實現;

  4. 緩存不會過時,除非將緩存刪除,而瀏覽器對每一個網站 Cache Storage 的大小有硬性的限制,因此須要清理沒必要要的緩存。

上面的代碼並無作緩存的清除和更新,因此,還要更新一下。同時,經過給跨域請求添加 {mode: 'cors'} 屬性來使請求支持跨域,從而拿到響應頭信息。

const HOST_NAME = location.host;
const VERSION_NAME = 'CACHE-v1';
const CACHE_NAME = HOST_NAME + '-' + VERSION_NAME;
const CACHE_HOST = [HOST_NAME, 'cdn.bootcss.com'];

const isNeedCache = function(url) {
    return CACHE_HOST.some(function(host) {
        return url.search(host) !== -1;
    });
};

const isCORSRequest = function(url, host) {
    return url.search(host) === -1;
};

const isValidResponse = function(response) {
    return response && response.status >= 200 && response.status < 400;
};

const handleFetchRequest = function(req) {
    if (isNeedCache(req.url)) {
        const request = isCORSRequest(req.url, HOST_NAME) ? new Request(req.url, {mode: 'cors'}) : req;
        return caches.match(request)
            .then(function(response) {
                // Cache hit - return response directly
                if (response) {
                    // Update Cache for next time enter
                    fetch(request)
                        .then(function(response) {

                            // Check a valid response
                            if(isValidResponse(response)) {
                                caches
                                    .open(CACHE_NAME)
                                    .then(function (cache) {
                                        cache.put(request, response);
                                    });
                            } else {
                                sentMessage('Update cache ' + request.url + ' fail: ' + response.message);
                            }
                        })
                        .catch(function(err) {
                            sentMessage('Update cache ' + request.url + ' fail: ' + err.message);
                        });
                    return response;
                }

                // Return fetch response
                return fetch(request)
                    .then(function(response) {
                        // Check if we received an unvalid response
                        if(!isValidResponse(response)) {
                            return response;
                        }

                        const clonedResponse = response.clone();

                        caches
                            .open(CACHE_NAME)
                            .then(function(cache) {
                                cache.put(request, clonedResponse);
                            });

                        return response;
                    });
            });
    } else {
        return fetch(req);
    }
};

升級以後,仍是有緩存先拿緩存,這樣比較快,但依舊會在後臺發出請求,若是返回合法的請求,就更新 cache 中的值,那麼,下次訪問時就是此次訪問返回的結果了。

service worker 的 installactivite 事件對象都包含一個 waitUntil 方法,方法接受一個 promise,當 promise 被 resolve 後纔會繼續執行到下一個狀態。若是,想要強制更新緩存,就能夠經過這個方法在 service worker 激活時除舊版本緩存。

// service-worker.js
const onActive = function(event) {
    event.waitUntil(
        caches
            .keys()
            .then(function(cacheNames) {
                return Promise.all(
                    cacheNames.map(function(cacheName) {
                        // Remove expired cache response
                        if (CACHE_NAME.indexOf(cacheName) === -1) {
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
    );
};

_self.addEventListener('activate', onActive);

這樣請求的緩存就能隨時更新了,不過,你可能會和我有一樣的疑問——那 service workers 怎麼更新呢?

Service workers 的生命週期與更新

事實上,service workers 的更新並不須要咱們操心,只要 service workers 文件有任何一點的修改,瀏覽器就會當即裝載它。然而,它仍是有須要注意的地方,否則也就不值一提了。

雖然,瀏覽器當即裝載它,但它並無當即生效,這和它的生命週期有關。下面這張圖來自 Service Workers 101,很是形象地展現了 service workers 的生命週期。

先看圖的右邊,它展現了 service workers 的 3 種狀態:Installing, WaitingActive;左邊是 service workers 的生命週期,二者結合在一塊兒,直觀地展示了在 service workers 不一樣的生命週期時,service workers 所處的狀態。能夠看到,installactivate 2 個時間中間,service workers 是處於 Waiting 的狀態。

回到剛纔提到的 service workers 更新,瀏覽器雖然會當即裝載最新的 service workers,但只是讓它 install,並進入 Waiting 的狀態,而並無當即 activate。只有當用戶將瀏覽器關閉後,從新打開頁面時,舊的 service workers 纔會被新的 service workers 替換。不過,圖中也有提到,能夠在 install 事件中 self.skipWaiting 方法來跳過等待,直接進入 activate 狀態。一樣的,能夠在 activate 事件中調用 self.clients.claim 方法來更新全部客戶端上的 service works。

爲 service workers 添加上述兩個方法就能較好地處理更新問題。代碼改動很小,這裏就再也不重複貼了,全部的代碼都已上傳 Github

下次準備搗鼓 service workers 相關的服務器推送,敬請關注...?

相關文章
相關標籤/搜索