前端共享让前端飞前端开发那些事

Javascript:用Service Worker做一个离线网

2018-03-04  本文已影响40人  Lxylona

参考资料
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 状态。

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)——

 
我们已经理解了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

依然可以访问页面。

上一篇 下一篇

猜你喜欢

热点阅读