PWA輔助工具sw-precache、sw-toolbox簡易教程

簡要說明

sw-precache 用來處理預緩存
sw-toolbox 用來處理運行時緩存
sw-precache 默認集成了 sw-toolbox

如果你是太長不想看,下面是使用sw-precache的配置說明

先看一下開發目錄

Working
├─ app
│  ├─ css
│  ├─ images
│  ├─ js
│  ├─ index.html
│  ├─ manifest.json
│  ├─ serviceworker.js
│  ├─ sync.js
│  └─ config.js
├─ node_modules
│  └─ ....
└─ gulpfile.js

gulp的配置

'use strict';

var gulp = require('gulp');
var path = require('path');
var swPrecache = require('sw-precache');

gulp.task('make-service-worker', function(callback) {
    var rootDir = 'app';  // 開發文件和工程文件隔離
    swPrecache.write(path.join(rootDir, 'serviceworker.js'), {
        staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
                          rootDir + '/js/*.js'],  // 避免serviceworker被緩存
        // staticFileGlobs: 需預緩存的靜態資源,路徑是相對于gulpfile的路徑

        stripPrefix: rootDir,  
        // stripPrefix: 跳過的前綴,不加的話生成的serviceworker中尋找緩存資源路徑中都會帶上'app'
        // 因為gulpfile和最終生成的serviceworker不在一個路徑下,gulpfile中尋找資源的路徑必然不能與生成的serviceworker中一致

        importScripts: ['config.js', 'sync.js'],  
        // importScripts: 在servicerworker中引入直接js的文件

        navigateFallback: 'message.html',
        // navigateFallback: 在尋找資源網絡訪問失敗時默認回退到的url(測試不可用)


        /*以上都是預緩存的內容,下面runtimeCaching是運行時緩存,由sw-toolbox控制的*/
        /*urlPattern: 支持以正則的形式捕獲http請求*/
        /*handler: 處理請求的策略,共有五種:cacheOnly, networkOnly, cacheFirst, networkFirst,  Fastest*/
        /*options: 可選參數,這里我們給每一類緩存用不同的緩存名稱存儲,方便查找*/        
        runtimeCaching: [
        {
            urlPattern: /https:\/\/www\.reddit\.com\/api\/subreddits_by_topic.json?query=javascript/,
            handler: 'cacheOnly',
            options: {
                cache: {
                    name: 'subreddits'
                }
            }
        },
        {
            urlPattern: /https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json/,
            handler: 'networkFirst',
            options: {
                cache: {
                    name: 'titles'
                }
            }
        },
        {
            urlPattern: /https:\/\/www\.reddit\.com\/r\/[javascript|node|reactnative|reactjs|web_design]\/comments\/\w{6}\/[\w]{0,255}\.json/,
            handler: 'cacheFirst',
            options: {
                cache: {
                    name: 'articles'
                }
              }
        }],
        verbose: true  // 為每個緩存打出日志
    }, callback);
});

<br />

好吧詳細點來說

一、總覽

本教程將展示如何使用兩個google的pwa輔助庫sw-precachesw-toolbox來幫助更加快速和簡單的創建service worker。這兩個庫可以分開使用也可以一起使用,本教程將使用gulp task來利用這兩個庫創建service woreker。
本教程使用了一個小型的pwa應用 Redder——Redder是Reddit的客戶端,用來讀JavaScript相關的文章。Redder在app中讀取Reddit的文章,在新網頁中讀取其他網站的文章。

二、初始化工程
  1. 下載工程
    https://github.com/NowhereToRun/PWA_caching-with-libraries/archive/master.zip
  2. 設置工作區
