likes
comments
collection
share

无网络?别担心!Service Worker 让你的网站离线也能访问

作者站长头像
站长
· 阅读数 20

原创首发于微信公众号:mp.weixin.qq.com/s/2MpWdflG8…

万字长文警告! 收藏 + 关注,学习不迷路 ~


你好,我是小甲,一枚前端开发攻城狮。

有这样一个场景不知道你有没有留意过:

你正在浏览网页,突然发现断网了!这时候你可能在页面上胡乱地点几下,但是你发现浏览的网站仍然能正常显示内容——就好像,你正在用“云”流量来上网……

你不觉得奇怪嘛 ~

明明没有网络,为啥浏览的网站仍然能正常工作呢?

这就是今天要分享的主题:离线缓存

说到离线缓存,你可能会想到浏览器的 Cache API。没错,Cache API 确实可以为我们提供离线缓存的能力。不过,Cache API 的功能较为有限,它只能缓存 HTTP 响应。

那有没有其他方案呢?

答案是肯定的——Service Worker 最适合干这件事。

因此,我们今天要分享的完整内容主题便是:Service Worker 实现离线缓存

如果你想让你的用户在离线状态下能继续使用你的网站,那么,你一定不要错过今天的内容。我可以保证,通过这篇文章,你将对 Service Worker 有更深入的理解,并能在你的项目中实现离线缓存的功能。

今天的内容主要有:

  • 注册 Service Worker;
  • 如何缓存资源;
  • 如何拦截请求;
  • 如何更新缓存;
  • 离线分析用户行为数据;
  • 最佳实践和注意事项。

如果你觉得不过瘾,还可以去看看上一篇关于 Service Worker 实现“推送通知”的文章,相信也会对你有所启发:当你的网站有新内容时,如何立即通知你的用户呢?

现在,就让我们正式开始今天的内容吧 ~

1. 基础知识

关于什么是 Service Worker,在《当你的网站有新内容,如何立即通知你的用户呢?》一文中有提及,这里我再简单介绍一下。

Service Worker 运行在浏览器后台,其独立于网页,不会因为网页的关闭而停止运行。也就是说,Service Worker 能在没有用户交互的情况下执行任务,比如接收推送通知,或者是我们今天分享的——实现离线缓存。

那么,什么是离线缓存呢?离线缓存,顾名思义,就是在没有网络连接的情况下,通过缓存的方式让用户仍然能访问到网站的内容。这是通过 Service Worker 的拦截请求和返回缓存内容的能力实现的。

除此之外,你还需要了解 Service Worker 的生命周期。

Service Worker 的生命周期主要包括三个阶段:安装(install)、激活(activate)和等待(idle)。

在安装阶段,我们可以预缓存一些资源;在激活阶段,我们可以清理旧的缓存;在等待阶段,Service Worker 就可以开始接收和处理事件了。

那它的工作原理是什么呢?

首先,当用户第一次访问我们的网站时,Service Worker 会被安装并激活,然后我们可以在 Service Worker 中缓存需要的资源。这样,当用户再次访问我们的网站时,Service Worker 就可以拦截请求,直接返回缓存的资源,而不需要从网络获取。

这就是离线缓存的基本工作原理。

在你了解了上面这些基础知识后,我们就可以着手实现离线缓存的功能了。

准备好了嘛?

Let’s go ~

2. 注册 Service Worker

现在,我们就要进入实战环节了。第一步要做的,就是注册 Service Worker。

注册 Service Worker 的目的,是让浏览器知道我们要在网站上使用 Service Worker,并告诉浏览器 Service Worker 的位置。

不过呢,在注册 Service Worker 之前你需要先检查一下浏览器是否支持 Service Worker。

if ('serviceWorker' in navigator) {
  // 浏览器支持 Service Worker
  // 稍后,将注册 Service Worker 的代码放在这里……
else {
  // 浏览器不支持 Service Worker
}

如果浏览器支持 Service Worker,那我们就可以开始注册了。

navigator.serviceWorker.register('/sw.js')
  .then(function(registration) {
    // 注册成功
    console.log('Service Worker 注册成功,作用域是: ', registration.scope);
  })
  .catch(function(err) {
    // 注册失败
    console.log('Service Worker 注册失败: ', err);
  });

在上面这段代码中,注册 Service Worker 有两点需要注意:

  1. 注册 Service Worker 是一个异步操作。所以,navigator.serviceWorker.register 会返回一个 Promise,可以使用 .then 来处理注册成功的情况,使用 .catch 来处理注册失败的情况。
  2. 注册时需要告诉浏览器 Service Worker 的文件位置。通常情况下,Service Worker 文件的位置放在网站根目录即可。代码中的 /sw.js 就是 Service Worker 的文件位置。

是不是很简单?Service Worker 的注册过程就是这几行代码。归纳起来有两点:先检查浏览器是否支持,如果支持就可以注册 Service Worker 了。

现在,我们已经成功注册了 Service Worker,那么,该如何缓存资源呢?

3. 缓存资源

前面我们提到 Service Worker 的生命周期主要包括三个阶段,那么,缓存资源这一步应该放在哪个阶段呢?

没错,在安装阶段。

在 Service Worker 安装阶段,把需要的资源都缓存起来,这样在没有网络的的时候,我们就可以从缓存中取出这些资源。

具体该怎么实现呢?

其实很简单,只需要在 Service Worker 的 install 事件中,打开一个缓存,然后把我们需要的资源添加到缓存中即可。

self.addEventListener('install'function(event) {
  event.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/script.js',
        '/logo.png',
      ]);
    })
  );
});

