Javascript:用Service Worker做一个离线网页应用

参考资料
MDN --- Service Worker API
Service Workers: an Introduction
服务工作线程生命周期
Service Worker Cookbook(收集了Service Worker的一些实践例子)
理解 Service Workers

温馨提示

  1. 使用限制
    Service Worker由于权限很高,只支持https协议或者localhost。
    个人认为Github Pages是一个很理想的练习场所。
  2. 储备知识
    Service Worker大量使用Promise,不了解的请移步:Javascript:Promise对象基础

兼容性

Service Worker的兼容性

一、 生命周期

个人觉得先理解一下它的生命周期很重要!之前查资料的时候,很多文章一上来就监听install事件、waiting事件、activate事件……反正我是一脸懵逼。

Service Worker的生命周期

1. Parsed

SW是一个JS文件,如果我们要使用一个SW(Service Worker),那么我们需要在我们的js代码中注册它,类似于:
navigator.serviceWorker.register('/sw-1.js')

现在并不需要知道这个方法各个部分的详细含义,只要知道我们现在在为我们的网页注册一个SW就可以了。

可以看到我们传入的参数是一个JS文件的路径,当浏览器执行到这里的时候,就会到相应的路径下载该文件,然后对该脚本进行解析,如果下载或者解析失败,那么这个SW就会被舍弃。

如果解析成功了,那就到了parsed状态。可以进行下面的工作了。

2. Installing

在installing状态中,SW 脚本中的 install 事件被执行。在能够控制客户端之前,install 事件让我们有机会缓存我们需要的所有内容。

比如,我们可以先缓存一张图片,那么当SW控制客户端之后,客户点击该链接的图片,我们就可以用SW捕获请求,直接返回该图片的缓存。

若事件中有 event.waitUntil() 方法,则 installing 事件会一直等到该方法中的 Promise 完成之后才会成功;若 Promise 被拒,则安装失败,Service Worker 直接进入废弃(redundant)状态。

3. Installed / Waiting

如果安装成功,Service Worker 进入installed(waiting)状态。在此状态中,它是一个有效的但尚未激活的 worker。它尚未纳入 document 的控制,确切来说是在等待着从当前 worker 接手。

处于 Waiting 状态的 SW,在以下之一的情况下,会被触发 Activating 状态。

  • 当前已无激活状态的 worker
  • SW 脚本中的 self.skipWaiting() 方法被调用
  • 用户已关闭 SW作用域下的所有页面,从而释放了此前处于激活态的 worker
  • 超出指定时间,从而释放此前处于激活态的 worker

4. Activating

处于 activating 状态期间,SW 脚本中的 activate 事件被执行。我们通常在 activate 事件中,清理 cache 中的文件(清除旧Worker的缓存文件)。

SW激活失败,则直接进入废弃(redundant)状态。

5. Activated

如果激活成功,SW 进入激活状态。在此状态中,SW开始接管控制客户端,并可以处理fetch(捕捉请求)、 push(消息推送)、 sync(同步事件)等功能性事件:

// sw.js

self.addEventListener('fetch', function(event) {  
  // Do stuff with fetch events
});

self.addEventListener('message', function(event) {  
  // Do stuff with postMessages received from document
}); 
......

6. Redundant 废弃

Service Worker 可能以下之一的原因而被废弃(redundant)——

  • installing 事件失败
  • activating 事件失败
  • 新的 Service Worker 替换其成为激活态 worker

 
我们已经理解了SW的生命周期了,那么现在就开始来做一个离线应用。

我们只实现最简单的功能:用户每发送一个http请求,我们就用SW捕获这个请求,然后在缓存里找是否缓存了这个请求对应的响应内容,如果找到了,就把缓存中的内容返回给主页面,否则再发送请求给服务器。

二、 register 注册

首先要注册一个SW,在index.js文件中:

// index.js

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    // 注册一个service worker,这个例子中worker的路径是根目录中的,所以这个worker可以缓存这个项目中任意文件。如果目录是‘/js/sw.js‘,那么只能缓存目录'/js'下的文件
    // 参数registration存储了本次注册的一些相关信息
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      // registration.scope 返回的是这个service worker的作用域
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

知识点:

1. window.navigator

返回一个Navigator对象,该对象简单来说就是允许我们获取我们用户代理(浏览器)的一些信息。比如,浏览器的官方名称,浏览器的版本,网络连接状况,设备位置信息等等。

2. navigator.serviceWorker

返回一个 ServiceWorkerContainer对象,该对象允许我们对SW进行注册、删除、更新和通信。

上面的代码中首先判断navigator是否有serviceWorker属性(存在的话表示浏览器支持SW),如果存在,那么通过navigator.serviceWorker.register()(也就是ServiceWorkerContainer.register())来注册一个新的SW,.register()接受一个 路径 作为第一个参数。

ServiceWorkerContainer.register()返回一个Promise,所以可以用.then().catch()来进行后续处理。

3. SW的作用域

如果没有指定该SW的作用域,那么它的默认作用域就是其所在的目录。
比如,.register('/sw.js')中,sw.js在根目录中,所以作用域是整个项目的文件。

如果是这样:.register('/controlled/sw.js'),sw.js的作用域是/controlled。

我们可以手动为SW指定一个作用域:
.register('service-worker.js', { scope: './controlled' });

3. 为什么在load事件中进行注册

为什么需要在load事件启动呢?因为你要额外启动一个线程,启动之后你可能还会让它去加载资源,这些都是需要占用CPU和带宽的,我们应该保证页面能正常加载完,然后再启动我们的后台线程,不能与正常的页面加载产生竞争,这个在低端移动设备意义比较大。

三、install 安装

