Service Worker的應用

WindrunnerMax發表於2021-12-13

Service Worker的應用

Service worker本質上充當Web應用程式、瀏覽器與網路(可用時)之間的代理伺服器,這個API旨在建立有效的離線體驗,它會攔截網路請求並根據網路是否可用來採取適當的動作、更新來自伺服器的的資源,它還提供入口以推送通知和訪問後臺同步API

描述

Service Worker本質上也是瀏覽器快取資源用的,只不過他不僅僅是Cache,也是通過worker的方式來進一步優化,其基於h5web worker,所以不會阻礙當前js執行緒的執行,其最主要的工作原理,1是後臺執行緒,是獨立於當前網頁執行緒,2是網路代理,在網頁發起請求時代理攔截,來返回快取的檔案。簡單來說Service Worker就是一個執行在後臺的Worker執行緒,然後它會長期執行,充當一個服務,很適合那些不需要獨立的資源資料或使用者互動的功能,最常見用途就是攔截和處理網路請求,以下是一些細碎的描述:

  • 基於web worker(一個獨立於JavaScript主執行緒的獨立執行緒,在裡面執行需要消耗大量資源的操作不會堵塞主執行緒)。
  • web worker的基礎上增加了離線快取的能力。
  • 本質上充當Web應用程式(伺服器)與瀏覽器之間的代理伺服器(可以攔截全站的請求,並作出相應的動作->由開發者指定的動作)。
  • 建立有效的離線體驗(將一些不常更新的內容快取在瀏覽器,提高訪問體驗)。
  • 由事件驅動的,具有生命週期。
  • 可以訪問cacheindexDB
  • 支援推送。
  • 可以讓開發者自己控制管理快取的內容以及版本。

Service worker還有一些其他的使用場景,以及service worker的標準能夠用來做更多使web平臺接近原生應用的事情:

  • 後臺資料同步。
  • 響應來自其它源的資源請求。
  • 集中接收計算成本高的資料更新,比如地理位置和陀螺儀資訊,這樣多個頁面就可以利用同一組資料。
  • 在客戶端進行CoffeeScriptLESSCJS/AMD等模組編譯和依賴管理(用於開發目的)。
  • 後臺服務鉤子。
  • 自定義模板用於特定URL模式。效能增強,比如預取使用者可能需要的資源,比如相簿中的後面數張圖片。
  • 可以配合App ManifestService Worker來實現PWA的安裝和離線等功能。
  • 後臺同步,啟動一個service worker即使沒有使用者訪問特定站點,也可以更新快取。
  • 響應推送,啟動一個service worker向使用者傳送一條資訊通知新的內容可用。
  • 對時間或日期作出響應。
  • 進入地理圍欄(LBS的一種應用)。

示例

實現一個簡單的Service worker應用示例,這個示例可以在斷網的時候同樣可以使用,相關的程式碼在https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker,在這裡就是用原生的Service Worker寫一個簡單示例,直接寫原生的Service Worker比較繁瑣和複雜,所以可以藉助一些庫例如Workbox等,在使用Service Worker之前有一些注意事項:

  • Service worker執行在worker上,也就表明其不能訪問DOM
  • 其設計為完全非同步,同步API(如XHRlocalStorage)不能在service worker中使用。
  • 出於安全考量,Service workers只能由HTTPS承載,localhost本地除錯可以使用http
  • Firefox瀏覽器的使用者隱私模式,Service Worker不可用。
  • 其生命週期與頁面無關(關聯頁面未關閉時,它也可以退出,沒有關聯頁面時,它也可以啟動)。

首先使用Node啟動一個基礎的web伺服器,可以使用anywhere這個包,當然使用其他伺服器都是可以的,執行完命令後訪問http://localhost:7890/即可。另外寫完相關程式碼後建議重啟一下服務,之前我就遇到了無法快取的問題,包括disk cachememory cache,要重啟服務才解決。還有要開啟的連結為localhost,自動開啟瀏覽器可能並不是localhost所以需要注意一下。如果要清理快取的話,可以在瀏覽器控制檯的Application專案中Storage點選Clear site data就能清理在網站中的所有快取了。如果使用express或者koa等伺服器環境,還可以嘗試使用Service Worker來快取資料請求,同樣提供資料請求的path即可。