在这段代码中,首先监听了 Service Worker 的 install 事件,然后在这个事件中,我们打开了一个名为 'my-cache' 的缓存,然后把我们需要的资源添加到这个缓存中。

要缓存的这些资源包括首页 HTML 文件、CSS 文件、JS 文件和一个图片文件。

你可能会注意到,这段代码中用到了一个 event.waitUntil 方法。这个方法的作用是什么呢?

event.waitUntil 的作用是让 Service Worker 等待我们的缓存操作完成。这个方法是一个异步操作,其接受一个 Promise 作为参数,Service Worker 会等待这个 Promise 完成,然后才会进入到下一个生命周期阶段。如果这个 Promise 被 reject,那么 Service Worker 的安装也就失败了,这个 Service Worker 也不会被激活。

你可能会想,会不会出现资源缓存失败的情况呢?

确实,这种情况是存在的。比如说,如果网络连接不稳定,或者某个资源的 URL 错误,那么这个资源就可能无法被缓存。在这种情况下,你可以在 cache.addAll 方法后面添加一个 .cache 方法,用来处理可能出现的错误。这样即便某个资源无法被缓存,Service Worker 也可以正常安装。

self.addEventListener('install'function(event) {
  event.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/script.js',
        '/logo.png',
      ]).catch(function(error) {
        console.log('资源缓存失败:', error);
      });
    })
  );
});

到这里,我们已经把需要的资源都缓存起来了。当用户再次访问我们的网站时,我们就可以拦截请求并返回缓存的资源。

那么,拦截请求的功能,该如何实现呢?

4. 拦截请求

当我们把网站资源缓存后,用户再次访问我们的网站时,如果缓存中有对应的资源,Service Worker 就拦截所有的网络请求,将我们缓存中的资源返回给用户。当缓存中没有对应的资源时,再去请求资源展示给用户。

具体做法如下。

当网站发起网络请求时,会触发一个叫做 fetch 的事件,我们可以在 Service Worker 中通过监听这个事件来实现我们的需求。在这个事件中,我们就可以做到检查网站的缓存,看看是否有对应的资源。

self.addEventListener('fetch'function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // 缓存中有对应的资源,直接返回
        if (response) {
          return response;
        }

        // 缓存中没有对应的资源,从网络获取
        return fetch(event.request);
      })
  );
});

代码中的 event.respondWith 是 Service Worker 中一个比较重要的方法,通过函数名你应该也能猜到它是干什么用的。是的,它用于告诉浏览器我们该如何响应监听的事件。

当你在 Service Worker 中监听一个事件(比如这里的 fetch 事件)时,你就可以用 event.respondWith 方法来控制这个事件的响应。该方法接受一个 Promise 作为参数,这个 Promise 会解析成一个 Response 对象,这个对象就是我们的响应内容。

之后,caches.match(event.request) 会在我们的缓存中检查是否有对应的资源。如果有,那么 response 就是我们的缓存资源,我们可以直接返回这个资源。而如果没有,那么 response 就是 undefined ,这时,我们就需要从网络获取资源。

这就是 Service Worker 拦截请求并返回缓存的基本过程。通过这种方式,你可以在没有网络连接的情况下,让你的用户仍然能访问到一部分内容。

不过,你可能会注意到,如果我们的缓存中没有对应的资源,我们就会从网络上获取相应的资源。那么,如果这时候用户的网络连接处于断开的状态,那么用户是无法通过网络获取到资源的,用户仍然会看到一个错误提示。

那么,在这种情况下,有没有办法也给用户展示一些有用的信息呢?

答案是肯定的。

我们可以在 Service Worker 中预缓存一个离线页面,然后当无法从网络获取资源时,就返回这个离线的页面。

