service worker在移動端H5項目的應用

1. PWA和Service Worker的關係 html

PWA (Progressive Web Apps) 不是一項技術,也不是一個框架,咱們能夠把她理解爲一種模式,一種經過應用一些技術將 Web App 在安全、性能和體驗等方面帶來漸進式的提高的一種 Web App的模式。對於 webview 來講,Service Worker 是一個獨立於js主線程的一種 Web Worker 線程, 一個獨立於主線程的 Context,可是面向開發者來講 Service Worker 的形態其實就是一個須要開發者本身維護的文件,咱們假設這個文件叫作 sw.js。經過 service worker 咱們能夠代理 webview 的請求至關因而一個正向代理的線程,fiddler也是幹這些事情),在特定路徑註冊 service worker 後,能夠攔截並處理該路徑下全部的網絡請求,進而實現頁面資源的可編程式緩存,在弱網和無網狀況下帶來流暢的產品體驗,因此 service worker 能夠看作是實現pwa模式的一項技術實現。前端

2. service worker簡介vue

  • 注意事項webpack

    • service worker 是一種JS工做線程,沒法直接訪問DOM, 該線程經過postMessage接口消息形式來與其控制的頁面進行通訊;
    • service worker 普遍使用了Promise,這些在接下來代碼示例中將會看到;
    • 目前並非全部主流瀏覽器支持 service worker, 能夠經過 navigator && navigator.serviceWorker 來進行特性探測;
    • 在開發過程當中,能夠經過 localhost 使用服務工做線程,如若上線部署,必需要經過https來訪問註冊服務工做線程的頁面,但有種場景是咱們的測試環境可能並不支持https,這時就要經過更改host文件將localhost指向測試環境ip來巧妙繞過該問題(例如:192.168.22.144 localhost);
  • 生命週期web

    • service worker的生命週期徹底獨立於網頁,要爲網站安裝服務工做線程,咱們須要在頁面業務js代碼中註冊,瀏覽器從指定路徑下載並解析服務工做線程腳本進而瀏覽器將會在後臺啓動安裝步驟,在安裝過程當中,咱們一般會緩存靜態資源,若是全部文件都成功緩存,那麼服務工程線程就安裝完畢,若是任何文件下載失敗或緩存失敗,那麼安裝步驟將會失敗,固然也不會被激活。安裝後就進入激活步驟,這裏是管理舊緩存的絕佳機會(後面代碼示例中將會介紹緣由),激活後service worker將開始對其做用域內的全部頁面實施控制。這裏須要注意的是,首次註冊 service worker 線程的頁面須要再次加載纔會受其控制。在成功安裝完成並處於激活狀態以前,服務工程線程不會收到fetch和push事件;
    • 工做流程chrome

      • 註冊npm

        • 這裏須要注意的是register方法註冊服務工做線程文件的位置,該path就是默認的 serviceworker 的做用域,例如註冊path爲/a/b/service-worker.js,則默認scope爲/a/b/,固然也能夠經過傳入{scope: '/a/b/c/'}來指定本身的scope,但這裏要特別注意的是,傳入的scope參數必定是在默認做用域範圍內再自定義(例如/a/b/c/),反之自定義爲/d/e/就不行;
        • 通俗來說,上面提到的scope就是 service worker 可以控制和發揮做用的範圍;
        • 注意註冊是在本身的業務代碼中進行,後面會有具體經過插件來實現註冊的代碼示例;
        if(navigator && navigator.serviceWorker) {
            navigator.serviceWorker.register('/service-worker.js').then(function (registration) {
                console.log(registration)
            }).catch(function (err) {
                console.log(err)
            })
        }
      • 安裝編程

        • 下面代碼就是前面註冊的service-worker.js文件內容;
        • 咱們經過install事件來定義安裝步驟,經過緩存名稱調用caches.open(), 以後再調用cache.addAll()並傳入具體緩存文件清單數組,這是一個Promise鏈式event.waitUntil()方法帶有Promise參數並使用它來判斷花費耗時以及安裝是否成功;
        • 正如前面提到,安裝過程當中若是全部清單中文件成功緩存,則安裝結束,不然安裝過程視爲失敗,因此在實踐中咱們儘量緩存核心資源以免服務工做線程未能安裝;
        var cacheVersion = 'test_2017122608';
        // 安裝服務工做線程
        self.addEventListener('install', function(event){
            // 須要緩存的資源
            var cacheFiles = [
                '/dist/index.html',
                '/dist/js/index_async_bundle.js'
            ];
            console.log('service worker: run into install');
            event.waitUntil(caches.open(cacheVersion).then(function(cache)
            {
                return cache.addAll(cacheFiles);
            }));
        });
      •  激活segmentfault

        • 在某個時間點服務工程線程須要更新(例如:service-worker.js文件發生更改並上線),用戶訪問頁面時瀏覽器會嘗試在後臺從新下載service-worker.js,若是服務工程線程文件與當前所用文件存在字節差別,則將其視爲「新服務工做線程」;
        • 新服務工做線程將會啓動,且將會觸發 install 事件;
        • 此時舊的服務工做線程仍將控制着當前頁面,所以新服務工做線程將會進入waiting狀態;
        • 當網站當前頁面關閉時,舊服務工做線程將會終止,新服務工做線程將會取得控權;
        • 新服務工做線程取得控制權後,將會觸發 activate 事件;
        • 監聽 activate 事件的回調函數中常見的任務是管理緩存,前面我也提到過這是管理舊緩存的絕佳時機,由於若是在安裝步驟中清理了舊緩存,因爲舊的服務工做線程仍舊控制着頁面,將沒法從緩存中提取文件,可是在 activate 時舊服務工做線程已經終止了頁面控制權,所在在這裏清理舊緩存再合適不過;
        // 新的service worker線程被激活(其實和離線包同樣存在"二次生效"的機理)
        self.addEventListener('activate', function (event) {
            console.log('service worker: run into activate');
            event.waitUntil(caches.keys().then(function (cacheNames) {
                return Promise.all(cacheNames.map(function (cacheName) {
                    // 注意這裏cacheVersion也能夠是一個數組
                    if(cacheName !== cacheVersion){
                        console.log('service worker: clear cache' + cacheName);
                        return caches.delete(cacheName);
                    }
                }));
            }));
        });
      • 監聽數組

        • 這裏經過監聽fetch事件來代理響應,進而實現自定義前端資源緩存;
        • 在event.respondWith()中咱們傳入來自caches.match()的一個promise,此方法攔截請求並從服務工做線程所建立的任何緩存中查找緩存結果,如若發現匹配的響應則返回緩存的值,不然,將會調用fetch以代理髮出網絡請求,並將從網絡中檢索的數據做爲結果返回;
        • 若是但願連續性緩存新的請求,則注意註釋的代碼部分,其經過cache.put來將請求的響應添加到緩存來實現;
        • 在fetch請求中添加對then()的回調,得到響應後執行檢查,並clone響應,注意這樣處理的緣由是該響應是stream,主體只能使用一次,咱們須要返回能被瀏覽器使用的響應,還要傳遞到緩存以供使用,所以須要克隆一份副本;
        // 攔截請求並響應
        self.addEventListener('fetch', function (event) {
            console.log('service worker: run into fetch');
            event.respondWith(caches.match(event.request).then(function (response) {
                // 發現匹配的響應緩存
                if(response){
                    console.log('service worker 匹配並讀取緩存:' + event.request.url);
                    return response;
                }
                console.log('沒有匹配上:' + event.request.url);
                return fetch(event.request);
                /*var fetchRequest = event.request.clone();
                return fetch(fetchRequest).then(function(response){
                    if(!response || response.status !== 200 || response.type !== 'basic'){
                        return response;
                    }
                    var responseToCache = response.clone();
                    caches.open(cacheVersion).then(function (cache) {
                        console.log(cache);
                        cache.put(fetchRequest, responseToCache);
                    });
                    return response;
                });*/
            }));
        });

