簡要說明
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-precache
和sw-toolbox
來幫助更加快速和簡單的創建service worker。這兩個庫可以分開使用也可以一起使用,本教程將使用gulp task來利用這兩個庫創建service woreker。
本教程使用了一個小型的pwa應用 Redder——Redder是Reddit的客戶端,用來讀JavaScript相關的文章。Redder在app中讀取Reddit的文章,在新網頁中讀取其他網站的文章。
二、初始化工程
$ cd caching-with-libraries
$ mkdir work
$ cp -r step-02/* work
$ cd work
- 安裝&運行web server
Chrome Web Server(需翻墻訪問谷歌商店)( 或者別的HTTP Server,自行啟動)
點擊CHOOSE FOLDER,選擇路徑work/app,勾選上Automatically show index.html
打開對應的URL,即可看到基本的網頁。 - 安裝依賴庫
$ 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.
- 引入sw-precache庫
var swPrecache = require('sw-precache');
- 添加空的gulp task
gulp.task('make-service-worker', function(callback) {
});
- 你會注意到work下有app文件夾,包含著web app實際的文件。這樣我們的開發文件(例如gulp file)和應用文件時隔離的。讓我們在變量中標記應用文件的位置,稍后使用。
gulp.task('make-service-worker', function(callback) {
var rootDir = 'app';
});
- 調用 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對象添加參數,可以配置運行時緩存的策略。運行時緩存必須的兩個參數是urlPattern
和handler
,有些緩存策略可能會需要更多。參數配置類似下面這種形式。其中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