self.addEventListener('fetch'function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // 缓存中有对应的资源,直接返回
        if (response) {
          return response;
        }

        // 缓存中没有对应的资源,从网络获取
        return fetch(event.request).catch(function() {
          // 网络获取失败,返回离线页面
          return caches.match('/offline.html');
        });
      })
  );
});

在这段代码中,当用户网络请求失败的情况下,给用户返回了一个预缓存的离线页面 offline.html。通过这种方式,在用户没有网络连接的情况下,用户仍然能看到一个”正常“显示的页面,你可以在这个离线页面中写一些有用的信息展示给用户。这样做的好处是,可以极大提升用户体验,尤其是在网络环境不稳定的情况下。

这时候你可能会问,我们缓存的资源会一直保留在用户的设备上吗?如果我们的网站更新了,用户怎么能看到最新的内容呢?

别急,这就是我们接下来的内容:如何更新缓存。

5. 更新缓存

首先,你需要明白一点:Service Worker 的更新是由浏览器自动完成的。

当我们对 Service Worker 文件做了任何改动,浏览器就会认为 Service Worker 有更新,然后就会开始更新整个缓存流程。这个流程包括重新安装 Service Worker,然后在新的 Service Worker 激活后,旧的 Service Worker 就会被替换掉。

那么,我们如何在新的 Service Worker 安装后更新我们的缓存呢?

其实不难,我们只需要在 Service Worker 的 activate 事件中,删除旧的缓存,然后在新的 Service Worker 安装时,创建新的缓存即可。

