阅读 345

离线缓存Service Worker

这是我参与8月更文挑战的第7天,活动详情查看: 8月更文挑战


前言

        随着技术的不断发展,应用的性能已经优化用户体验逐渐成为重中之重。所以网页秒开就是提升用户体验重要的一部分。

        我负责的业务线是广告投放、推广,经常会碰到高并发,刚上线时经常因此导致服务器崩溃,页面打不开,被用户投诉,只能和后端小伙伴们一点点优化。这次记录一下网页离线缓存技术。可以使用户在无网络、弱网、服务器崩溃的情况下有一个较好的用户体验,延长用户留存时间。

        Service Worker类似一个在web应用程序和浏览器之间的代理服务器,javaScript是单线程的运行方式,而Service Worker独立于主线程不会造成js堵塞,因此它也不能访问dom元素,可以通过postMessage与页面实现通信。Service Worker可以实现向后台传递消息网络代理请求转发伪造响应离线缓存消息推送等功能。处于安全性考虑,Service Worker只能被使用在https环境或者本地环境下。

Service Worker基于事件监听机制。最常用的事件包括以下几个:

  • install 安装Service Worker,通常在这个事件缓存资源,以便于用户在下次访问时能提速。
  • activate 第一次加载会在install之后被触发。
  • fetch 激活后对网页的请求拦截。
  • message 在接收到消息的时候执行。

使用Service Worker

注册服务

因为Service Worker兼容性较低,所以需要先判断浏览器是否支持。将Service Worker放到load()事件内加载是为了保证优先加载页面资源,先保证页面渲染,不占用额外的cpu。 图为 caniuser.com提供的兼容性:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <script src="js/jqurey.js"></script>
</head>

<body>
    <img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/154cd131b76e431897c2c04af97880df~tplv-k3u1fbpfcp-watermark.image" width="100px">
    <div>这是一串文字</div>
    <div class="list"></div>
    <script>
        let baseUrl = ""
        function getList() {
            $.ajax({
                type: "get",
                url: "http://localhost:3000/api/getList",
                data: {},
                dataType: "json",
                success: res => {
                    let html = ""
                    for (let i in res.data) {
                        html += "<div>" + res.data[i].name + "</div>"
                    }
                    $(".list").html(html)
                }
            })
        }
        getList()
    </script>
    <script>
        window.addEventListener("load", event => {
            if ("serviceWorker" in navigator) {
                navigator.serviceWorker
                    .register("sw.js")
                    .then(registration => {
                        console.log("注册成功");
                    })
                    .catch(error => {
                        console.log("注册失败", error);
                    });
            }
        });
    </script>
</body>

</html>
复制代码

这里模拟了大多数网页的场景,页面包含文字、图片以及ajax资源(Service Worker无法缓存post请求)。 register()可以注册一个服务,这个方法接收两个参数,第一个为Service Worker要加载的文件,第二个参数可选,用来指定要控制的文件目录,默认是跟sw.js所在的同级目录。该方法返回一个 Promise 。如果注册失败,可以通过 then和catch 来捕获成功或错误信息。

安装 installing

sw.jsService Worker的核心模块,里面定义了各个生命周期如何去缓存资源。当我们注册完Service Worker后,就会触发install事件。

var verson = "v1"
self.addEventListener('install', event => {
   event.waitUntil(caches.open(verson).then(cache => {
     return cache.addAll(['demo.html', 'js/jqurey.js','https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/154cd131b76e431897c2c04af97880df~tplv-k3u1fbpfcp-watermark.image'])
   }))
})
复制代码
  • waitUntil该事件接收一个promise,可以延长事件的作用事件。
  • caches.open(String)可以创建名为String的缓存空间,将需要缓存的文件添加进去;
  • cache.addAll(Array)选择哪些文件被缓存;

此时我们打开控制台 > Application > Cache Storage会发现多出了一个叫v1的缓存库。里面缓存的资源正好是我们设置的资源。

激活 activated

只有在第一次安装Service Worker会进入activated状态,如果有正在运行中的Service Worker则该事件不会执行。需要等待新的Service Worker被安装或者调用self.skipWaiting()才会执行。 activated阶段可以更新缓存。

self.addEventListener('activate', event => {
  console.log("activate")
  var fn = caches.keys().then(function (cacheList) {
    return Promise.all(
      cacheList.map(function (cacheName) {
        if (cacheName !== verson) {
          return caches.delete(cacheName);
        }
      })
    );
  })
  event.waitUntil(Promise.all([fn])
    .then(() => {
      return self.clients.claim()
    })
  )
});
复制代码
  • claim()让新的Service Worker接管页面

fetch

写到这一步我们断开网络刷新页面,发现还是提示的无网络页面,这是因为少了最重要的一步:监听fetch事件,该事件会拦截请求,判断缓存是否存在,优先取缓存中的数据,缓存中不存在的话就继续请求。

self.addEventListener('fetch', function (event) {
  console.log("fetch",event.request.url)
  event.respondWith(
      caches.match(event.request).then(function (response) {
          if (response) {
              return response
          }
          return fetch(event.request)
      })
  )
})
复制代码

至此,我们就完成了静态资源的缓存。这时我们来测试一下,修改页面的文字内容以及接口返回的数据刷新页面对比一下,可以看到只有接口返回的数据发生了变化,页面的文字修改并没有更新到页面。

修改前:

修改后:

如果我们想将接口的数据也缓存起来,可以将请求拷贝出来放入缓存,如果请求失败或者post方式则直接返回,代码如下:

self.addEventListener('fetch', function (event) {
  console.log("fetch", event.request.url)
  event.respondWith(
    caches.match(event.request).then(function (response) {
      if (response) {
        return response;
      }
      var request = event.request.clone(); 
      return fetch(request).then(function (httpRes) {
        if (!httpRes || (httpRes.status !== 200 && httpRes.status !== 304 && httpRes.type !== 'opaque') || request.method === 'POST') {
          return httpRes;
        }
        var responseClone = httpRes.clone();
        caches.open(verson).then(function (cache) {
          cache.put(event.request, responseClone);
        });
        return httpRes;
      });
    })
  );
});
复制代码

更新机制

如果代码有了新版本,想让页面都得到更新怎么办呢?Service Worker中的sw.js每次都会被执行,利用这个特性我们可以更新sw.js文件的版本号,然后在install事件中调用 self.skipWaiting() 方法使Service Worker直接进入activate事件。然后在activate事件内通过版本号判断删除旧版本缓存,然后通过elf.clients.claim()更新缓存。

var verson = "v1"
self.addEventListener('install', event => {
  console.log("install")
  this.skipWaiting()
})
self.addEventListener('activate', event => {
  console.log("activate")
  caches.open(verson).then(cache => {
      return cache.addAll(['demo.html', 'js/jqurey.js', 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/154cd131b76e431897c2c04af97880df~tplv-k3u1fbpfcp-watermark.image'])
    })
  var delFn = caches.keys().then(function (cacheList) {
    return Promise.all(
      cacheList.map(function (cacheName) {
        if (cacheName !== verson) {
          return caches.delete(cacheName);
        }
      })
    );
  })
  event.waitUntil(Promise.all([delFn])
    .then(() => {
      return self.clients.claim()
    })
  )
});
self.addEventListener('fetch', function (event) {
  console.log("fetch",event.request.url)
  event.respondWith(
      caches.match(event.request).then(function (response) {
          if (response) {
              return response
          }
          return fetch(event.request)
      })
  )
})
复制代码
文章分类
前端