我们已经注册好了SW,如果 sw.js 下载并且解析成功,我们的SW就进入安装阶段了,这时候会触发install事件。我们一般在install事件中缓存我们想要缓存的静态资源,供SW控制主页面之后使用:

// sw.js

var CACHE_NAME = 'my-site-cache-v1'; // cache对象的名字
var urlsToCache = [ // 想要缓存的文件的数组
  '/',
  '/styles/main.css',
  '/script/main.js'
];

// 如果所有文件都成功缓存,则将安装成功
self.addEventListener('install', function(event) {
  // 执行安装步骤
  // ExtendableEvent.waitUntil()方法延长了安装过程,直到其传回的Promise被resolve之后才会安装成功
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

知识点:

1. cache

Cache是允许我们管理缓存的 Request / Response 对象对的接口,可以通过这个接口增删查改 Request / Response 对。

上面代码中cache.addAll(urlsToCache)表示把数组中的文件都缓存在内存中。
详细了解请戳 : Cache

2. caches

caches是一个CacheStorage对象,提供一个可被访问的命名Cache对象的目录,维护字符串名称到相应Cache对象的映射。

我们可以通过该对象打开某一个特定的Cache对象,或者查看该列表中是否有名字为“xxx”的Cache对象,也可以删除某一个Cache对象。

四、activate 激活

我们的SW已经安装成功了,它可以准备控制客户端并处理 push 和 sync 等功能事件了,这时,我们获得一个 activate 事件。

// sw.js

self.addEventListener("activate", function(event) {
    console.log("service worker is active");
});

如果SW安装成功并被激活,那么控制台会打印出"service worker is active"。

如果我们是在更新SW的情况下,此时应该还有一个旧的SW在工作,这时我们的新SW就不会被激活,而是进入了 "Waiting" 状态。

我们需要关闭此网站的所有标签页来关闭旧SW,使新的SW激活。或者手动激活。

那么activate事件可以用来干什么呢?假设我们现在换了一个新的SW,新SW需要缓存的静态资源和旧的不同,那么我们就需要清除旧缓存。

为什么呢?因为一个域能用的缓存空间是有限的,如果没有正确管理缓存数据,导致数据过大,浏览器会帮我们删除数据,那么可能会误删我们想要留在缓存中的数据。

这个以后会详细讲,现在只需要知道activate事件能用来清除旧缓存旧可以了。

五、 fetch事件

现在我们的SW已经激活了,那么可以开始捕获网络请求,来提高网站的性能了。

当网页发出请求的时候,会触发fetch事件。

Service Workers可以监听该事件,'拦截' 请求,并决定返回内容 ———— 是返回缓存的数据,还是发送请求,返回服务器响应的数据。

下面的代码中,SW会检测缓存中是否有用户想要的内容,如果有,就返回缓存中的内容。否则再发送网络请求。

// sw.js

self.addEventListener('fetch', event => {
    const { request } = event; // 获取request
    const findResponsePromise = caches.open(CACHE_NAME)
    // 在match的时候,需要请求的url和header都一致才是相同的资源
    // caches.match(event.request, {ignoreVary: true}) 表示只要请求url相同就认为是同一个资源。
    .then(cache => cache.match(request)) // 查看cache对象中是否有匹配的项
    .then(response => {
        if (response) { // 如果response不为空,则返回response,否则发送网络请求
            return response;
        }

        return fetch(request);
    });
    // event.respondWith 是一个 FetchEvent 对象中的特殊方法,用于将请求的响应发送回浏览器。它接收一个对响应(或网络错误)resolve 后的 Promise 对象作为参数。
    event.respondWith(findResponsePromise);
});

箭头函数真的很适合用于Promise对象,省略了一堆的functionreturn关键字,看着舒服多了……

关于缓存策略
不同的应用场景需要使用不同的缓存策略。

比如,小红希望她的网站在在线的时候总是返回缓存中的内容,然后在后台更新缓存;在离线的时候,返回缓存的内容。

比如,小明希望他的网站可以在在线的时候返回最新的响应内容,离线的时候再返回缓存中的内容。
……
如果想要研究一下各种缓存策略,可以参考下面的资料,这里就不详述了,不然文章就成裹脚布了……
The Service Worker Cookbook
离线指南
Service Worker最佳实践

不过,既然标题是“做一个离线网页应用”,那我们就做一个最简单的缓存策略:如果缓存中保存着请求的内容,则返回缓存中的内容,否则,请求新内容,并缓存新内容。

self.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request)
        .then(response => {
            // Cache hit - return response
            if (response) {
                return response;
            }
            // 克隆请求。因为请求是一个“stream”,只能用一次。但我们需要用两次,一次用来缓存,一次给浏览器抓取内容,所以需要克隆
            var fetchRequest = event.request.clone();
            // 返回请求的内容
            return fetch(fetchRequest).then(
                response => {
                    // 检查是否为有效的响应。basic表示同源响应,也就是说,这意味着,对第三方资产的请求不会添加到缓存。
                    if (!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }
                    // 同request,response是一个“stream”,只能用一次,但我们需要用两次,一次用来缓存一个返回给浏览器,所以需要克隆。
                    var responseToCache = response.clone();
                    // 缓存新请求
                    caches.open(CACHE_NAME)
                        .then(cache => cache.put(event.request, responseToCache));
                    return response;
                }
            );
        })
    );
});

 
完成啦!我们简陋的离线应用!
打开页面,看一下缓存中有什么内容:


offline1

然后点击“Vue”的链接:


offline2

可以看到缓存中多了一张后缀为.png的图片。
SW缓存了我们的新请求!

打开chrome的开发者工具,点击offline,使标签页处于离线状态:


offline3

然后,刷新页面。


offline4

依然可以访问页面。

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

推荐阅读更多精彩内容