PWA 入門: 理解和創建 Service Worker 腳本
Service Worker 是一段腳本,與 Web Worker 一樣,也是在後台運行。作為一個獨立的線程,運行環境與普通腳本不同,所以不能直接參与 Web 交互行為。Native App 可以做到離線使用、消息推送、後台自動更新,Service Worker 的出現是正是為了使得 Web App 也可以具有類似的能力。
關於手工編寫 Service Worker 腳本, 可以閱讀上一篇文章 PWA 入門: 寫個非常簡單的 PWA 頁面或者查看 Google 給出的例子。這篇文章主要介紹 Service Worker 生命周期, 緩存策略以及如何快速生成配置。
Service Worker 的生命周期
Service Worker 腳本通過 navigator.serviceWorker.register 方法註冊到頁面, 之後可以脫離頁面在瀏覽器後台運行:
if (navigator.serviceWorker) {n navigator.serviceWorker.register(service-worker.js)n .then(function(registration) {n console.log(service worker 註冊成功);n }).catch(function (err) {n console.log(servcie worker 註冊失敗);n });n}n
處於安全原因, Service Worker 腳本的作用範圍不能超出腳本文件所在的路徑。比如地址是 "/sw-test/sw.js" 的腳本只能控制 "/sw-test/" 下的頁面。
Service Worker 從註冊開始需要先 install, 如果 install 成功, 接下來需要 activate, 然後才能接管頁面。但是如果頁面被先前的 Service Worker 控制著, 那麼它會停留在 installed(waiting) 這個階段等到頁面重新打開才會接管頁面, 或者可以通過調用 self.skipWaiting() 方法跳過等待。所以一個 Service Worker 腳本的生命周期有這樣一些階段(從左往右):
- parsed: 註冊完成, 腳本解析成功, 尚未安裝
- installing: 對應 Service Worker 腳本 install 事件執行, 如果事件里有 event.waitUntil() 則會等待傳入的 Promise 完成才會成功
- installed(waiting): 頁面被舊的 Service Worker 腳本控制, 所以當前的腳本尚未激活。可以通過 self.skipWaiting() 激活新的 Service Worker
- activating: 對應 Service Worker 腳本 activate 事件執行, 如果事件里有 event.waitUntil() 則會等待這個 Promise 完成才會成功。這時可以調用 Clients.claim() 接管頁面
- activated: 激活成功, 可以處理 fetch, message 等事件
- redundant: 安裝失敗, 或者激活失敗, 或者被新的 Service Worker 替代掉
Service Worker 腳本最常用的功能是截獲請求和緩存資源文件, 這些行為可以綁定在下面這些事件上:
- install 事件中, 抓取資源進行緩存
- activate 事件中, 遍歷緩存, 清除過期的資源
- fetch 事件中, 攔截請求, 查詢緩存或者網路, 返回請求的資源
更多細節可以參考文章: Service Worker 生命周期
緩存策略
處在 activated 狀態的 Service Worker 可以攔截作用範圍下的頁面的網路請求, 由 Service Worker 監聽 fetch 事件來決定請求如何響應。Service Worker 可以訪問 Cache API, Fetch API 等介面, 獲取數據和完成響應。
self.addEventListener(fetch, function(event) {n event.respondWith(n caches.match(event.request)n .then(function(response) {n // Cache hit - return responsen if (response) {n return response;n }n return fetch(event.request);n }n )n );n});n
event.waitUntil() 接收一個 Promise, 等到 Promise 完成時, 事件才最終完成。
有一些常用的資源緩存的策略, 比如在 sw-toolbox 當中定義的有幾種:
- 網路優先: 從網路獲取, 失敗或者超時再嘗試從緩存讀取
- 緩存優先: 從緩存獲取, 緩存插敘不到再嘗試從網路抓取
- 最快: 同時查詢緩存和網路, 返回最先拿到的
- 僅限網路: 僅從網路獲取
- 僅限緩存: 僅從緩存獲取
藉助 Fetch API 和 Cache API 可以編寫出複雜的策略用來區分不同類型或者頁面的資源的處理方式。詳細的解釋可以看比如 Service workers explained。
官方工具
除了前面提到的手工編寫 Service Worker 腳本, Google 提供了 sw-toolbox 和 sw-precache 兩個工具方便快速生成 service-worker.js 文件:
- sw-precache 可以用來生成配置使 PWA 在安裝時進行靜態資源的緩存
- sw-toolbox 提供了動態緩存使用的通用策略, 這些動態的資源不合適用 sw-precache 預先緩存。同時它提供了一套類似 Express.js 路由的語法, 用於編寫策略
sw-precache 可以通過 Node.js 直接調用來生成 sw.js 文件, 比如配合 Gulp 使用:
gulp.task(generate-service-worker, function(callback) {n var path = require(path);n var swPrecache = require(sw-precache);n var rootDir = app;nn swPrecache.write(`${rootDir}/service-worker.js`, {n staticFileGlobs: [rootDir + /**/*.{js,html,css,png,jpg,gif,svg,eot,ttf,woff}],n stripPrefix: rootDirn }, callback);n});n
其中也可以配置 runtimeConfig 進而調用 sw-toolbox 生成動態緩存的配置, 比如:
{n "staticFileGlobs": [n "app/css/**.css",n "app/**.html",n "app/images/**.*",n "app/js/**.js"n ],n "stripPrefix": "app/",n "runtimeCaching": [{n "urlPattern": "/express/style/path/(.*)",n "handler": "networkFirst"n }]n}n
另外也推薦用 sw-precache 的 Webpack 插件來生成 sw.js。
var path = require(path);nvar SWPrecacheWebpackPlugin = require(sw-precache-webpack-plugin);nnmodule.exports = {n context: __dirname,n entry: {n main: path.resolve(__dirname, src/index),n },n output: {n path: path.resolve(__dirname, src/bundles/),n filename: [name]-[hash].js,n },n plugins: [n new SWPrecacheWebpackPlugin(n {n cacheId: my-project-name,n filename: my-service-worker.js,n maximumFileSizeToCacheInBytes: 4194304,n minify: true,n runtimeCaching: [{n handler: cacheFirst,n urlPattern: /[.]mp3$/,n }],n }n )n ]n}n
可以參考 CodeLabs: Using Cache 以及 API 文檔來了解細節。
結尾
除了攔截請求之外, Service Worker 還能用來做後台通知, Mock 請求, 離線統計等等工作。可以瀏覽 GoogleChrome/samples 了解更多的功能和寫法。
圖片來源
- 頭圖 https://developers.google.com/web/progressive-web-apps/
- Service Worker 生命周期 https://bitsofco.de/the-service-worker-lifecycle/
- Service Worker 攔截請求 https://www.w3ctrain.com/2016/09/17/service-workers-note/
推薦閱讀:
※Progressive Web Apps - Part.3 U4 PWA 特性支持概覽
※Progressive Web Apps - Part.1 為什麼是 PWA?
※SSR 架構項目實現離線可用(思路&案例)