PWA入門之路

volare發表於2019-01-06

由於專案中有個問題涉及到了Service Worker,所以找了時間去研究了一下PWA。也趁此寫一篇文章總結一下。

前言:PWA作為今年最火熱的技術概念之一,對提升Web應用的安全、效能和體驗有著很大的意義,非常值得我們去了解與學習。

什麼是PWA

PWA,全稱Progressive Web App,即漸進式WEB應用, 是提升 Web App 的體驗的一種新方法,能給使用者原生應用的體驗。它的優勢主要體現在:

  • 可在離線網路較差的環境下正常開啟頁面。
  • 安全(HTTPS)。
  • 保持最新(及時更新)。
  • 支援安裝(新增到主螢幕)和訊息推送
  • 向下相容,在不支援相關技術的瀏覽器中仍可正常訪問。

PWA本身其實是一個概念集合,它不是指某一項技術,而是通過一系列的Web技術與Web標準來優化Web App的安全、效能和體驗。其中涉及到的一些技術概念包括但不限於:

  • Web App Manifest
  • Service Worker
  • Cache API 快取
  • Push&Notification 推送與通知
  • Background Sync 後臺同步

本文主要講一下Service Worker相關的東西。

Service Worker

1. 什麼是Service Worker?

Service worker是一個註冊在指定源和路徑下的事件驅動worker。它採用JavaScript控制關聯的頁面或者網站,攔截並修改訪問和資源請求,細粒度地快取資源。你可以完全控制應用在特定情形(最常見的情形是網路不可用)下的表現。

Service worker執行在worker上下文,因此它不能訪問DOM。相對於驅動應用的主JavaScript執行緒,它執行在其他執行緒中,所以不會造成阻塞。它設計為完全非同步,同步API(如XHR和localStorage)不能在service worker中使用。

下圖展示普通Web App與新增了Service Worker的Web App在網路請求上的差異:

img

2. 使用Service Worker

2.1. 註冊Service Worker

在index.js檔案裡面註冊Service Worker。

// index.js
// 註冊service worker,service worker指令碼檔案為sw.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js').then(function () {
        console.log('Service Worker 註冊成功');
    });
}
複製程式碼

值得一提的是,Service Worker裡的各類操作都被設計為非同步,以避免一些長時間的阻塞操作。這些非同步操作都是建立在Promise的基礎上的,如果你對Promise不夠了解,建議去熟悉一下Promise。傳送門:Promise(ES6標準入門)

2.2. 使用Service Worker

Service Worker的生命週期

當我們註冊了Service Worker後,它會經歷生命週期的各個階段,同時會觸發相應的事件。整個生命週期包括了:installing --> installed --> activating --> activated --> redundant。當Service Worker安裝(installed)完畢後,會觸發install事件;而啟用(activated)後,則會觸發activate事件。

下面的例子監聽了install事件

// 在sw.js裡面
// 監聽install事件
self.addEventListener('install', function (e) {
    console.log('Service Worker installed');
});
複製程式碼

self是Service Worker中的一個特殊的全域性變數,類似於windowself指向當前這個Service Worker。

快取靜態資源

一般情況下,我們會列出一個需要快取的資源列表,當Service Worker install時,會將改列表的資源快取下來。

// sw.js
var cacheName = 'v1';
var cacheFiles = [
    '/',
    './index.html',
    './index.js',
    './index.css'
];

// 監聽install事件,安裝完成後,進行檔案快取
self.addEventListener('install', e => {
    console.log(e);
    e.waitUntil(
        caches.open(cacheStorageKey)
        .then(cache => cache.addAll(cacheList))
        .then(_ => self.skipWaiting()) // 該函式可使新的sw.js馬上生效。
    );
})
複製程式碼

看完這段程式碼,你可能會有所疑惑。caches是個什麼鬼東西?

caches是暴露在window作用域的一個變數,我們通過caches屬性訪問CacheStorage

CacheStorage是一種新的本地儲存,它的儲存結構是這樣的:

每個域有若干個儲存模組,每個模組內可以儲存若干個鍵值對。 它的鍵是網路請求(Request),值是請求對應的響應(Response)。 CacheStorage的介面集中在全域性變數caches中,且僅在HTTPS協議(或localhost:*域)下可用。

我們在chrome上的devtool-application中可以看到CacheStorage的相關資訊。

image-20190106172427647.png

介紹變數caches常用方法

  • open(cacheName)

    返回一個 Promise,resolve為匹配 cacheName (如果不存在則建立一個新的cache)的 Cache物件。

  • keys()

    返回一個 Promise ,它將使用一個包含與 CacheStorage 追蹤的所有命名 Cache物件對應字串的陣列來resolve。 使用該方法迭代所有 Cache物件的列表。

Cache物件常用方法

  • match(request, options)

    返回一個 Promise物件,resolve的結果是跟 Cache物件匹配的第一個已經快取的請求。

  • add(request)

    抓取這個URL,檢索並把返回的response物件新增到給定的Cache物件。這在功能上等同於呼叫 fetch(),然後使用 Cache.put() 將response新增到cache中。

  • addAll(requests)

    抓取一個URL陣列,檢索並把返回的response物件新增到給定的Cache物件。

  • put(request, response)

    同時抓取一個請求及其響應,並將其新增到給定的cache。

  • keys(request, options)

    返回一個Promise物件,resolve的結果是Cache物件key值組成的陣列。

更多詳細介紹和方法請查閱MDN-CacheStorageMDN-Cache

看到這裡你可能又會問,Request???Response???

PWA入門之路

這裡跟Fetch API有著密切的關係。

Request物件,用來表示資源的請求。

Response物件,用來表示一次請求的響應資料。

詳細資料傳送門在這裡:RequestResponse

我們列印一下這兩個東西,就非常明瞭了。

image-20190106173445713.png

好了,接下來繼續我們的Service Worker。

我們可以給 service worker 新增一個 fetch 的事件監聽器,接著呼叫 event 上的 respondWith() 方法來劫持我們的 HTTP 響應,然後我們就可以進行一波操作了。

// sw.js
self.addEventListener('fetch', e => {
    e.respondWith(
        caches.match(e.request).then(res => {
            return res || fetch(e.request);
        })
    )
})
複製程式碼

這裡的邏輯是這樣的:

  1. 瀏覽器發起請求,請求各類靜態資源(html/js/css/img);
  2. Service Worker攔截瀏覽器請求,並查詢當前cache;
  3. 若存在cache則直接返回,結束;
  4. 若不存在cache,則通過fetch方法向服務端發起請求,並返回請求結果給瀏覽器。

最終這裡就簡單實現了快取靜態資原始檔的目的了。

更新靜態快取資源

我們通過修改cacheName來達到更新快取資源的目的。由於瀏覽器判斷sw.js是否更新是通過位元組方式,因此修改cacheName會重新觸發install並快取資源。此外,在activate事件中,我們需要檢查cacheName是否變化,如果變化則表示有了新的快取資源,原有快取需要刪除。

self.addEventListener('activate', e => {
    e.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cache => {
                    if (cache !== cacheStorageKey) {
                        return caches.delete(cache);
                    }
                })
            )
        })
    )
    return self.clients.claim();
})
複製程式碼

最後我們可以在network看到請求資源的資訊。

image-20190106175113184.png

資源來自於Service Worker,時間也是在10ms左右,可以說是非常快的載入速度了,這樣的體驗對使用者非常友好。

2.4. Service Worker其他功能

除了快取靜態資原始檔以外,Service Worker還有快取API資料,進行訊息提醒,後臺同步的功能。東西很多,目前還在慢慢探索當中。

參考資料

相關文章