service worker靜態資源離線緩存實踐

前記

早在半年前,在公司內部的前端研習會上,就在研究pwa這個東東了,我負責的特色恰好是用service-worker來實現資源緩存。因此以前就已經嘗試在本地的富途資訊頁面中引入pwa了,後面也在準備着要在正式環境中也引入,可一直沒有好的機會。javascript

而就在上上個星期,收到用戶反饋,種子頁面進不去,一直卡着,上服務器上查看了nginx日誌,發現是資源響應太慢超時了(這個站點沒有使用cdn資源),js等資源沒有加載出來。種子這個頁面又是用前端渲染的,因此用戶就一直白屏了。css

恰好,組內的技術建設一直在準備引入pwa這個東西,此次正好能夠如今種子這個影響面不會太廣而且更新太頻繁的頁面來作實驗,萬一出了問題影響面也不會太大。。前端

因而就吭哧吭哧地開幹了java

首先問了一下公司內的同事,發現並無人在正式環境中引入過sw。。看來我是第一個吃螃蟹的人,刺激。。webpack

一. service worker介紹

service worker的由來

service worker是瀏覽器的一個高級特性,本質是一個web worker,是獨立於網頁運行的腳本。 web worker這個api被造出來時,就是爲了釋放主線程。由於,瀏覽器中的JavaScript都是運行在單一個線程上,隨着web業務變得愈來愈複雜,js中耗時間、耗資源的運算過程則會致使各類程度的性能問題。 而web worker因爲獨立於主線程,則能夠將一些複雜的邏輯交由它來去作,完成後再經過postMessage的方法告訴主線程。 service worker則是web worker的升級版本,相較於後者,前者擁有了持久離線緩存的能力。ios

service worker的特色

sw有如下幾個特色:nginx

  • 獨立於主線程、在後臺運行的腳本
  • 被install後就永遠存在,除非被手動卸載
  • 可編程攔截請求和返回,緩存文件。sw能夠經過fetch這個api,來攔截網絡和處理網絡請求,再配合cacheStorage來實現web頁面的緩存管理以及與前端postMessage通訊。
  • 不能直接操縱dom:由於sw是個獨立於網頁運行的腳本,因此在它的運行環境裏,不能訪問窗口的window以及dom。
  • 必須是https的協議才能使用。不過在本地調試時,在http://localhost 和http://127.0.0.1 下也是能夠跑起來的。
  • 異步實現,sw大量使用promise。

service worker的生命週期

service worker從代碼的編寫,到在瀏覽器中的運行,主要通過下面幾個階段 installing -> installed -> activating -> activated -> redundant; web

installing: 這個狀態發生在service worker註冊以後,表示開始安裝。在這個過程會觸發install事件回調指定一些靜態資源進行離線緩存。chrome

installed: sw已經完成了安裝,進入了waiting狀態,等待其餘的Service worker被關閉(在install的事件回調中,能夠調用skipWaiting方法來跳過waiting這個階段)編程

activating: 在這個狀態下沒有被其餘的 Service Worker 控制的客戶端,容許當前的 worker 完成安裝,而且清除了其餘的 worker 以及關聯緩存的舊緩存資源,等待新的 Service Worker 線程被激活。

activated: 在這個狀態會處理activate事件回調,並提供處理功能性事件:fetch、sync、push。(在acitive的事件回調中,能夠調用self.clients.claim())

redundant: 廢棄狀態,這個狀態表示一個sw的使命週期結束

service worker代碼實現

//在頁面代碼裏面監聽onload事件,使用sw的配置文件註冊一個service worker
 if ('serviceWorker' in navigator) {
        window.addEventListener('load', function () {
            navigator.serviceWorker.register('serviceWorker.js')
                .then(function (registration) {
                    // 註冊成功
                    console.log('ServiceWorker registration successful with scope: ', registration.scope);
                })
                .catch(function (err) {
                    // 註冊失敗
                    console.log('ServiceWorker registration failed: ', err);
                });
        });
    }
複製代碼
//serviceWorker.js
var CACHE_NAME = 'my-first-sw';
var urlsToCache = [
    '/',
    '/styles/main.css',
    '/script/main.js'
];

