簡要說明
sw-precache 用來處理預(yù)緩存
sw-toolbox 用來處理運行時緩存
sw-precache 默認集成了 sw-toolbox
如果你是太長不想看悔醋,下面是使用sw-precache的配置說明
先看一下開發(fā)目錄
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'; // 開發(fā)文件和工程文件隔離
swPrecache.write(path.join(rootDir, 'serviceworker.js'), {
staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
rootDir + '/js/*.js'], // 避免serviceworker被緩存
// staticFileGlobs: 需預(yù)緩存的靜態(tài)資源摩窃,路徑是相對于gulpfile的路徑
stripPrefix: rootDir,
// stripPrefix: 跳過的前綴,不加的話生成的serviceworker中尋找緩存資源路徑中都會帶上'app'
// 因為gulpfile和最終生成的serviceworker不在一個路徑下,gulpfile中尋找資源的路徑必然不能與生成的serviceworker中一致
importScripts: ['config.js', 'sync.js'],
// importScripts: 在servicerworker中引入直接js的文件
navigateFallback: 'message.html',
// navigateFallback: 在尋找資源網(wǎng)絡(luò)訪問失敗時默認回退到的url(測試不可用)
/*以上都是預(yù)緩存的內(nèi)容猾愿,下面runtimeCaching是運行時緩存鹦聪,由sw-toolbox控制的*/
/*urlPattern: 支持以正則的形式捕獲http請求*/
/*handler: 處理請求的策略,共有五種:cacheOnly, networkOnly, cacheFirst, networkFirst, Fastest*/
/*options: 可選參數(shù)蒂秘,這里我們給每一類緩存用不同的緩存名稱存儲泽本,方便查找*/
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
來幫助更加快速和簡單的創(chuàng)建service worker姻僧。這兩個庫可以分開使用也可以一起使用规丽,本教程將使用gulp task來利用這兩個庫創(chuàng)建service woreker。
本教程使用了一個小型的pwa應(yīng)用 Redder——Redder是Reddit的客戶端段化,用來讀JavaScript相關(guān)的文章嘁捷。Redder在app中讀取Reddit的文章,在新網(wǎng)頁中讀取其他網(wǎng)站的文章显熏。
二雄嚣、初始化工程
- 下載工程
https://github.com/NowhereToRun/PWA_caching-with-libraries/archive/master.zip - 設(shè)置工作區(qū)
$ 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
打開對應(yīng)的URL,即可看到基本的網(wǎng)頁蕴轨。 - 安裝依賴庫
$ cd work
$ npm init
$ npm install --save-dev sw-precache
本工程使用gulp來構(gòu)建港谊,如果沒有安裝gulp,還需以下命令進行安裝
npm install gulp-cli -g
npm install gulp --save-dev
三橙弱、相關(guān)背景
在創(chuàng)建工程之前歧寺,需要明確幾個問題:
- 我們需要緩存什么資源
- 什么時候需要進行緩存
- 怎么緩存
看一下網(wǎng)頁的結(jié)構(gòu)
所有可以緩存的資源可能有以下這些:
- 基礎(chǔ)的資源,特別是HTML棘脐、CSS斜筐、Images、可能還需要JS
- 與JS相關(guān)的子目錄列表
- 文章鏈接和標題
- 文章內(nèi)容
緩存類型
預(yù)緩存 Precaching
我們需要預(yù)緩存APP需要立即使用的資源蛀缝,并且隨著版本更新而更新. 這是 sw-precache 的主要功能顷链。
運行時緩存 Runtime caching
這是我們緩存所有的其他資源的方法。即運行時緩存包括以下五種類型屈梁,
sw-toolbox 都已提供 —— network first, cache first, fastest, cache only, network only. 如果你已經(jīng)閱讀過 Jake Archibald的 The Offline Cookbook 你將會很熟悉這些內(nèi)容嗤练。
本例子將使用到帶星號的這些策略
-
網(wǎng)絡(luò)優(yōu)先 Network first *
- 我們假設(shè)讀者希望讀到最新的文章。對于文章的標題在讶,我們總是網(wǎng)絡(luò)優(yōu)先煞抬,優(yōu)先去請求最新的資源。
-
緩存優(yōu)先 Cache first *
- 你對Reddit文章的第一印象會是我們總是想要從網(wǎng)絡(luò)上加載它构哺。然而Service worker的代碼可以在 app啟動時 和 子目錄被選中時 后臺加載這些文章此疹。因為文章可能在我們創(chuàng)建后并沒有改變,我們選擇使用緩存優(yōu)先去瀏覽這些文章。
-
最快 Fastest
- 即使本例中沒有使用這個策略蝗碎,我們?nèi)钥梢允褂眠@個策略用來緩存文章。在這個策略中旗扑,同步請求緩存和網(wǎng)絡(luò)蹦骑。哪個先返回先使用哪一個。
-
只用緩存 Cache Only *
- 因為我們改變頻率很低臀防,子目錄subreddits將會在應(yīng)用第一次加載時獲取眠菇,之后都將會從緩存中讀取。在其他情況下袱衷,我們可以升級service worker時更新子目錄subreddit的名稱捎废。
-
只用網(wǎng)絡(luò) Network Only
- 只用網(wǎng)絡(luò)即不使用任何緩存,因為你不想緩存的資源可能被其他的策略所緩存致燥,Network Only給你了一個用來排除指定的路徑登疗,防止被緩存的明確的策略。
四嫌蚤、Gulp配置
work中的gulpfile.js辐益。目前他應(yīng)該包含這些代碼
'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實際的文件脱吱。這樣我們的開發(fā)文件(例如gulp file)和應(yīng)用文件時隔離的智政。讓我們在變量中標記應(yīng)用文件的位置,稍后使用箱蝠。
gulp.task('make-service-worker', function(callback) {
var rootDir = 'app';
});
- 調(diào)用 swPrecache.write()
sw-precache
庫的方法write()
续捂,可以用來在指定位置創(chuàng)建service worker。在task中添加此方法宦搬。
gulp.task('make-service-worker', function(callback) {
var rootDir = 'app';
swPrecache.write(path.join(rootDir, 'serviceworker.js'), {
}, callback);
});
write()
方法有三個參數(shù)
filePath牙瓢,生成service worker的路徑
options,配置service worker的對象床三,包含前面提到過的兩種緩存策略precaching和runtime caching一罩。目前為空
callback,我們必須添加gulp callback到sw-precache中
剩下的代碼將會被添加到options對象中∑膊荆現(xiàn)在聂渊,可以執(zhí)行g(shù)ulp task
$ gulp make-service-worker
五、預(yù)緩存 Precaching
讓我們開始關(guān)注業(yè)務(wù)四瘫,我們需要讓service worker做一些事情汉嗽。
告訴sw-precache需要緩存的資源
首先我們需要precache Redder的app shell。
在options中使用staticFileGlobs
字段找蜜,它的值為字符串數(shù)組饼暑。 例如
{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可以把他們?nèi)烤彺嬖跒g覽器中陈惰。
staticFileGlobs
屬性告訴sw-precache到哪里去尋找文件,而不是告訴生成的service worker在哪里去獲取這些資源(這句話的意思是毕籽,尋找文件的路徑上可能會帶上不需要的路徑抬闯,例如app/,在服務(wù)啟動時我們是直接在app/路徑下啟動的关筒,所以資源上帶有app會造成瀏覽器在獲取資源時出錯)溶握,所以使用stripPrefix
來截取資源的前綴。
{staticFileGlobs: [rootDir + '/**/*.{html,css,png,jpg,gif}',
rootDir + '/js/*.js'],
stripPrefix: rootDir
}
為什么JS文件要單獨列出來
如果我們在第一行引入蒸播,precaching會把service worker和他import的文件全部緩存睡榆,這樣是不對的。我們在更新應(yīng)用時廉赔,使用了舊版的Service worker會帶來很多困擾肉微。
因為service worker存在rootDir中,我們可以跳過rootDir蜡塌,來緩存其他的js文件碉纳。
生成service worker
$ gulp make-service-worker
service worker生成在work/app/路徑下
驗證預(yù)緩存precaching
六、運行時緩存 Runtime caching
通過給write()的options對象添加參數(shù)馏艾,可以配置運行時緩存的策略劳曹。運行時緩存必須的兩個參數(shù)是urlPattern
和handler
,有些緩存策略可能會需要更多琅摩。參數(shù)配置類似下面這種形式铁孵。其中urlPattern支持正則匹配。
runtimeCaching: [
{
urlPattern: /some regex/,
handler: 'cachingStrategy'
},
{
urlPattern: /some regex/,
handler: 'cachingStrategy'
}
// Repeat as needed.
],
緩存文章標題
如果你偷看final/中的gulpfile.js房资,你可以發(fā)現(xiàn)為三類內(nèi)容使用了三類緩存策略蜕劝。
我們首先來看文章標題的緩存。
因為標題變化頻率高轰异,使用網(wǎng)絡(luò)優(yōu)先緩存策略岖沛。
給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特定的名稱來區(qū)別他們唉俗,給runtimeCaching
數(shù)組中的對象添加帶有cache
屬性的第三個參數(shù)options嗤朴,配置緩存名稱。
runtimeCaching: [
{
urlPattern: /https:\/\/www\.reddit\.com\/r\/\w{1,255}\.json/,
handler: 'networkFirst',
options: {
cache: {
name: 'titles'
}
}
}],
再次執(zhí)行命令虫溜,刷新網(wǎng)頁查看效果
$ gulp make-service-worker
后臺同步運行時緩存
Redder有一個額外的技巧雹姊,使用后臺同步來預(yù)填充運行時緩存。對子目錄衡楞,標題和文章都會執(zhí)行容为。它是怎么工作的和怎么觸發(fā)的并不是本次教程的重點,但是后面會介紹到寺酪。
給write方法添加importScript
參數(shù),可以在service worker中import js文件替劈。
importScripts: ['sync.js']
七寄雀、Debugging 緩存
開啟debugging
sw-toolbox庫有debug開關(guān),打開后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。
模擬離線和低延時環(huán)境
選擇不同的子標題聘殖,我們會看到下面的輸出
sw-toolbox輸出了對應(yīng)url的緩存策略晨雳。
在network中選擇offline,再點擊之前點擊過的子列表奸腺,可看到以下輸出
可看到已切換到緩存餐禁,頁面也可正常展示。
添加導(dǎo)航回退
在offline模式下點擊沒有點擊過得子列表突照,必然獲取不到相關(guān)數(shù)據(jù)帮非。為此,我們希望創(chuàng)建一個回退頁面讹蘑,以顯示所請求的資源不可用末盔。添加以下配置:
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會檢測多次是否需要使用緩存響應(yīng),前面的不命中才會執(zhí)行后面的檢測角骤。
對回退的檢測放在最后
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()
: 對于沒有白名單的(第一個參數(shù)隅忿,數(shù)組為空)心剥,默認返回true
event.request.mode === 'navigate'
這個不知道什么情況下會觸發(fā)
留著這個問題,以后再來
來看一下后臺同步緩存
頁面在腳本加載完畢后會調(diào)用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');
觸發(fā)后臺同步
在sync.js
中監(jiān)聽同步事件背桐,并發(fā)出對應(yīng)請求优烧。
針對于本應(yīng)用
在頁面初始化時會同步請求子目錄
在點擊子目錄展示本目錄內(nèi)的文章時,會同步請求所有符合規(guī)則的文章內(nèi)容链峭,相當于做到了預(yù)加載畦娄。
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應(yīng)用程序通常在不可靠網(wǎng)絡(luò)的環(huán)境中運行(eg:手機)和未知的生命周期(瀏覽器可能關(guān)閉或用戶點擊跳轉(zhuǎn)了)。這使得很難同步web app客戶端與服務(wù)端的數(shù)據(jù)(如照片上傳弊仪,文檔變更熙卡,或電子郵件)。如果在同步完成之前瀏覽器關(guān)閉或用戶跳轉(zhuǎn)励饵,數(shù)據(jù)同步將會中斷驳癌,直到用戶再次使用這個頁面并再次嘗試。此規(guī)范提供了一個新的serviceworker事件onsync役听,即使在數(shù)據(jù)最初請求時網(wǎng)絡(luò)情況不佳颓鲜,仍可以在后臺進行同步操作。這個API是為了減少內(nèi)容創(chuàng)建和與服務(wù)端內(nèi)容同步的時間典予。
同步請求會在觸發(fā)時立刻執(zhí)行甜滨,如果網(wǎng)絡(luò)狀況不好,會run the event at the soonest convenience瘤袖。
當已不再會執(zhí)行更多的請求時衣摩,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