HTML5 的离线储存及其工作原理

在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。使用离线缓存技术一般是为了让用户在:
- 离线状态也能正常访问
- 提高访问速度
- 减轻服务器响应压力

实现离线缓存,目前主要使用 Service Workers方案,而文后提到的manifest属性方案已弃用,建议不要使用,这里只是作为了解。

历史方案:利用manifest属性实现App Cache(已弃用)

原理: HTMLS 的离线存储是基于一个新建的. aPpcache 文件的缓存机制(不是存储技术) , 通过这个文件上的解析清单离线存储资源,这些资源就会像 cookle 一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示。

如何使用:

( 1 )创建一个和 html 同名的 manifest 文件,然后在页面头部像下面一样加入一个 manifest 的属性。

    <html lang="en" manifest="index.manifest">

( 2 )在如下 cache . manifest 文件的编写离线存储的资源。

# 缓存清单:后续列出的文件会在第一次下载完毕后缓存起来
CACHE MANIFEST
index.html
cache.html
style.css
image1.png
 
# 缓存白名单:列出不需要缓存的文件
NETWORK:
network.html
 
# 定了一个后备页面,当资源无法访问时,浏览器会使用该页面。
# 该段落的每条记录都列出两个 URI—第一个表示资源,第二个表示后备页面。
FALLBACK:
/ fallback.html
/userpage user_404.html

CACHE MANIFEST,NETWORK 和 FALLBACK 段落可以以任意顺序出现在缓存清单文件中,并且每个段落可以在同一清单文件中出现多次。

CACHE: 表示需要离线存储的资源列表,由于包含 manifest 文件的页面将被自动离线

存储,所以不需要把页面自身也列出来。
NETWORK: 表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离
线存储,所以在离线情况下无法使用这些资源。不过,如果在 CACHE 和 NETWORK 中有一
个相同的资源,那么这个资源还是会被离线存储,也就是说 CACHE 的优先级更高。
FALLBACK: 表示如果访问第一个资源失败,那么就使用第二个资源来替换他,比如上
面 这 个 文 件 表 示 的 就 是 如 果 访 问 根 目 录 下 任 何 一 个 资 源 失 败 了 , 那 么 就 去 访 问 fallback.html 。

(3)缓存清单写好了,我们还需要按时更新,否则一直使用缓存会导致界面内容老旧,更新缓存使用JavaScript脚本来更新。

如何更新缓存:
(1)更新 manifest 文件
(2)通过 javascript 操作

window.applicationCache.update()手动更新


function onUpdateReady() {
  alert('found new version!');
}
window.applicationCache.addEventListener('updateready', onUpdateReady);
// window.applicationCache.status记录缓存状态
if(window.applicationCache.status === window.applicationCache.UPDATEREADY) {
  onUpdateReady();
} else {
   window.applicationCache.update()
}
(3)清除浏览器缓存

注意事项:
(1)浏览器对缓存数据的容量限制可能不太一样(某些浏览器设置的限制是每个站点
5MB)。
(2)如果 manifest 文件,或者内部列举的某一个文件不能正常下载,整个更新过程都将
失败,浏览器继续全部使用老的缓存。
(3)引用 manifest 的 html 必须与 manifest 文件同源,在同一个域下。
(4)FALLBACK 中的资源必须和 manifest 文件同源。
(5)当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。
(6)站点中的其他页面即使没有设置 manifest 属性,请求的资源如果在缓存中也从缓存
中访问。
( 7)当 manifest 文件发生改变时,资源请求本身也会触发更新

Service Workers
AppCache — 看起来是个不错的方法,因为它可以很容易地指定需要离线缓存的资源。但是,它假定你使用时会遵循诸多规则,如果你不严格遵循这些规则,它会把你的APP搞得一团糟。
Service worker 最终要去解决这些问题。虽然 Service Worker 的语法比 AppCache 更加复杂,但是你可以使用 JavaScript 更加精细地控制 AppCache 的静默行为。

在谈Service Worker ,先简单看看什么是Web Worker。
Web Worker概念:
1.Web Worker 是浏览器内置的线程所以可以被用来执行非阻塞事件循环的 JavaScript 代码。