self.addEventListener('install', function(event) {
    // 在install階段裏能夠預緩存一些資源
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('Opened cache');
                return cache.addAll(urlsToCache);
            })
    );
});

//在fetch事件裏能攔截網絡請求,進行一些處理
self.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request).then(function (response) {
            // 若是匹配到緩存裏的資源,則直接返回
            if (response) {
                return response;
            }
          
            // 匹配失敗則繼續請求
            var request = event.request.clone(); // 把原始請求拷過來

            //默認狀況下,從不支持 CORS 的第三方網址中獲取資源將會失敗。
            // 您能夠向請求中添加 no-CORS 選項來克服此問題,不過這可能會致使「不透明」的響應,這意味着您沒法辨別響應是否成功。
            if (request.mode !== 'navigate' && request.url.indexOf(request.referrer) === -1) 						{
                request = new Request(request, { mode: 'no-cors' })
            }

            return fetch(request).then(function (httpRes) {
								//拿到了http請求返回的數據,進行一些操做
              
              	//請求失敗了則直接返回、對於post請求也直接返回,sw不能緩存post請求
                if (!httpRes  || ( httpRes.status !== 200 && httpRes.status !== 304 && httpRes.type !== 'opaque') || request.method === 'POST') {
                    return httpRes;
                }

                // 請求成功的話,將請求緩存起來。
                var responseClone = httpRes.clone();
                caches.open('my-first-sw').then(function (cache) {
                    cache.put(event.request, responseClone);
                });

                return httpRes;
            });
        })
    );
});


複製代碼

二. service worker在seed中的引入

上面展現了在半年前研究pwa離線緩存時寫的代碼,而此次,真正要在正式環境上使用時,我決定使用webpack一個插件:workbox-webpack-plugin。workbox是google官方的pwa框架,workbox-webpack-plugin是由其產生的其中一個工具,內置了兩個插件:GenerateSWInjectManifest

  • GenerateSW:這個插件會幫你生成一個service worker配置文件,不過這個插件的能力較弱,主要是處理文件緩存和install、activate
  • InjectManifest:這個插件能夠自定義更多的配置,好比fecth、push、sync事件

因爲此次是爲了進行資源緩存,因此只使用了GenerateSW這部分。

//在webpack配置文件裏
		var WorkboxPlugin = require('workbox-webpack-plugin');
		
		new WorkboxPlugin.GenerateSW({
            cacheId: 'seed-cache',

            importWorkboxFrom: 'disabled', // 可填`cdn`,`local`,`disabled`,
            importScripts: '/scripts-build/commseed/workboxswMain.js',

            skipWaiting: true, //跳過waiting狀態
            clientsClaim: true, //通知讓新的sw當即在頁面上取得控制權
            cleanupOutdatedCaches: true,//刪除過期、老版本的緩存
            
            //最終生成的service worker地址,這個地址和webpack的output地址有關
            swDest: '../workboxServiceWorker.js', 
            include: [
                
            ], 
            //緩存規則,可用正則匹配請求,進行緩存
            //這裏將js、css、還有圖片資源分開緩存,能夠區分緩存時間(雖然這裏沒作區分。。)
            //因爲種子農場此站點較長時間不更新,因此緩存時間能夠稍微長一些
            runtimeCaching: [
                {
                    urlPattern: /.*\.js.*/i,
                    handler: 'CacheFirst',
                    options: {
                        cacheName: 'seed-js',
                        expiration: {
                            maxEntries: 20,  //最多緩存20個,超過的按照LRU原則刪除
                            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                        },
                    },
                },
                {
                    urlPattern: /.*css.*/,
                    handler: 'CacheFirst',
                    options: {
                        cacheName: 'seed-css',
                        expiration: {
                            maxEntries: 30,  //最多緩存30個,超過的按照LRU原則刪除
                            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                        },
                    },
                },
                {
                    urlPattern: /.*(png|svga).*/,
                    handler: 'CacheFirst',
                    options: {
                        cacheName: 'seed-image',
                        expiration: {
                            maxEntries: 30,  //最多緩存30個,超過的按照LRU原則刪除
                            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                        },
                    },
                }
            ]
        })