$ cd caching-with-libraries
$ mkdir work
$ cp -r step-02/* work
$ cd work
  1. 安裝&運行web server
    Chrome Web Server(需翻墻訪問谷歌商店)( 或者別的HTTP Server,自行啟動)
    點擊CHOOSE FOLDER,選擇路徑work/app,勾選上Automatically show index.html
    打開對應的URL,即可看到基本的網頁。
  2. 安裝依賴庫
$ cd work
$ npm init
$ npm install --save-dev sw-precache

本工程使用gulp來構建,如果沒有安裝gulp,還需以下命令進行安裝

npm install gulp-cli -g
npm install gulp --save-dev
三、相關背景

在創建工程之前,需要明確幾個問題:

  • 我們需要緩存什么資源
  • 什么時候需要進行緩存
  • 怎么緩存

看一下網頁的結構


網頁布局

所有可以緩存的資源可能有以下這些:

  • 基礎的資源,特別是HTML、CSS、Images、可能還需要JS
  • 與JS相關的子目錄列表
  • 文章鏈接和標題
  • 文章內容
緩存類型

預緩存 Precaching
我們需要預緩存APP需要立即使用的資源,并且隨著版本更新而更新. 這是 sw-precache 的主要功能。
運行時緩存 Runtime caching
這是我們緩存所有的其他資源的方法。即運行時緩存包括以下五種類型,
sw-toolbox 都已提供 —— network first, cache first, fastest, cache only, network only. 如果你已經閱讀過 Jake Archibald的 The Offline Cookbook 你將會很熟悉這些內容。
本例子將使用到帶星號的這些策略

  • 網絡優先 Network first *

    • 我們假設讀者希望讀到最新的文章。對于文章的標題,我們總是網絡優先,優先去請求最新的資源。
  • 緩存優先 Cache first *

    • 你對Reddit文章的第一印象會是我們總是想要從網絡上加載它。然而Service worker的代碼可以在 app啟動時子目錄被選中時 后臺加載這些文章。因為文章可能在我們創建后并沒有改變,我們選擇使用緩存優先去瀏覽這些文章。
  • 最快 Fastest

    • 即使本例中沒有使用這個策略,我們仍可以使用這個策略用來緩存文章。在這個策略中,同步請求緩存和網絡。哪個先返回先使用哪一個。
  • 只用緩存 Cache Only *

    • 因為我們改變頻率很低,子目錄subreddits將會在應用第一次加載時獲取,之后都將會從緩存中讀取。在其他情況下,我們可以升級service worker時更新子目錄subreddit的名稱。
  • 只用網絡 Network Only

    • 只用網絡即不使用任何緩存,因為你不想緩存的資源可能被其他的策略所緩存,Network Only給你了一個用來排除指定的路徑,防止被緩存的明確的策略。
四、Gulp配置

work中的gulpfile.js。目前他應該包含這些代碼

'use strict';
var gulp = require('gulp');
var path = require('path');
// Gulp commands go here.
  1. 引入sw-precache庫
var swPrecache = require('sw-precache');
  1. 添加空的gulp task
gulp.task('make-service-worker', function(callback) {
});
  1. 你會注意到work下有app文件夾,包含著web app實際的文件。這樣我們的開發文件(例如gulp file)和應用文件時隔離的。讓我們在變量中標記應用文件的位置,稍后使用。
gulp.task('make-service-worker', function(callback) {
    var rootDir = 'app';
});
  1. 調用 swPrecache.write()
    sw-precache庫的方法write(),可以用來在指定位置創建service worker。在task中添加此方法。
gulp.task('make-service-worker', function(callback) {
    var rootDir = 'app';
    swPrecache.write(path.join(rootDir, 'serviceworker.js'), {
        
    }, callback);
});

write()方法有三個參數
filePath,生成service worker的路徑
options,配置service worker的對象,包含前面提到過的兩種緩存策略precaching和runtime caching。目前為空
callback,我們必須添加gulp callback到sw-precache中
剩下的代碼將會被添加到options對象中。現在,可以執行gulp task

$ gulp make-service-worker
五、預緩存 Precaching

讓我們開始關注業務,我們需要讓service worker做一些事情。

告訴sw-precache需要緩存的資源
首先我們需要precache Redder的app shell。
在options中使用staticFileGlobs字段,它的值為字符串數組。 例如

{staticFileGlobs: [rootDir + '/index.html',
                   rootDir + 'css/styles.css',
                   rootDir + 'images/dog.png'
                  ...], // contents excerpted
}

然而我們并不想把每個文件單獨列出,這樣可能會有漏掉的文件,當文件過多時,代碼也將變得很長。所幸staticFileGlobs使用node glob,所以可以使用以下的形式

{staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
                        rootDir + '/js/*.js']
}

這樣會拿到app shell的所有文件,service worker可以把他們全部緩存在瀏覽器中。
staticFileGlobs屬性告訴sw-precache到哪里去尋找文件,而不是告訴生成的service worker在哪里去獲取這些資源(這句話的意思是,尋找文件的路徑上可能會帶上不需要的路徑,例如app/,在服務啟動時我們是直接在app/路徑下啟動的,所以資源上帶有app會造成瀏覽器在獲取資源時出錯),所以使用stripPrefix來截取資源的前綴。

{staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
                        rootDir + '/js/*.js'],
 stripPrefix: rootDir
}



為什么JS文件要單獨列出來
如果我們在第一行引入,precaching會把service worker和他import的文件全部緩存,這樣是不對的。我們在更新應用時,使用了舊版的Service worker會帶來很多困擾。
因為service worker存在rootDir中,我們可以跳過rootDir,來緩存其他的js文件。

生成service worker

$ gulp make-service-worker

service worker生成在work/app/路徑下

驗證預緩存precaching

六、運行時緩存 Runtime caching

通過給write()的options對象添加參數,可以配置運行時緩存的策略。運行時緩存必須的兩個參數是urlPatternhandler,有些緩存策略可能會需要更多。參數配置類似下面這種形式。其中urlPattern支持正則匹配。

runtimeCaching: [
{
        urlPattern: /some regex/,
        handler: 'cachingStrategy'
},
{
        urlPattern: /some regex/,
        handler: 'cachingStrategy'
}
// Repeat as needed.
],

緩存文章標題
如果你偷看final/中的gulpfile.js,你可以發現為三類內容使用了三類緩存策略。
我們首先來看文章標題的緩存。
因為標題變化頻率高,使用網絡優先緩存策略。
swPrecache.write()stripPrefix字段后添加runtimeCaching屬性。
子目錄的標題名稱由以下url返回
http://www.reddit.com/r/subredit_name.json
正則形式為 https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json
所以配置如下:

runtimeCaching: [
{
        urlPattern: /https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json/,
        handler: 'networkFirst'
}],

使用正確的緩存
因為我們使用了三種不同的緩存策略(自行把final下的三種緩存的代碼拷過來吧...),存儲了標題、文章、子目錄。我們需要給cache特定的名稱來區別他們,給runtimeCaching數組中的對象添加帶有cache屬性的第三個參數options,配置緩存名稱。

runtimeCaching: [
{
        urlPattern: /https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json/,
                handler: 'networkFirst',
                options: {
                        cache: {
                                name: 'titles'
                        }
                }
}],

再次執行命令,刷新網頁查看效果

$ gulp make-service-worker

后臺同步運行時緩存
Redder有一個額外的技巧,使用后臺同步來預填充運行時緩存。對子目錄,標題和文章都會執行。它是怎么工作的和怎么觸發的并不是本次教程的重點,但是后面會介紹到。
給write方法添加importScript參數,可以在service worker中import js文件。

importScripts: ['sync.js']
七、Debugging 緩存

開啟debugging
sw-toolbox庫有debug開關,打開后sw-precache可以輸出信息到DevTools的console中。
我們可以在service worker中添加toolbox.options.debug = true;來開啟debug
但是這樣會每次生成service worker都需要手動輸入
于是我們把這段代碼寫在config.js中,如果需要開啟debug模式,在importScript中引入config.js即可。
打開console可以看到

注意到輸出信息
[sw-toolbox] preCache List: (none)
這并不是個錯誤。sw-toolbox庫可以與sw-precache分割使用,擁有自己的precaching能力,因為我們沒有使用這個特征,我們才看到了這段message。
模擬離線和低延時環境
選擇不同的子標題,我們會看到下面的輸出

sw-toolbox輸出了對應url的緩存策略。
在network中選擇offline,再點擊之前點擊過的子列表,可看到以下輸出


可看到已切換到緩存,頁面也可正常展示。

添加導航回退
在offline模式下點擊沒有點擊過得子列表,必然獲取不到相關數據。為此,我們希望創建一個回退頁面,以顯示所請求的資源不可用。添加以下配置:

navigateFallback: 'message.html'

為了能啟用message.html必須precache
對于這個功能,測試失敗,還沒搞懂為什么,查看了一下生成的service worker

self.addEventListener('fetch', function(event) {
  if (event.request.method === 'GET') {
    // Should we call event.respondWith() inside this fetch event handler?
    // This needs to be determined synchronously, which will give other fetch
    // handlers a chance to handle the request if need be.
    var shouldRespond;

    // First, remove all the ignored parameters and hash fragment, and see if we
    // have that URL in our cache. If so, great! shouldRespond will be true.
    var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching);
    shouldRespond = urlsToCacheKeys.has(url); 
    // If shouldRespond is false, check again, this time with 'index.html'
    // (or whatever the directoryIndex option is set to) at the end.
    var directoryIndex = 'index.html';
    if (!shouldRespond && directoryIndex) {
      url = addDirectoryIndex(url, directoryIndex);
      shouldRespond = urlsToCacheKeys.has(url);
    }

    // If shouldRespond is still false, check to see if this is a navigation
    // request, and if so, whether the URL matches navigateFallbackWhitelist.
    var navigateFallback = 'message.html';
    if (!shouldRespond &&
        navigateFallback &&
        (event.request.mode === 'navigate') &&
        isPathWhitelisted([], event.request.url)) {
      url = new URL(navigateFallback, self.location).toString();
      shouldRespond = urlsToCacheKeys.has(url);
    }

    // If shouldRespond was set to true at any point, then call
    // event.respondWith(), using the appropriate cache key.
    if (shouldRespond) {
      event.respondWith(
        caches.open(cacheName).then(function(cache) {
          return cache.match(urlsToCacheKeys.get(url)).then(function(response) {
            if (response) {
              return response;
            }
            throw Error('The cached response that was expected is missing.');
          });
        }).catch(function(e) {
          // Fall back to just fetch()ing the request if some unexpected error
          // prevented the cached response from being valid.
          console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
          return fetch(event.request);
        })
      );
    }
  }
});

shouldRespond:service worker會檢測多次是否需要使用緩存響應,前面的不命中才會執行后面的檢測。
對回退的檢測放在最后

    var navigateFallback = 'message.html';
    if (!shouldRespond &&
        navigateFallback &&
        (event.request.mode === 'navigate') &&
        isPathWhitelisted([], event.request.url)) {
      url = new URL(navigateFallback, self.location).toString();
      shouldRespond = urlsToCacheKeys.has(url);
    }

針對于回退頁面的檢測的四個條件
!shouldRespond: true
navigateFallback: true
isPathWhitelisted(): 對于沒有白名單的(第一個參數,數組為空),默認返回true
event.request.mode === 'navigate'這個不知道什么情況下會觸發
留著這個問題,以后再來

來看一下后臺同步緩存

頁面在腳本加載完畢后會調用redder.js中的getReddit方法

function getReddit() {
  fetchSubreddits();
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(reg => {
      return reg.sync.register('subreddits');
    });
  }
  var anchorLocation = window.location.href.indexOf('#');
  if (anchorLocation != -1) {
    fetchTopics(window.location.href.slice(anchorLocation + 1));
  }
}

第五行 reg.sync.register('subreddits');觸發后臺同步
sync.js中監聽同步事件,并發出對應請求。
針對于本應用
在頁面初始化時會同步請求子目錄
在點擊子目錄展示本目錄內的文章時,會同步請求所有符合規則的文章內容,相當于做到了預加載。

self.addEventListener('sync', function (event) {
    if (event.tag == 'articles') {
        console.log('in sync articles');
        syncArticles();
    } else if (event.tag == 'subreddits') {
        console.log('in sync subreddits');
        syncSubreddits();
    }
});

Web應用程序通常在不可靠網絡的環境中運行(eg:手機)和未知的生命周期(瀏覽器可能關閉或用戶點擊跳轉了)。這使得很難同步web app客戶端與服務端的數據(如照片上傳,文檔變更,或電子郵件)。如果在同步完成之前瀏覽器關閉或用戶跳轉,數據同步將會中斷,直到用戶再次使用這個頁面并再次嘗試。此規范提供了一個新的serviceworker事件onsync,即使在數據最初請求時網絡情況不佳,仍可以在后臺進行同步操作。這個API是為了減少內容創建和與服務端內容同步的時間。

同步請求會在觸發時立刻執行,如果網絡狀況不好,會run the event at the soonest convenience
當已不再會執行更多的請求時,event.lastChance置為true,用戶可自行決定如何提示。

參考資料:

codelab,源碼的文章標題顯示邏輯有問題,稍作了修改
sync API
詳細文檔,It is not a W3C Standard nor is it on the W3C Standards Track.
sw-precache => gulp
sw-precache => webpack

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容