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


1. PWA和Service Worker的關系

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簡介

  • 注意事項

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

    • service worker的生命周期完全獨立於網頁,要為網站安裝服務工作線程,我們需要在頁面業務js代碼中注冊,瀏覽器從指定路徑下載並解析服務工作線程腳本進而瀏覽器將會在后台啟動安裝步驟,在安裝過程中,我們通常會緩存靜態資源,如果所有文件都成功緩存,那么服務工程線程就安裝完畢,如果任何文件下載失敗或緩存失敗,那么安裝步驟將會失敗,當然也不會被激活。安裝后就進入激活步驟,這里是管理舊緩存的絕佳機會(后面代碼示例中將會介紹原因),激活后service worker將開始對其作用域內的所有頁面實施控制。這里需要注意的是,首次注冊 service worker 線程的頁面需要再次加載才會受其控制。在成功安裝完成並處於激活狀態之前,服務工程線程不會收到fetch和push事件;
    • 工作流程

      • 注冊

        • 這里需要注意的是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); })); });
      •  激活

        • 在某個時間點服務工程線程需要更新(例如: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開發者文檔
原文直通車


免責聲明!

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



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