$ npm install -g anywhere
$ anywhere 7890 # http://localhost:7890/

編寫一個index.html檔案和sw.js檔案,以及引入相關的資原始檔,目錄結構如下,可以參考https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker,當然直接clone下來執行一個靜態檔案伺服器就可以直接使用了。

simple--service-worker
├── static
│   ├── avatar.png
│   └── cache.js
├── index.html
└── sw.js

html中引入相關檔案即可,主要是為了藉助瀏覽器環境,而關注的位置是js

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Service Worker</title>
    <style type="text/css">
        .avatar{
            width: 50px;
            height: 50px;
            border-radius: 50px;
        }
    </style>
</head>
<body>
    <img class="avatar" src="./static/avatar.png">
    <script type="text/javascript">
        navigator.serviceWorker
            .register("sw.js")
            .then(() => {
                console.info("註冊成功");
            })
            .catch(() => {
                console.error("註冊失敗");
            });
    </script>
    <script src="./static/cache.js"></script>
</body>
</html>

使用Service worker的第一步,就是告訴瀏覽器,需要註冊一個Service worker指令碼,在這裡我們直接將其寫到了index.html檔案中了。預設情況下,Service worker只對根目錄/生效,如果要改變生效範圍可以在register時加入第二個引數{ scope: "/xxx"},也可以直接在註冊的時候就指定路徑/xxx/sw.js

navigator.serviceWorker
.register("sw.js")
.then(() => {
    console.info("註冊成功")
}).catch(err => {
    console.error("註冊失敗")
})

一旦登記成功,接下來都是Service worker指令碼的工作,下面的程式碼都是寫在service worker指令碼里面的,登記後,就會觸發install事件,service worker指令碼需要監聽這個事件。首先定義這個cache的名字,相當於是標識這一個快取物件的鍵值,之後的urlsToCache陣列是即將要快取的資料,只要給定了相關的path,連資料請求也是同樣能夠快取的,而不僅僅是資原始檔,當然這邊必須是Get的請求下使用,這是Cache這個API決定的。之後便是進行install,關於event.waitUntil可以理解為new Promise的作用,是要等待serviceWorker執行起來才繼續後邊的程式碼,其接受的實際引數只能是一個Promise。在MDN的解釋是因為oninstallonactivate完成前需要一些時間,service worker標準提供一個waitUntil方法,當oninstall或者onactivate觸發時被呼叫,接受一個promise,在這個promise被成功resolve以前,功能性事件不會分發到service worker。之後便是從caches取出這個CACHE_NAMEkey標識的cache,之後使用cache.addAll將陣列中的path告訴cache,在第一次開啟的時候,Service worker會自動去請求相關的資料並且快取起來,使用Service worker去請求的資料,在Chrome控制檯的Network中會顯示一個小小的齒輪圖示,很好辨認。

const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];

this.addEventListener("install", event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => {
            console.log("[Service Worker]", urlsToCache);
            return cache.addAll(urlsToCache);
        })
    );
});

之後是activated階段,如果是第一次載入sw,在安裝後,會直接進入activated階段,而如果sw進行更新,情況就會顯得複雜一些,流程如下:首先老的swA,新的sw版本為B, B進入install階段,而A還處於工作狀態,所以B進入waiting階段,只有等到Aterminated後,B才能正常替換A的工作。這個terminated的時機有如下幾種方式,1、關閉瀏覽器一段時間。2、手動清除Service Worker3、在sw安裝時直接跳過waiting階段。然後就進入了activated階段,啟用sw工作,activated階段可以做很多有意義的事情,比如更新儲存在Cache中的keyvalue。在下邊的程式碼中,實現了不在白名單的CACHE_NAME就清理,可以在這裡實現一個version也就是版本的控制,之前的版本就要清理等,另外還檢視了一下目前的相關快取。

this.addEventListener("activate", event => {
    // 不在白名單的`CACHE_NAME`就清理
    const cacheWhitelist = ["service-worker-demo"];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
    // 檢視一下快取
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
    );
});