self.addEventListener('activate'function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheName !== 'my-new-cache') {
            // 如果这个缓存不是我们新创建的缓存,那么就把它删除
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

上面的代码中,我们通过 caches.keys() 获取了所有的缓存名称,然后遍历这些缓存名称,如果缓存名称不是我们新创建的缓存名称,那么我们就删除这个缓存。这样,我们就可以保证用户的设备上只保留最新的缓存。

不过,即使 Service Worker 已经更新了,用户看到的可能仍然是旧的页面内容。

这是因为,虽然新的 Service Worker 已经激活,但是旧的 Service Worker 控制的页面(也就是用户当前打开的页面)在用户关闭浏览器或者刷新页面之前,仍然会被旧的 Service Worker 控制。所以,如果你想让用户立即看到最新的内容,这里就需要处理一下。

常见的处理方法有两种:立即获取控制权,静默更新站点内容;页面上提示用户手动刷新以更新网站缓存。

这两种方式各有优缺点,你需要根据你的用户群体和你网站的特性来决定用哪种方案。下面我会简单介绍一下这两种方案,你可以根据需要自行选择。

5.1 立即获取控制权

这种方式可以通过 activate 事件中调用 self.clients.claim() 方法来实现。

self.addEventListener('activate'function(event) {
  event.waitUntil(self.clients.claim());
});

activate 事件会在 Service Worker 安装完成且没有其他版本的 Service Worker 控制着浏览器时触发。self.clients.claim() 的作用是强制所有在这个 Service Worker 控制下的浏览器立即受到这个 Service Worker 的控制。

这样,新的 Service Worker 就可以立即开始控制页面,用户也就可以立即看到最新的页面内容了。

这是一种”静默“的方式,也就是说,用户可能感知不到网站已经更新。其优点是,用户无需进行任何操作,但缺点是用户可能会错过重要的更新信息。如果你的项目是企业内部的管理系统的话,可以考虑这种方式,因为需求方很有可能就是你的用户,他们应该知道这次上线的版本都迭代了哪些功能。因此,在这种情况下,该方案是可行的。

但是你的用户如果对更新的内容比较感兴趣呢?比如你的企业维护着一个对外的平台,每次上线,用户对更新的内容都比较感兴趣。那么这种情况下,你可以考虑下面的第二种方案。

5.2 提示用户手动刷新页面

这个方案在许多网站上都非常常见,如果你对 Vue.js 比较熟悉的话,你应该留意过,他们官方文档的站点在每次更新后,网页右下角都会有一个全局提示框,提示你文档有更新,请刷新页面。

这种做法可以让用户有更多的控制权,他们可以自行决定什么时候加载新的内容。当然,有的网站会在提示信息中添加一个导航到”更新日志“的超链接,感兴趣的用户可以通过这个链接导航到更新日志页面,看看这次更新都有哪些新功能或者修复了哪些问题。

那么,这个功能该怎么实现呢?

要实现这个功能,你需要在 Service Worker 更新后,向页面发送一个消息,然后在页面上监听这个消息,并在收到消息后显示一个提示框。

具体做法就是,在 Service Worker 的 install 事件中,你可以通过 self.skipWaiting() 方法来让新的 Service Worker 跳过等待状态,直接进入激活状态。然后,在 activate 事件中,你可以向所有用户发送一个消息,告诉他们 Service Worker 已经更新。

self.addEventListener('install'function(event) {
  self.skipWaiting();
});

self.addEventListener('activate'function(event) {
  event.waitUntil(
    self.clients.matchAll().then(function(clients) {
      clients.forEach(function(client) {
        client.postMessage({ type'SW_UPDATED' });
      });
    })
  );
});

之后,你可以在页面上监听 message 事件,当收到 SW_UPDATED 类型的消息时,显示一个提示框,让用户来选择是否刷新页面。

这样,当 Service Worker 更新后,用户就会收到一个提示,他们可以选择是否刷新页面来加载新的内容。

这个方案的优点是用户可以自主选择何时加载新的内容,但缺点是需要用户手动进行操作。

不论选择哪种方案,都是为了能给用户一个好的使用体验,强烈建议你私下里尝试一下。

到这里,整个离线缓存的功能就已经实现了。不过呢,为了能更好地分析我们的用户画像,有时候我们需要在离线状态下也能收集一些用户行为的数据。

这就涉及到一个问题:如何在网络连接恢复后发送这些数据呢?

6. 离线分析

用户在离线状态下浏览我们的网站时,我们该如何知道他们的行为呢?比如,他们点击了哪些链接,浏览了哪些页面,或者是在表单中填写了什么信息。这些都是我们非常关心的问题,因为这些信息可以帮助我们更好地完善用户画像,从而能更精准地对他们提供服务。

那么,我们该如何收集这些信息呢?

要收集用户的这些行为信息,可以在 Service Worker 中监听用户的行为,比如点击事件、表单提交事件等。然后,我们可以把这些事件的信息保存到 IndexedDB 中。

IndexedDB 是一个运行在浏览器中的非关系型数据库,它可以在用户的设备上存储大量的结构化数据。而且,IndexedDB 是异步的,因此它不会阻塞浏览器的主线程。

之后,当网络连接恢复后,我们可以在 Service Worker 中监听 sync 事件。这个事件会在网络连接恢复后触发,我们可以在这个事件中把 IndexedDB 中的数据发送到服务器。

这样,我们就可以在网络连接恢复后,把用户在离线状态下的行为数据发送到服务器,然后在服务器端进行分析。

这就是离线分析的基本思路。

不过,IndexedDB 涉及到很多内容,这里因篇幅有限就不多赘述了,将来我会专门和大家分享这方面的应用实践。

下面是一段示例代码,这段代码展示了离线分析这一过程:

// 监听用户的点击事件
self.addEventListener('click'function(event) {
  // 把点击事件的信息保存到 IndexedDB 中
  // ...
});

// 监听网络连接恢复后的 sync 事件
self.addEventListener('sync'function(event) {
  // 从 IndexedDB 中获取数据
  // ...
  // 把数据发送到服务器
  // ...
});

通过这种方式,我们就能做到在用户离线状态下收集用户的行为数据,当网络连接恢复后再将这些数据发送到服务器。这对我们理解用户行为、完善用户画像以及优化产品服务等都非常有帮助。

7. 最佳实践和注意事项

在使用 Service Worker 实现离线缓存的功能时,一定要小心谨慎。若使用不当,很有可能会导致用户看到的内容一直是旧的页面,这就要求你要了解 Service Worker 的生命周期和更新问题。

再者,合理使用缓存,避免过渡消耗用户的设备存储空间。一定要根据具体的业务需求来选择合适的缓存策略,比如,我们可以只缓存关键的资源,或者我们可以提供一个设置项,让用户选择是否启用离线缓存。

最后就是 Service Worker 的兼容性问题了。虽然现代浏览器多数都支持 Service Worker,但是你仍然要考虑那些不支持的浏览器。所以,你也需要做好兼容性处理,确保在不支持 Service Worker 的浏览器上,你的网站仍然能正常运行。

8. 小结

今天的内容主要是借助 Service Worker 实现离线缓存,包括注册 Service Worker,缓存资源,拦截请求,更新缓存,以及离线分析等。相信,你现在对 Service Worker 肯定有了更深的理解,同时,我也希望你能在自己的项目中尝试一下。

这还只是 Service Worker 的冰山一角,它还有很多其他用途,比如后台同步等。借今天分享的机会,希望你能再去深入了解一下 Service Worker。

最后,如果你有任何问题或者想法,欢迎在下方评论区留言,大家一起交流、进步。

感谢你的阅读,我们下期再见 :-)

无网络?别担心!Service Worker 让你的网站离线也能访问

文章出处戳 这里 ~

转载自:https://juejin.cn/post/7244420528420143141
评论
请登录