js是单线程,一次只能完成一件事,如果出现一个复杂的任务,线程就会被阻塞,严重影响用户体验, Web Worker 的作用就是允许主线程创建 worker 线程,与主线程同时进行。worker 线程只需负责复杂的计算,然后把结果返回给主线程就可以了。简单的理解就是,worker 线程执行复杂计算并且页面(主线程)ui很流畅,不会被阻塞。Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

2. 类型
    - Dedicated Workers 【专用 Worker】 是由主进程实例化并且只能与主线程进行通信。
    - Shared Workers 【共享 Worker】可以被运行在同源的所有进程访问。
    - Service workers【服务Worker】它可以控制它关联的网页,解释且修改导航,资源的请求,以及缓存资源以让你非常灵活地控制程序在某些情况下的行为。
  1. 限制
    • 同源限制,分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源,一般都放在项目下。
    • DOM限制,worker 线程无法读取主线程所在网页的 DOM 对象,无法使用 document、window、parent 这些对象,可以使用 navigator 和 location 对象。
    • 文件限制,worker 线程无法读取本地文件,不能打开文件系统,所加载的脚本,必须来自网络,不能是 file:// 文件。
    • 通信限制,worker 线程和主线程不再同一个上下文环境中,不能直接通信,必须通过消息完成。
    • 脚本限制,worker 线程不能执行 alert 方法和 confirm 方法,但是可以发出 ajax 请求。
      4.worker 线程怎样监听主线程的消息的?如何发送消息的?worker 线程又是如何关闭的?
      Worker 线程内部需要有一个监听函数,监听 message 事件。
    ```
    

// 监听
self.addEventListener('message', function (e) {
// 发送消息
self.postMessage('You said: ' + e.data);
}, false);
```
5.关闭 worker 线程

  - 主线程关闭 worker 线程
worker.terminate()
  - worker 线程关闭
self.close()
- worker 线程如何加载其他脚本?
importScript('scripts.js')
importScript('scripts1.js', 'scripts2.js')

什么是Service Worker ?
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

image.png

Service Worker 优点/缺点:
优点:
- 拦截网络请求
- 缓存可用时返回缓存内容
- 对缓存内容进行管理
- 向客户端推送信息
- 后台数据同步
- 资源预取

缺点:
Web Worker的限制

Service Worker 生命周期:
- 下载
- 安装
- 激活

Service Worker 生命周期

image.png

