抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

一个简单的ServiceWorker应用

目标

实现在离线状态下能正常加载页面以及本域的图片等资源

下载素材包

点击下载素材包

注册sw.js文件

新建sw.js文件
创建后目录结构如下:
├─index.html
├─sw.js
├─img
└1.jpg

<!-- index.html -->
...
<script>
    window.addEventListener('load', () => {
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(registration => {
          console.log('ServiceWorker成功注册在域: ', registration.scope);
        }).catch(err => {
          console.log('ServiceWorker注册失败: ', err);
        }
        );
      }
    }
    );
  </script>
...

博客ServiceWorker注册成功

先缓存文件

// sw.js
const VERSION = 0;
// 缓存版本
const CACHE_NAME = 'sw-cache-v' + VERSION;
// 缓存空间名字
const CACHE_URLS = [
    './index.html',
    './img/1.jpg',
];
// 需要缓存的url

addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(function (cache) {
            return cache.addAll(CACHE_URLS);
        })
    )
    skipWaiting();
    // 加上这句话,可以让sw立即生效,从而立刻删除旧的sw文件
})

博客ServiceWorker缓存成功
这样就将文件缓存在浏览器了,接下来就是监听网络请求了,如果是缓存的请求,就直接返回缓存的文件,如果是网络请求,就返回网络请求的文件。

监听网络请求,实现离线访问功能

// sw.js
...
addEventListener('fetch', event => {
    // fech事件用于接收fetch请求来选择走缓存还是走网络

    if(new URL(event.request.url).origin != location.origin) {
        // 过滤非本站的资源
        return;
    }

    event.respondWith(
            caches.match(event.request)
    )
})

博客ServiceWorker脱机加载成功

更新缓存

在说明更新缓存的时候,我需要先说明它的书写规范

  1. 不要给 service-worker.js 设置不同的名字
  2. 不要给 service-worker.js 设置缓存。Cache-control: no-store 是比较安全的。

    浏览器的更新机制

    SW的waiting状态
    注册navigator.serviceWorker.register()时有两种情况:
  • 如果没有活跃(activate)的SW,那就直接安装且激活。
  • 如果已经有SW安装着,那么就和新的SW比较。如果没有差别则不做任何操作,如果有差别,那么就安装新的SW(执行install阶段),之后进入waiting状态。等待老的SW所有页面被关闭后,才会执行activate阶段,从而接管页面。

    由于浏览器是等待新的页面渲染之后才销毁旧的页面,因此简单的切换页面或者刷新是不能使得SW进行更新,只能关闭页面刷新。默认的更新是一种比较温和的做法,但让用户关闭页面并不是程序员能控制的。
    假设我们提供了一次重大升级,希望新的 SW 尽快接管页面,应该怎么做呢?

    方法一:skipWaiting()

    在遭遇突发情况时,很容易想到通过“插队”的方式来解决问题,现实生活中的救护车消防车等特种车辆就采用了这种方案。SW 也给程序员提供了实现这种方案的可能性,那就是在 SW 内部的 skipWaiting() 方法。

    addEventListener('install', event => {
    self.skipWaiting()
    // 预缓存其他静态内容
    })
    

    虽然这个方法显得很简单,但是它的实现还是有很多缺点的

比如有文件sw.v1.js和sw.v2.js。浏览器默认先异步加载sw.v1.js,缓存了前半部分文件。但是假如sw.v2.js应用了skipWaiting方法,那么sw.v1.js就会被强制退休,sw.v2.js立马接管页面。从而导致一个页面前半部分在sw.v1.js后半部分在sw.v2.js,在脱机状态浏览器就会找不到某些文件而报错

除非你能保证同一个页面在两个版本的 SW 相继处理的情况下依然能够正常工作,才能使用这个方案。

SW的其它更新机制
  • 浏览器每24小时自动更新一次Service Worker
    写个js自己试试就知道了
  • 注册新的 Service Worker,带上版本号,如:/sw.js?v=201807021920
    const VERSION = 1;
    // 修改上面的缓存版本
    const CACHE_NAME = 'sw-cache-v' + VERSION;
    // 缓存空间名字
    ...
    addEventListener('activate', event => {
      // Service Worker 激活时,删除旧的缓存
      event.waitUntil(
          caches.keys().then(function (keys) {
              keys.forEach(function (key) {
                  if (key !== CACHE_NAME) {
                  // 遍历所有缓存,如果不是当前版本,则删除
                  return caches.delete(key);
                      // 删除旧的缓存
                  }
              })
          })
      )
    })
    ...
    
  • 手动更新registration.update()
  • 逐字节对比新的sw文件和旧的sw文件,有区别才更新

Service Worker更新后通知用户

Service Worker更新过程
博客ServiceWorker更新过程

完整sw.js代码

const VERSION = 0;
// 缓存版本
const CACHE_NAME = 'sw-cache-v' + VERSION;
// 缓存空间名字
const CACHE_URLS = [
    './',
    './index.html',
    './img/1.jpg',
];
// 需要缓存的url

addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(function (cache) {
            return cache.addAll(CACHE_URLS);
        })
    )
    skipWaiting();
    // 加上这句话,可以让sw立即生效,从而立刻删除旧的sw文件
})

addEventListener('activate', event => {
    // Service Worker 激活时,删除旧的缓存
    event.waitUntil(
        caches.keys().then(function (keys) {
            keys.forEach(function (key) {
                if (key !== CACHE_NAME) {
                // 遍历所有缓存,如果不是当前版本,则删除
                return caches.delete(key);
                    // 删除旧的缓存
                }
            })
        })
    )
})

addEventListener('fetch', event => {
    // fech事件用于接收fetch请求来选择走缓存还是走网络

    if(new URL(event.request.url).origin != location.origin) {
        // 过滤非本站的资源
        return;
    }

    event.respondWith(
            caches.match(event.request)
    )
})

评论