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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容