Service Worker的應用


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM