PWA 缓存

目录

  • 一、PWA
    • 1、认识 PWA
    • 2、用 Manifest 将 web 应用添加至主屏幕
    • 3、Service Worker
      • (1)、什么是 Service worker
      • (2)、Service Worker 缓存
      • (3)、Service Worker 实现消息推送
    • 4、结合 webpack 实现 PWA
      • (1)、在 index.html 文件中启用 ServiceWorker
      • (2)、修改 webpack 的配置文件
        • ①、了解 workbox
        • ②、在 webpack 配置文件中使用 `workbox-webpack-plugin` 插件。
    • 5、使用 Chrome Devtools 调试 Service Worker


一、PWA

1、认识 PWA

【原生应用、Web App 与 PWA 的对比】
原生应用(Native app):

  • 好处:
    - 更高的安全性。
    - 更强的计算能力。
    - 更好的 UX(用户体验)。
    - 更少的电池消耗。
  • 不足:
    - 开发成本高(iOS 和 安卓)、速度慢。
    - 版本更新需要将新版本上传到不同的应用商店;
    - 必须去下载才能使用 app。

    网页应用(Web App):
  • 好处:
    - 开发成本低、速度快。
    - 版本更新时上传最新的资源到服务器即可。
    - 应用的加载速度更快。
  • 不足:
    - 缺少离线使用能力。在离线或者在弱网环境下基本上是无法使用的。
    - 缺少了消息推送的能力。
    - 桌面入口不够便捷,想要进入一个页面每次都需要通过浏览器来打开。

    渐进式网页应用(PWA):针对以上 Web App 的缺陷,PWA 进行了查缺补漏。
  • 可以添加至主屏幕。
  • 实现启动动画以及隐藏地址栏。
  • 实现离线缓存功能。即使用户手机没有网络,依然可以使用一些离线功能。
  • 实现消息推送。
  • 实现秒开优化。
  • 兼容性良好:现在 IE 退出了,iOS 也兼容 PWA 了。
  • 其余的优点继承 Web App。

    这些特性将使得 PWA 应用非常接近原生 App,在加载速度方便甚至超越了原生 App。

PWA(Progressive Web App)是渐进式 web app。它实现了网页应用的安装、离线缓存 和 消息推送等功能。

PWA 的功能:

  • 通过引入 Manifest 将 web 应用 添加至主屏幕
  • 通过引入 Service Worker 实现了 离线缓存消息推送

2、用 Manifest 将 web 应用添加至主屏幕

如果需要赋予该 web app 添加至主屏幕(能够作为独立应用独立安装)的能力,则需要 2 步:

  • 在 index.html 中注入 manifest
  • 实现 manifest.json 文件。

在 index.html 中注入 manifest:

<head>
  <link rel="manifest" href="/manifest.json" />
head>

实现 manifest.json 文件:

{
  // 复制后需要把注释删除掉
  "name": "My 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"
  }
  ]
}

Manifest参考文档

3、Service Worker

(1)、什么是 Service worker

Service Worker 是一个在页面和网络之间的拦截器。是基于 HTML5 API 实现的。也是 PWA 的核心。

Service Worker 能够 拦截请求缓存资源,从而完全控制你的网站。

Service Worker 的结构如下图:
PWA 缓存_第1张图片
Service Worker 的实现原理:

  • Service Worker 的是基于 Web Worker 的功能来实现的。不同之处在于,Service Worker 具有存储功能,并且 Service Worker 是与浏览器的生命周期相关联的而非某个页面。

【拓展】Web Worker
Web Worker 让 JavaScript 能够运行在页面主线程之外,不过由于 Web Worker 中是没有当前页面的 DOM 环境的,所以在 Web Worker 中只能执行一些和 DOM 无关的 JavaScript 脚本,并通过 postMessage 方法将执行的结果返回给主线程。所以说在 Chrome 中, Web Worker 其实就是在渲染进程中开启的一个新线程,它的生命周期是和页面关联的。