3. 前端資源緩存演進

  • 利用webview自身的http緩存機制。這裏每每須要服務器運維同事配合,對於前端來說不夠靈活且緩存粒度太粗,並且在http協議在不一樣版本下緩存機制有必定的差別(例如1.0版本中If-Modified-Since、Last-Modified、expires, 1.1版本中對緩存進行了優化,添加If-None-Match、Etag、cache-control等;
  • 離線包策略,其大體原理是經過將靜態資源打包至離線管理平臺(自行開發),在app啓動時從離線管理平臺拉取資源包並存放於本地,後續終端將會攔截url請求並基於約定規則將請求代理到本地文件系統,進而加快靜態資源的訪問以及爲cdn減壓,該方案的缺陷在於須要離線資源管理平臺和終端的配合,牽扯資源過多,但其優勢是不存在兼容性問題;
  • h5離線緩存manifest,其實質就是一個緩存清單文件(xx.manifest),而後在html標籤設置manifest屬性爲xx.manifest,該緩存方案也存在「二次更新」的問題,該方案須要注意的問題是xx.manifest文件自身不要被webview緩存,且manifest文件cache部分不能使用通配符,必須手動指定,不過好在能夠經過構建工具來解決,主流瀏覽器對該方案支持度也不錯。與service worker相對,其業務JS代碼沒法感知緩存更新的時機,因此service worker方案更具備想象空間;
  • service worker 經過一個獨立JS線程來實現資源的可編程式緩存;

4. 項目如何快速接入service worker

  • 在接入前有兩個問題擺在咱們面前,service worker能夠幫助咱們解決資源緩存問題,有緩存就必需要有更新的機制,service-worker.js自己也會被瀏覽器緩存,後續產品迭代過程當中如何解決該文件自身的更新問題,不然其餘資源的緩存更新也就無從談起(舊的服務工做線程將一直控制頁面),無可厚非每次構建部署時service-worker.js須要攜帶版本號(例如?v=201801021721),固然也能夠在服務器運維層控制該文件的cache-control: no-cache從而規避瀏覽器緩存問題,但這樣太麻煩;
  • 咱們是在業務代碼中經過register的方式引入service-worker.js, 那問題就變爲如何在註冊服務工做線程的位置引入版本號呢,咱們能夠經過sw-register-webpack-plugin來解決該問題,其思路是將服務工做線程的註冊放在一個單獨的文件中(sw-register.js),而後自動在頁面入口(例如index.html)寫入一段JS腳原本動態加載sw-register.js文件,這裏sw-register.js的加載路徑是帶有實時時間戳的,而生成的sw-register.js文件內容中註冊service-worker.js的位置自動攜帶構建版本號參數(默認是當前構建時間),該插件配置以下(基於webpack構建的項目):

    let SwRegisterWebpackPlugin = require('sw-register-webpack-plugin')
    ...
    plugins: [
        new SwRegisterWebpackPlugin({
            filePath: path.resolve(__dirname, '../src/sw-register.js')
        })
    ]
  • 構建後html新增部分如圖:

html新增部分

  • 構建後生成的sw-register.js文件變化如圖:

圖片描述

  • 這樣處理後,sw-register.js文件就不會被瀏覽器緩存,也即每次刷新會多一次sw-register.js的文件請求,因爲它只是用來作註冊的工做,體量不會太大,能夠接受,關鍵是前端能夠自行控制
  • 已緩存資源文件如何更新呢?上述插件只是解決了service-worker.js文件自己的更新的問題(保證每次構建部署後會新啓一個服務工做線程),但對於service-worker.js文件中定義的cacheFiles而言,當咱們修改了已緩存文件後如何來更新緩存呢,個人項目是基於vue.js + webpack,打包後的JS文件是[name].[hash].[ext]格式,從前面的介紹可知資源的緩存也是基於url(做爲key)來的,不可能每次構建後都手動去調整service-worker.js文件內容中cacheFiles的路徑值吧,應該是將構建後的文件名(包括路徑)直接放到service-worker.js內容中,看到這裏你應該想到了有webpack插件已經幫咱們作好了,那就是sw-precache-webpack-plugin,該插件會自動在dist目錄下生成service-worker.js文件,供給service worker運行,也就是說service-worker.js文件自己不須要咱們手動添加了,但問題是咱們如何自定義須要緩存的文件呢,該插件的配置參數會告訴你,個人項目該插件配置以下:

    // 生成service-worker.js和配置緩存清單
    new SwPrecacheWebpackPlugin({
        cacheId: 'attendance-mobile-cache',
        filename: 'service-worker.js',
        minify: true,
        dontCacheBustUrlsMatching: false,
        staticFileGlobs: [
            'dist/static/js/manifest.**.*',
            'dist/static/js/vendor.**.*',
            'dist/static/js/app.**.*'
        ],
        stripPrefix: 'dist/'
    })
  • 由上可知,咱們可以經過正則來匹配須要緩存的文件,這裏特別要注意的是stripPrefix參數的使用,咱們配置的緩存文件路徑是項目中的路徑,但對於部署線上而言,咱們可能須要過濾前綴的部分路徑(個人項目線上部署文件根目錄下就是static等,因此須要過濾dist路徑),最終該插件生成的service-worker.js文件如圖所示(僅截取緩存文件清單部分代碼)

圖片描述

4. 調試service worker

  • 經過上述兩個插件,咱們的service-worker接入工做基本完成,那接下來就是驗證服務工做線程運行是否ok,經過chrome devTools(Application項)咱們能夠很方面的查看當前服務工做線程的運行狀況和已緩存了哪些文件,具體如何查看這裏再也不介紹;
  • 當首次運行 service worker 時咱們會發現要緩存的文件仍是走正常的網絡請求,cache storage 下也看不到咱們的緩存項,由於服務工程線程也存在「二次生效」的機制(即便須要緩存的資源延遲加載),具體以下圖所示:

圖片描述
圖片描述

  • 經過刷新訪問咱們能夠看到,service worker 緩存文件已經生效,在network面板下自定義的緩存文件size項都顯示爲「from ServiceWorker」, 耗時也明顯很低。在cache storage下面也能夠看到已經緩存的文件列表,具體以下圖所示:

圖片描述
圖片描述

  • 接下來咱們更新service-worker.js文件來看下新服務工做線程如何工做,正如前面所講新服務工做線程將會啓動安裝,但因爲舊服務工做線程控制着頁面,因此新服務工做線程將進入waiting狀態,噹噹前打開的頁面關閉時,舊服務工做線程將會被終止,新服務工做線程會得的控制權並觸發activate事件,在開發過程當中咱們須要經過Chrome Devtools的skipWaiting或者勾選Updated on reload來強制激活新服務工做線程,具體以下圖所示:

圖片描述

  • 在開發過程當中咱們能夠經過上述來了解新服務工做線程的更新流程,但在實際項目中咱們能夠經過self.skipWaiting()跳過等待過程安裝後直接激活,通常咱們在install事件中調用,具體可參見sw-precache-webpack-plugin生成的service-worker源代碼。這會致使新服務工做線程將當前活動的工做線程逐出,skipWaiting()意味着新服務工做線程可能會控制使用較舊工做線程加載的頁面,也就是頁面獲取的部分數據由舊工做線程處理,而新服務工做線程處理後來獲取的數據,若是有問題就不要使用skipWaiting();
  • 手動清理service worker緩存後刷新頁面,在 Network 面板中,咱們會看到本應緩存文件的一組初始請求。以後是前面帶有齒輪圖標的第二輪請求,這些請求彷佛要獲取相同的資源,「齒輪」圖標表明這些請求來自服務工做線程,若是不unregsiter該服務工做線程,咱們會發現即便屢次刷新頁面,Network 面板依然如此,其實也就是說資源沒有再次緩存(由於服務工做線程已經安裝且控制當前頁面,刷新操做不會從新觸發install事件,也就不會再次添加資源到緩存,除非unregister或者更新service-worker.js文件),具體以下圖所示:

圖片描述
圖片描述

5. 異常回滾(註銷)

  • 某些場景下若是service worker使用出現異常,好比不一樣頁面間 service worker 控制的scope存在「重疊污染」的問題,那麼咱們就須要緊急回滾(撤銷)當前 service worker,在開發環境很好解決,咱們依然能夠經過Chrome Devtools來進行unregister, 那麼在線上環境已經有服務工做線程在運行的狀況下呢,咱們須要在新上線版本的service worker註冊前將被污染或者異常的service worker註銷掉,具體代碼以下:

    if (navigator.serviceWorker) {
        navigator.serviceWorker.getRegistrations().then(function (registrations) {
            for (var item of registrations) {
                if (item.scope === 'http://localhost/attendance-mobile/dist/') {
                    item.unregister();
                }
            }
            // 註銷掉污染 Service Worker 以後再從新註冊...
        });
    }
備註:文中部份內容摘選自Google開發者文檔
相關文章
相關標籤/搜索