之後便是攔截請求的階段了,該階段是sw關鍵的一個階段,用於攔截代理所有指定的請求,並進行對應的操作,所有的快取部分,都是在該階段。首先我們直接攔截掉所有的請求,在最前邊的判斷操作是為了防止所有的請求都被攔截從而都在worker裡邊發起請求,當然不進行判斷也是可以使用的。然後對於請求如果匹配到了快取,那麼就直接從快取中取得資料,否則就使用fetch去請求新的。另外如果有需要的話我們不需要在事件響應時進行匹配 可以直接將所有發起過的請求快取。

this.addEventListener("fetch", event => {
    const url = new URL(event.request.url);
    if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
        event.respondWith(
            caches.match(event.request).then(resp => {
                if (resp) {
                    console.log("fetch ", event.request.url, "有快取,從快取中取");
                    return resp;
                } else {
                    console.log("fetch ", event.request.url, "沒有快取,網路獲取");
                    return fetch(event.request);
                    // // 如果有需要的話我們不需要在事件響應時進行匹配 可以直接將所有發起過的請求快取
                    // return fetch(event.request).then(response => {
                    //     return caches.open(CACHE_NAME).then(cache => {
                    //         cache.put(event.request, response.clone());
                    //         return response;
                    //     });
                    // });
                }
            })
        );
    }
});

第一次開啟時控制檯的輸出:

cache.js loaded
[Service Worker] (3) ['/', '/static/avatar.png', '/static/cache.js']
註冊成功
(3) [Request, Request, Request]

第二次及之後開啟的控制檯輸出:

fetch  http://localhost:7811/static/avatar.png 有快取,從快取中取
fetch  http://localhost:7811/static/cache.js 有快取,從快取中取
註冊成功
cache.js loaded

至此我們就完成了一個簡單的示例,在第二次開啟頁面的時候,我們可以將瀏覽器的網路連線斷開,例如關閉檔案伺服器或者在控制檯的Network中選擇Offline,而我們也可以看到頁面依舊正常載入,不需要網路服務,另外也可以在Network的相關的資料的Size列會出現(ServiceWorker)這個資訊,說明資源是從ServiceWorker載入的快取資料。可以在https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-workerclone下來後執行這個示例。

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Service Worker</title>
    <style type="text/css">
        .avatar{
            width: 50px;
            height: 50px;
            border-radius: 50px;
        }
    </style>
</head>
<body>
    <img class="avatar" src="./static/avatar.png">
    <script type="text/javascript">
        navigator.serviceWorker
            .register("sw.js")
            .then(() => {
                console.info("註冊成功");
            })
            .catch(() => {
                console.error("註冊失敗");
            });
    </script>
    <script src="./static/cache.js"></script>
</body>
</html>
// sw.js
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];

this.addEventListener("install", event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => {
            console.log("[Service Worker]", urlsToCache);
            return cache.addAll(urlsToCache);
        })
    );
});

this.addEventListener("activate", event => {
    // 不在白名單的`CACHE_NAME`就清理
    const cacheWhitelist = ["service-worker-demo"];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
    // 檢視一下快取
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
    );
});

this.addEventListener("fetch", event => {
    const url = new URL(event.request.url);
    if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
        event.respondWith(
            caches.match(event.request).then(resp => {
                if (resp) {
                    console.log("fetch ", event.request.url, "有快取,從快取中取");
                    return resp;
                } else {
                    console.log("fetch ", event.request.url, "沒有快取,網路獲取");
                    return fetch(event.request);
                    // // 如果有需要的話我們不需要在事件響應時進行匹配 可以直接將所有發起過的請求快取
                    // return fetch(event.request).then(response => {
                    //     return caches.open(CACHE_NAME).then(cache => {
                    //         cache.put(event.request, response.clone());
                    //         return response;
                    //     });
                    // });
                }
            })
        );
    }
});
// cache.js
console.log("cache.js loaded");
// avatar.png
// [byte]png

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://github.com/mdn/sw-test/
https://zhuanlan.zhihu.com/p/25459319
https://zhuanlan.zhihu.com/p/115243059
https://zhuanlan.zhihu.com/p/161204142
https://github.com/youngwind/service-worker-demo
https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g
https://developer.mozilla.org/zh-CN/docs/Web/API/Cache
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://www.bookstack.cn/read/webapi-tutorial/docs-service-worker.md

相關文章