Service Worker 的特点:

  • 一个独立的执行线程,单独的运行环境——会在浏览器的渲染进程中,开启一个新的事件驱动型服务线程,运行于浏览器后台,与整个浏览器的生命周期绑定在一起。
  • 单独的作用域范围,能够为作用域范围内的页面请求提供服务。
  • 处于安全性考虑,必须在 HTTPS 环境下才能工作。在本地调试时,使用 localhost 则不受 HTTPS 限制——service worker 的权限太大,能拦截所有页面的请求,若对 HTTP 的请求做拦截处理,很容易受到网络攻击。
  • 由于是独立线程,Service Worker 不能操作页面 DOM。但可以通过事件机制来将执行的结果返回给浏览器主线程,例如使用:postMessage 方法。
  • 一旦被 install 就永远存在,除非被手动 unregister。即使Chrome(浏览器)关闭也会在后台运行。利用这个特性可以实现离线消息推送功能。
  • 一旦请求被 Service Worker 拦截接管,意味着任何请求都由你来控制,一定要做好容错机制,保证页面的正常运行。

【拓展】目前 Service Worker 的各大浏览器的兼容情况:请戳这里。

(2)、Service Worker 缓存

service worker 技术实现离线缓存,可以将一些不经常改变的静态文件放到缓存中,提升用户体验。

Service Worker 缓存的实现步骤:

  • 在 index.html 文件中启用 ServiceWorker。
  • 编写 service-worker.js 的缓存脚本。

index.html 中:

DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Caching World!title>
  head>
  <body>
    
    <img src="/images/hello.png" />                 
    
    <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 请求还要再消耗一次,所以我们需要在此时克隆请求。

(3)、Service Worker 实现消息推送

service worker 一旦被 install 就永远存在,除非被手动 unregister。即使Chrome(浏览器)关闭也会在后台运行。利用这个特性可以实现离线消息推送功能。

Service Worker 实现消息推送的步骤:

  • 提示用户并获得他们的订阅详细信息。
  • 将这些详细信息保存在服务器上。
  • 在需要时发送任何消息。

【拓展】不同浏览器需要用不同的推送消息服务器。以 Chrome 上使用 Google Cloud Messaging 作为推送服务为例,第一步是注册 applicationServerKey(通过 GCM 注册获取),并在页面上进行订阅或发起订阅。每一个会话会有一个独立的端点(endpoint),订阅对象的属性(PushSubscription.endpoint) 即为端点值。将端点发送给服务器后,服务器用这一值来发送消息给会话的激活的 Service Worker (通过 GCM 与浏览器客户端沟通)。

具体实现如下:

①、index.html 文件中:实现提示用户并获得他们的订阅详细信息,并将这些详细信息保存在服务器上。

DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Progressive Timestitle>
    <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>

②、app.js 文件中:实现服务器发送消息给 service worker。

const webpush = require('web-push');                 
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(                             
  'mailto:[email protected]',
  '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(200))
    .catch(err => {
      console.log(err);
    });
});
app.listen(3000, 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
    })
  );
});

4、结合 webpack 实现 PWA

结合 webpack 实现 PWA 的步骤:

  • 在 index.html 文件中启用 ServiceWorker。
  • 在 webpack 配置文件中使用 workbox-webpack-plugin 插件。
  • 编写 service-worker.js 脚本。

(1)、在 index.html 文件中启用 ServiceWorker

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker
        .register('/service-worker.js') // 这里无需传入第二个参数 scope 指定 serviceWorker 的作用域,会在 webpack 里通过 urlPattern 来实现。
        .then((registration) => {
          // console.log('ServiceWorker registered: ', registration);
        })
        .catch((err) => {
          console.log('ServiceWorker registration failed: ', err);
        });
    });
  }
script>

【注意】尽量把这段 js 放在其他资源引入之前。

(2)、修改 webpack 的配置文件

workbox-webpack-plugin 插件是 workbox 的构建工具的实现之一。

①、了解 workbox

workbox 是 GoogleChrome 团队推出的一套 Web App 静态资源本地存储(静态资源离线缓存)的解决方案,该解决方案包含一些 Js 库和构建工具。