当用户首次导航至 URL 时,服务器会返回响应的网页。

  • 第1步:当你调用 register() 函数时, Service Worker 开始下载。
  • 第2步:在注册过程中,浏览器会下载、解析并执行 Service Worker ()。如果在此步骤中出现任何错误,register() 返回的 promise 都会执行 reject 操作,并且 Service Worker 会被废弃。
  • 第3步:一旦 Service Worker 成功执行了,install 事件就会激活
  • 第4步:安装完成,Service Worker 便会激活,并控制在其范围内的一切。如果生命周期中的所有事件都成功了,Service Worker 便已准备就绪,随时可以使用了!
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Caching World!</title>
  </head>
  <body>
    <!-- Image -->
    <img src="/images/hello.png" />                 
    <!-- JavaScript -->
    <script async src="/js/script.js"></script>     
    <script>
      // 注册 service worker
      if ('serviceWorker' in navigator) {           
        navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
          // 注册成功
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {                   
          // 注册失败 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

注:Service Worker 的注册路径决定了其 scope 默认作用页面的范围。
如果 service-worker.js 是在 /sw/ 页面路径下,这使得该 Service Worker 默认只会收到 页面/sw/ 路径下的 fetch 事件。
如果存放在网站的根路径下,则将会收到该网站的所有 fetch 事件。
如果希望改变它的作用域,可在第二个参数设置 scope 范围。示例中将其改为了根目录,即对整个站点生效。

service-worker.js

var cacheName = 'helloWorld';     // 缓存的名称  
// install 事件,它发生在浏览器安装并注册 Service Worker 时        
self.addEventListener('install', event => { 
/* event.waitUtil 用于在安装成功之前执行一些预装逻辑
但是建议只做一些轻量级和非常重要资源的缓存,减少安装失败的概率
安装成功后 ServiceWorker 状态会从 installing 变为 installed */
event.waitUntil(
  caches.open(cacheName)                  
  .then(cache => cache.addAll([    // 如果所有的文件都成功缓存了,便会安装完成。如果任何文件下载失败了,那么安装过程也会随之失败。        
    '/js/script.js',
    '/images/hello.png'
  ]))
);
});

/**
为 fetch 事件添加一个事件监听器。接下来,使用 caches.match() 函数来检查传入的请求 URL 是否匹配当前缓存中存在的任何内容。如果存在的话,返回缓存的资源。
如果资源并不存在于缓存当中,通过网络来获取资源,并将获取到的资源添加到缓存中。
*/
self.addEventListener('fetch', function (event) {
event.respondWith(
  caches.match(event.request)                  
  .then(function (response) {
    if (response) {                            
      return response;                         
    }
    var requestToCache = event.request.clone();  //          
    return fetch(requestToCache).then(                   
      function (response) {
        if (!response || response.status !== 200) {      
          return response;
        }
        var responseToCache = response.clone();          
        caches.open(cacheName)                           
          .then(function (cache) {
            cache.put(requestToCache, responseToCache);  
          });
        return response;             
  })
);
});

注:为什么用request.clone()和response.clone()
需要这么做是因为request和response是一个流,它只能消耗一次。因为我们已经通过缓存消耗了一次,然后发起 HTTP 请求还要再消耗一次,所以我们需要在此时克隆请求
Clone the request—a request is a stream and can only be consumed once.

激活: 接下来就是进入激活状态:Activate。
在这个状态可以更新 Service Worker。

  • 用户导航至站点时,浏览器会尝试在后台重新下载定义 Service Worker 的脚本文件。 如果 Service Worker 文件与其当前所用文件存在字节差异,则将其视为新 Service Worker。
  • 新 Service Worker 将会启动,且将会触发 install 事件。
  • 旧 Service Worker 仍控制着当前页面,因此新 Service Worker 将进入 waiting 状态。
  • 当网站上当前打开的页面关闭时,旧 Service Worker 将会被终止,新 Service Worker 将会取得控制权。
  • 新 Service Worker 取得控制权后,将会触发其 activate 事件。
this.addEventListener('activate', function(event) {
var cacheAllowlist = ['pages-cache-v1', 'blog-posts-cache-v1'];
event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheAllowlist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

缓存和返回请求:
在安装 Service Worker 且用户转至其他页面或刷新当前页面后,Service Worker 将开始接收 fetch 事件。下面是缓存优先的策略:
首先监听浏览器 fetch 事件,拦截原本的请求。
检查 cache 中是否存在将要请求的资源,有则返回缓存。
然后远程请求资源,将资源缓存后返回。

this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
     
        if (response) {
          return response;
        }
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
       
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});
PWA

PWA(Progressive Web Apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。它是有多种技术合集实现web页面能具备app三大准则(粘性,快速响应,离线可靠)的概念。相关技术支持:Web App Manifest,Service Worker,Notifications API,Web Push。service worker 是实现pwa理念的核心技术支持。

优点: 可被发现、易安装、可链接、独立于网络、渐进式、可重用、响应性和安全的。
- 渐进式 - 适用于所有浏览器,因为它是以渐进式增强作为宗旨开发的
- 连接无关性 - 能够借助 Service Worker 在离线或者网络较差的情况下正常访问
- 类原生应用 - 由于是在 App Shell 模型基础上开发,因此应具有 Native App 的交互,给用户 Native App 的体验
- 持续更新 - 始终是最新的,无版本和更新问题
- 安全 - 通过 HTTPS 协议提供服务,防止窥探,确保内容不被篡改
- 可索引 - manifest 文件和 Service Worker 可以让搜索引擎索引到,从而将其识别为『应用』
- 黏性 - 通过推送离线通知等,可以让用户回流
- 可安装 - 用户可以添加常用的 Web App 到桌面,免去到应用商店下载的麻烦
- 可链接 - 通过链接即可分享内容,无需下载安装

缺点:
- 对系统功能的访问权限较低
- 没有审查标准

Web App Manifest简介:manifest 类型的文件一般用来配置app的基础属性,当然在pwa理念中,web项目manifest.json项目同样用来处理形成桌面应用的名称、结构、图标、打开路径、应用平台等。通过link的ref=manifest类型 地址引入使用。
具体配置config详情参考web app manifesst

部署一个 manifest

Web应用程序清单部署在您的HTML页面中,使用在你的文件的头部的一个链接标记:
index.html

<head>
  <title>Minimal PWA</title>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel="icon" href="/e.png" type="image/png" />
</head>

manifest.json :

{
  // 复制后需要把注释删除掉
  "name": "Minimal PWA", // 必填 显示的插件名称
  "short_name": "PWA Demo", // 可选  在APP launcher和新的tab页显示,如果没有设置,则使用name
  "description": "The app that helps you understand PWA", //用于描述应用
  "display": "standalone", // 定义开发人员对Web应用程序的首选显示模式。standalone模式会有单独的
  "start_url": "/", // 应用启动时的url
  "theme_color": "#313131", // 桌面图标的背景色
  "background_color": "#313131", // 为web应用程序预定义的背景颜色。在启动web应用程序和加载应用程序的内容之间创建了一个平滑的过渡。
  "icons": [ // 桌面图标,是一个数组
    {
    "src": "icon/lowres.webp",
    "sizes": "48x48",  // 以空格分隔的图片尺寸
    "type": "image/webp"  // 帮助userAgent快速排除不支持的类型
  },
  {
    "src": "icon/lowres",
    "sizes": "48x48"
  },
  {
    "src": "icon/hd_hi.ico",
    "sizes": "72x72 96x96 128x128 256x256"
  },
  {
    "src": "icon/hd_hi.svg",
    "sizes": "72x72"
  }
  ]
}

Notifications API简介: Notifications API 是浏览器提供用户的通知接口,用于桌面应用或者移动app,主要是显示在通知栏,类似消息提示等。
相关接口及API 详情Notifications API

Web Push API 简介: web push 为浏览器提供消息推送功能,该推送服务涉及三方交互并不能直接与后端交互,包含浏览器客户端web项目,浏览器厂商提供的push service(国外服务存在跨域问题),项目对应后端服务。

image.png

操作流程如下:
Ask Permission:这一步不在上图的流程中,这其实是浏览器中的策略。浏览器会询问用户是否允许通知只有在用户允许后,才能进行后面的操作。
步骤一.Subscribe:浏览器(客户端)需要向Push Service发起订阅(subscribe),订阅后会得到一个
Push Subscription对象。
步骤二.Monitor:订阅操作会和Push Service进行通信,生成相应的订阅信息,Push Service会维护相应信息,并基于此保持与客户端的联系。
步骤三.Distribute Push Resource:浏览器订阅完成后,会获取订阅的相关信息(存在于PushSubscription对象中),我们需要将这些信息发送到自己的服务端,在服务端进行保存。

步骤一和步骤二
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Progressive Times</title>
    <link rel="manifest" href="/manifest.json">                                      
  </head>
  <body>
    <script>
      var endpoint;
      var key;
      var authSecret;
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      // 方法很复杂,但是可以不用具体看,知识用来转化vapidPublicKey用
      function urlBase64ToUint8Array(base64String) {                                  
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
          .replace(/\-/g, '+')
          .replace(/_/g, '/');
        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);
        for (let i = 0; i < rawData.length; ++i) {
          outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
      }
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (registration) {
          return registration.pushManager.getSubscription()                            
            .then(function (subscription) {
              if (subscription) {                                                      
                return;
              }
              return registration.pushManager.subscribe({                              
                  userVisibleOnly: true,
                  applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
                })
                .then(function (subscription) {
                  var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                  key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                  var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                  authSecret = rawAuthSecret ?
                    btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                  endpoint = subscription.endpoint;
                  return fetch('./register', {                                         
                    method: 'post',
                    headers: new Headers({
                      'content-type': 'application/json'
                    }),
                    body: JSON.stringify({
                      endpoint: subscription.endpoint,
                      key: key,
                      authSecret: authSecret,
                    }),
                  });
                });
            });
        }).catch(function (err) {
          // 注册失败 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

步骤三 服务器发送消息给service worker
app.js

const webpush = require('web-push');                 
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(                             
  'mailto:contact@deanhume.com',
  'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
  'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', function (req, res) {           
  var endpoint = req.body.endpoint;
  saveRegistrationDetails(endpoint, key, authSecret); 
  const pushSubscription = {                          
    endpoint: req.body.endpoint,
    keys: {
      auth: req.body.authSecret,
      p256dh: req.body.key
    }
  };
  var body = 'Thank you for registering';
  var iconUrl = 'https://example.com/images/homescreen.png';
  // 发送 Web 推送消息
  webpush.sendNotification(pushSubscription,          
      JSON.stringify({
        msg: body,
        url: 'http://localhost:3111/',
        icon: iconUrl
      }))
    .then(result => res.sendStatus(201))
    .catch(err => {
      console.log(err);
    });
});
app.listen(3111, function () {
  console.log('Web push app listening on port 3111!')
});

service worker监听push事件,将通知详情推送给用户
service-worker.js

self.addEventListener('push', function (event) {
 // 检查服务端是否发来了任何有效载荷数据
  var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
  var title = 'Progressive Times';
  event.waitUntil(
    // 使用提供的信息来显示 Web 推送通知
    self.registration.showNotification(title, {                           
      body: payload.msg,
      url: payload.url,
      icon: payload.icon
    })
  );
});

PWA实现基础:
2016年 google 在next web 会议中提出pwa理念并在随后的chrome版本做出来响应的api提供支持,当下chrome和Firefox已经不同程度的实现了pwa 核心技术service worker相关的接口,也存在大量其他浏览器未做处理,故pwa在浏览器内核及版本上存在兼容问题。

pwa 注册相关:
大部分web项目使用pwa 主要在于使用service worker 的cache相关缓存及manifest可配置桌面应用,类似大型文档交互类项目 语雀(yuque),这些web'项目的缓存在于sw文件脚本的维护,pwa 提供注册于销毁api 来配置项目的sw服务脚本,如下所示:

// pwa 注册 service worker 服务
const registerServiceWorker = () => {
  if (!("serviceWorker" in navigator)) {
    return null
  }
  navigator.serviceWorker.register(
    '/sw.ts',
    {
      scope: SW_SCOPE,
    }
  ).then(reg => {
    // 注册成功后的callback
  })
}
// pwa 注销service worker 服务
const unregisterServiceWorker = () => {
  if (!("serviceWorker" in navigator)) {
    return Promise.resolve(false)
  }
  return navigator.serviceWork
    .getRegistration(SW_SCOPE)
    .then(registration =>
      (registration ? registration.unregister() : false)
    ).then(unregistered => {
       if (unregistered) {
         // 注销成功后的callback
       }
       return unregistered
    })
}  

pwa的可靠性主要逻辑处理在service worker注册的sw.ts脚本文件上 ,注册和销毁只是提供触发关联机制,脚本没有过期时间,智能通过unregistered 销毁

pwa离线缓存之service worker

离线缓存的功能确保web的可靠性,解决web 强依赖网络状态的情况,大大提升项目的用户粘性和数据可靠。注册service worker服务并配置缓存后,可配置浏览器服务监听项目web资源请求,通过浏览器提供的cache api 缓存request- respond 结构内容。

service worker 生命周期

parsed service worker 脚本解析

  pwa 注册service worker 脚本sw文件后,正常进去service worker 解析的生命周期,该周期service worker服务在于解析脚本逻辑

install service worker 安装
在service worker的脚本sw文件解析成功后会进入脚本的安装,此阶段通过self (service worker 注册后全局scope 属性ServiceWorkerGlobalScope)监听 install 状态 执行 awaitUtil 回调,awaitUtil接受 promise 参数,在promise 的pedding状态时service worker 一直处于installing 进行中状态,一般建议不能执行耗时过长的类似插入、读取缓存等操作避免超时安装失败。resolve后awaitUtil 执行结束,
install 状态中 支持self调用skipwaitting api,一般用于service worker脚本更新,新旧sw文件交换页面控制权

activate service worker 激活
service worker的激活阶段相关回调类似安装阶段。业务逻辑区别在于此时已经可读同源缓存cache内容 ,一般可用于处理更新非当前项目区间内容,减少查询复杂度

redundant service worker 服务销毁
service worker 脚本安装或激活失败后直接进入该生命周期,可用来处理缓存空间释放或关闭未执行完事件。

var cacheStorageKey = 'minimal-pwa'
var cacheList = [   '/',   "index.html",   "*.css",   "*.png",   "pwa-fonts.png" ]
// self ServiceWorkerGlobalScope

self.addEventListener('install', function(e) {
   console.log('cache event!')
   e.waitUntil(
     caches.open(cacheStorageKey)
       .then(function(cache) {
          console.log('adding to cache:', cacheList)
          return cache.addAll(cacheList)
       }).then(function() {
        console.log('skip waiting!')
        return self.skipWaiting()
       })
   )
})

self.addEventListener('activate', function(e) {
  console.log('activate event')
  e.waitUntil(
    caches.keys()
      .then(function(cacheNames) {
         return Promise.all(
           cacheNames.filter(function(cacheName) {
             return cacheName !== cacheStorageKey;
           }).map(function(cacheName) {
             return caches.delete(cacheName);
           })
         );
      })
  )
})

self.addEventListener('redundent', function(e) {
  console.log('redundent event')   // 清理 延时未完成任务 
})

注意: cache 缓存结构key-value是直接以资源的request 与 respond 储存,不需要转换成字符串

service worker 接口缓存
service worker提供服务cache api 提供读写缓存,据上文代码我们简单对文件的get 资源(html, css,js, png)等统一缓存,当用户再次打开已注册service worker服务脚本的web项目时,理想的状态时用户不管是断网还是正常情况,请求的静态页面资源直接从cache 缓存中获取,但上文代码只维护了缓存空间及存储资源内容,读写缓存如下配置:
cache web api 缓存空间

  fetch 资源请求监听
  service worker的资源请求监听是基于fetch的请求处理,而fetch的请求是在html 5提出的方案,所以service worker 只能监听到fetch资源请求。
self.addEventListener('fetch', function(e) {
   console.log('fetch event:', e.request.url)
   e.respondWith(
     caches.match(e.request)
      .then(function(response) {
        if (!!response) {
          console.log('using fetch cache for:', e.request.url)
          return response
        }

        const fetchRequest = e.request.clone()

        return fetch(fetchRequest).then(res => {
          const responseToCache = res.clone()
          caches.open(cacheStorageKey).then(function(cache) {
            cache.put(e.request, responseToCache)
          })
          
          return res;
        })
     })
  )
})

fetch监听回调会提供respondWith api 作为资源响应输出,主要流程通过fetch请求request 在缓存caches中匹配,命中缓存后配置响应体,并对响应体异常或空的状态做出请求并更新缓存。 pwa存在问题思考 资源热更新 web项目使用pwa后如需更新线上资源,service worker的特性静态资源取自缓存,无法实时识别线上最新资源,解决方案如下

      1.设置缓存资源hash值,caches 缓存资源的key值依赖于request,为确保每次线上资源更新能实时响应,可在web端项目直接维护版本信息的hash内容,当文件变化确保更新文件资源定点更新接口request body的hash值不同,导致首次缓存不能匹配获取的为线上资源

      【缺点】:hash值的控制无法指定caches keys的范围,导致caches会越来越庞大,需要定时清理,其次每次请求都会携带无效hash内容,浪费请求带宽

       2.配置sw脚本消息通知逻辑,通过push service服务连接后端服务,维护后端更新资源接口,在web项目发版上线成功时推送更新缓存key值信息到push service使之push message得到响应更改脚本内部缓存list,完成实时获取线上资源和本地资源切换逻辑。

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

推荐阅读更多精彩内容