简要说明
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