workbox 的 5 种缓存模式

  • stateWhileRevalidate
    • 允许你使用缓存的内容尽快响应请求(如果可用),如果未缓存,则回退到网络请求。 然后,网络请求用于更新缓存。
    • 这是一种相当常见的策略,适合更新频率很高但重要性要求不高(至少允许一次缓存 读取)的内容。在有缓存的情况下,该模式保证了客户端第一时间拿到数据的同时,也会去请求网络资源更新缓存, 保证下次客户端从缓存中读取的数据是最新的。因此该模式不能减轻后台服务器的访问压力,但却能给前端用户提供 快速响应的体验。
  • networkFirst(网络回落到缓存):
    • 默认情况下,它将尝试从网络获 取最新请求。 如果请求成功,它会将响应放入缓存中。 如果网络无法返回响应,则将使用缓存响应
    • 这意味着只要当 第一次请求成功时,service worker 就会缓存一份请求结果。当后续重复请求时,缓存也会每次更新。若网络请求失 败时,只要缓存里有内容,就能让前端获取一个响应(从service worker的缓存里)。
    • 因此,此种模式提高了前端页 面的稳固性,即使在网络请求失败的情况下,页面也能从上次的缓存中读取到数据展示,而不是粗鲁的告诉用户网络请 求失败的一个响应。
    • 这种策略一般适用于返回结果不太固定或对实时性有要求的请求,为网络请求失败进行兜底。对于经常更新且关键(由业务判断出来)的请求,网络优先策略是理想的解决方案。
  • cacheFirst(缓存优先,缓存回落到网络):
    • 如果缓存中有响应,则将使用缓存的响应来完成请求,根本不会使用网络。 如果没有缓存的响应,则将通过网络请求 来满足请求,并且将缓存响应,以便直接从缓存提供下一个请求。
    • 该模式可以在为前端提供快速响应的同时,减轻后 台服务器的访问压力。但是数据的时效性就需要开发者通过设置缓存过期时间或更改sw.js里面的修订标识来完成缓 存文件的更新。一般需要结合实际的业务场景来判断数据的时效性。
  • networkOnly:强制使用正常的网络请求,并将结果返回给客户端,这种策略比较适合对实时性要求非常高的请求。(不建议使用)
  • cacheOnly:直接使用 Cache 缓存的结果,并将结果返回给客户端,这种策略比较适合一上线就不会变的静态资源请求。(不建议使用)

②、在 webpack 配置文件中使用 workbox-webpack-plugin 插件。

const { GenerateSW } = require('workbox-webpack-plugin');
const id = new Date().getTime();

// 在 plugins 数组中添加以下配置
new GenerateSW({
  clientsClaim: true,
  skipWaiting: true,
  maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
  cacheId: `${id}-gsw`,
  exclude: ['service-worker.js'],
  // mode: 'development',
  runtimeCaching: [
  // 根据实际情况配置符合条件资源的缓存
    {
      urlPattern: /^https:\/\/test.ceshi.com\/static/,
      handler: 'CacheFirst', // 采用“缓存优先”的缓存模式
      options: {
        cacheName: `${id}-test-cdn`,
        expiration: {
          maxEntries: 50,
          maxAgeSeconds: 60 * 60 * 24 * 30, // 30 Days
        },
        cacheableResponse: {
          statuses: [0, 200],
        },
      },
    },
  ],
}),

5、使用 Chrome Devtools 调试 Service Worker

这是一个实现了 service worker 离线缓存功能的网站,打开调试工具,调试 service worker。

PWA 缓存_第2张图片





【推荐阅读】
渐进式 Web 应用(PWA)——MDN
PWA 技术解析及爱奇艺 PC 端的实践


【参考文章】
PWA Builder Blog
讲讲 PWA
PWA 构建器博客
第一本 PWA 中文书
PWA 英文书
网站渐进式增强体验(PWA)改造:Service Worker 应用详解
Basic Service Worker Sample
【翻译】Service Worker 入门
Web App Manifest
Service Workers: an Introduction
The Offline Cookbook
微信小程序和PWA对比分析
使用workbox-webpack-plugin实现PWA
npm 之 workbox-webpack-plugin
workbox-webpack-plugin创建pwa
Webpack 插件扫盲系列(四) WorkboxWebpackPlugin
深入浅出 PWA
前端缓存-workbox缓存策略

你可能感兴趣的:(代码优化之道,计算机与网络,前端缓存,PWA)