複製代碼
  1. importWorkboxForm和importScripts:

importWorkboxFrom:workbox框架文件的地址,可選cdn、local、disabled

  • cdn:引入google的官方cdn,固然在國內會被強。。pass
  • Local:workboxPlugin會在本地生成workbox的代碼,能夠將這些配置文件一塊兒上傳部署,這樣是每次都要部署一次這個生成的代碼。
  • Disabled:上面兩種都不選用,將生成出來的workbox代碼使用importscript指定js文件從而引入。

我最終選擇的是第三種,由於這樣能夠由本身指定要從哪裏引入,好比之後若是這個站點有了cdn,能夠將這個workbox.js放到cdn上面。目前是將生成的文件,放到script文件夾下。

  1. workbox的策略
    • Stale-While-Revalidate:儘量快地利用緩存返回響應,緩存無效時則使用網絡請求
    • Cache-First:緩存優先
    • Network-First:網絡優先
    • Network-Only:只使用網絡請求的資源
    • Cache-Only:只使用緩存

通常站點的 CSS,JS 都在 CDN 上,SW 並無辦法判斷從 CDN 上請求下來的資源是否正確(HTTP 200),若是緩存了失敗的結果,就很差了。這種狀況下使用stale-while-Revalidate策略,既保證了頁面速度,即使失敗,用戶刷新一下就更新了。

而因爲種子項目的js和css資源都在站點下面,因此這裏就直接使用了cache-first策略。

在webpack中配置好以後,執行webpack打包,就能看到在指定目錄下由workbox-webpack-plugin生成的service worker配置文件了。

接入以後,打開網站,在電腦端的chrome調試工具上能夠看到緩存的資源

接入過程的考慮

  • 前文也有介紹,service worker一旦被install,就永遠存在;若是有一天想要去除跑在瀏覽器背後的這個service worker線程,要手動去卸載。因此在接入以前,我得先知道如何卸載service worker,留好後手:
if ('serviceWorker' in navigator) {
       navigator.serviceWorker.getRegistrations()
           .then(function(registrations) {
				for(let registration of registrations) {
                     //安裝在網頁的service worker不止一個,找到咱們的那個並刪除
                    if(registration && registration.scope === 'https://seed.futunn.com/'){
                        registration.unregister();
                    }
                }
            });
    }
複製代碼
  • 使用service worker緩存了資源,那下次從新發布了,還會不會拉取新的資源呢?這裏也是能夠的,只要資源地址不同、修改了hash值,那麼資源是會從新去拉取並進行緩存的,以下圖,能夠看到對同一個js的不一樣版本,都進行了緩存。

  • 還有個就是對於考慮開發過程的問題,若是之後上線了,sw這個東西安裝下去了,每次打開都直接讀取緩存的資源,那之後在本地調試時怎辦?試了下,chrome的「disabled cache」也沒有用,總不能在本地開發時也給資源打上hash值吧(目前這個項目是在發佈到正式環境時纔會打上hash值)。。而後針對這個問題想了蠻久的,最後發現chrome早有這個設置,在devtool中能夠設置跳過service worker,bypass for network

  • 比起瀏覽器的默認緩存功能,service woker的緩存功能賦予咱們更強大地、更完善地控制緩存的能力。

  • 這個東西其中一個不足在於,尚未不少瀏覽器支持service worker這個東西,蘋果系統是從11.3纔開始支持,因此直到如今,富途nn的app的webview、微信ios版的webview都還不支持service worker這個特性;在安卓上的支持更爲普遍一些,因此此次在種子的優化上,安卓客戶能夠更好地感覺到這個成效。

後記

種子農場加入service worker上線快兩週了,到如今尚未啥問題,彷佛一切都挺順利的。

從最開始研習會上的接觸以後,就一直在想着要準備把它用起來,可一直都有種對於它的不肯定性的畏懼。隨着對它的愈來愈熟悉,此次終於把它搞起來了, 掛念許久的東西可算是有個交代了。。

相關文章
相關標籤/搜索