PWA的使用

809 阅读5分钟

Progressive Web App介绍

  • pwa可以让用户保存网页入口,并且离线访问
  • pwa依赖Service Worker功能

Web App Manifest(配置添加到桌面的设置项)

// html插入

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

{
  "name": "客服管理系统", // 应用名称
  "short_name": "客服管理系统", // 桌面应用的名称
  "display": "standalone", // 显示模式
  "background_color": "#fff", // 背景颜色
  "description": "一个集成工单处理,客服管理的系统", // 应用描述
  "start_url": "/index.html", // 打开时的网址
  "theme_color": "#fff", // 主题颜色
  "icons": [ // 图标,自动设置尺寸,图标尺寸需要和实际的一样
    {
      "src": "./android-chrome-maskable-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./android-chrome-maskable-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

  • tomitm.github.io/appmanifest… 可以根据配置自动生成manifest的网站
  • iOS系统对manifest.json属于部分支持,所以我们需要在head里给配置meta属性才能让iOS系统更加完善.
  <!-- 图标,支持尺寸匹配 -->
  <meta name="apple-touch-icon" sizes="192x192" href="/android-chrome-maskable-192x192">
  <meta name="apple-touch-icon" sizes="512x512" href="/android-chrome-maskable-512x512">
  <!-- short_name一致,标题 -->
  <meta name="apple-mobile-web-app-title" content="">
  <!-- 显示模式,有且仅支持yes,代表Standalone -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  
  // 还有其他很多属性,在上面提供的网址,选择也可生成

service Worker 特点

  • 不能访问/操作dom
  • 所有api都是基于promise的
  • 只有在https与localhost下navigator里才会有serviceWorker这个对象
  • 离线缓存
  • 不会随浏览器关闭而关闭,必须手动卸载

service Worker生命周期

触发更新条件

  • 当未安装serviceWorkbench的页面都打开时
  • 当上次写入ServiceWorker数据库的时间戳与本次之间超过24小时
  • 当该注册文件(sw.js)发生更新时

installing

当更新条件触发时,install会被执行,需要将声明的资源加入缓存之中

installed

install完成之后,进入该阶段,需要等待激活,可以告知用户需要升级了

activating

当前没有其他激活的serviceWorker,或者脚本中手动调用skipWaiting,或者作用域下面的页面全被关闭了,会进入该阶段,在该阶段activate会被执行,需要将之前的缓存清理了,

activated

activating完成之后,进入该阶段,serviceWorker内的fetch(网络请求)和message(与客户端通信)可以被监听了

redundant

安装或激活失败,或者当前serviceWorker被其他所替换,就会进入该状态

Cache(为ServiceWorker提供缓存)

Cache 接口为缓存的 Request / Response 对象对提供存储机制,例如,作为ServiceWorker 生命周期的一部分。请注意,Cache 接口像 workers 一样,是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中, 但是它不必一定要配合 service worker 使用.

caches.open(cacheName) 打开(创建)一个cache对象,用作缓存
Caches.match(request, options) 返回一个 Promise对象,resolve的结果是匹配到的第一条缓存
Caches.matchAll(request, options) 返回一个Promise 对象,resolve的结果是跟Cache对象匹配的所有请求组成的数组。
Caches.addAll(requests)接收一个URL数组,检索并把返回的response对象添加到给定的Cache对象。
Caches.delete(request, options)搜索key值为request的Cache 条目。如果找到,则删除该Cache 条目,并且返回一个resolve为true的Promise对象;如果未找到,则返回一个resolve为false的Promise对象。
Caches.keys(request, options)返回一个Promise对象,resolve的结果是Cache对象key值组成的数组。

实践

注册serviceWorker

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then(res => {
        // 当serviceWorker更新时触发
        res.onupdatefound = () => {
          const e = res.installing;
          // 当更新流程状态发生变化的时候触发
          e.onstatechange = () => {
            switch (e.state) {
              case 'installed':
                // 根据当前有没有serviceWorker,来判断是更新还是新的安装
                navigator.serviceWorker.controller
                  ? // 更新可以设计为向用户询问是否更新,手动触发,向sw.js传递信息,让其调用skipWaiting,进入activeting状态
                    console.log('serviceWorker有新的内容可以使用!')
                  : console.log('serviceWorker现在已经离线可用了!');
                break;
              case 'redundant':
                console.error('当前serviceWorker已经失效了.');
            }
          };
        };
      })
      .catch(error => {
        console.log('serviceWorker注册失败了!', error);
      });
  });
}

安装,更新,缓存

// 获取当前格式化后的时间
const time = () =>
  new Date(new Date() + 8 * 3600 * 1000)
    .toJSON()
    .substr(0, 19)
    .replace('T', ' ');

// 缓存的名称,根据当前时间变化而变化
const CACHE_NAME = `cache ${time()}`;

// 缓存资源列表
const CACHE_LISTS = [
  '/',
  '/workbench.js',
  '/workbench/',
  '/manifest.json',
  '/workbench/myTicketComponent.js',
  '/fonts/element-icons.313f7da.woff',
  '/fonts/element-icons.4520188.ttf'
];

// 监听请求事件,event.respondWith可以劫持相应内容,通过这个,在这里可以处理离线请求问题
self.addEventListener('fetch', e => {
  e.respondWith(
    // 方案1, 请求成功走网络,失败走缓存
    // fetch(e.request).catch(err => caches.match(e.request))

    // 方案2, 请求后直接走缓存,缓存没有才走网络
    caches
      .match(e.request)
      .then(cache => {
        // 有缓存就用缓存,没有就重新发请求获取
        return cache || fetch(e.request);
      })
      .catch(err => {
        // 缓存报错还直接重新发请求获取
        return fetch(e.request);
      })
  );
});

// 什么情况下会触发serviceWorker更新:
// 1,当未安装serviceWorkbench的页面都打开时
// 2,当上次写入ServiceWorker数据库的时间戳与本次之间超过24小时
// 3,当该sw文件发生更新时

// 注意: serviceWoker如果上一个没有销毁,需要手动调用skipWating才能跳过等待
self.addEventListener('install', e => {
  // service worker标准提供一个waitUntil方法,当oninstall或者onactivate触发时被调用,接受一个promise。在这个 promise被成功resolve以前,不会分发到serviceWorker中继续执行
  e.waitUntil(
    caches
      .open(CACHE_NAME)
      .then(cache => cache.addAll(CACHE_LISTS))
      .then(skipWaiting) // 缓存后手动跳过当前serviceWorker,进入下个serviceWorker激活,该操作也可以进行询问用户是否更新来进行操作
  );
});
// 当更新激活完毕后,将之前的缓存清除
self.addEventListener('activate', e => {
  e.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys.map(key => {
          // 如果新获取到的key和之前缓存的key不一致,就删除之前版本的缓存
          if (key !== CACHE_NAME) {
            return caches.delete(key);
          }
        })
      );
    })
  );
  //当一个 serviceWorker 被初始注册时,页面在下次加载之前不会使用它,clients.claim方法会立即控制这些页面
  return self.clients.claim();
});


offline-plugin(webpack插件)

介绍

  • 自动生成sw相关逻辑
  • 缓存列表根据打包资源自动生成

实践

webpack配置

const OfflinePlugin = require('offline-plugin');

plugins: [
    new OfflinePlugin({
      responseStrategy: 'cache-first', // 缓存/网络优先
      AppCache: false, // 不启用appCache
      safeToUseOptionalCaches: true, // Removes warning for about `additional` section usage
      autoUpdate: true, // 自动更新
      caches: {
        // webpack打包后需要换的文件正则匹配
        main: [
          '**/*.js',
          '**/*.css',
          /\.(png|jpe?g|gif|svg)(\?.*)?$/,
          /\.(woff2?|eot|ttf|otf)(\?.*)?$/
        ], // install时缓存
        additional: [':externals:'],
        optional:[] // 这里配置的文件清单在serviceWorker安装激活阶段不会进行缓存,只有在监听到网络请求的时候才进行缓存
      },
      externals: [
        'http://127.0.0.1:3000/api/workbench/me',
        'http://127.0.0.1:3000/api/user'
      ], // 设置外部链接,例如配置http://hello.com/getuser,那么在请求这个接口的时候就会进行接口缓存
      excludes: ['**/.*', '**/*.map', '**/*.gz', '**/manifest-last.json'], // 需要过滤的文件
      ServiceWorker: {
        output: './static/sw.js', // 输出目录
        publicPath: '/static/sw.js', // sw.js 加载路径
        scope: '/', // 作用域(此处有坑)
        minify: true, // 开启压缩
        events: true // 当sw状态改变时候发射对应事件
      }
    })
]

offline-plugin配置项

Caches: 'all' | Object,
all: 意味着所有webpack构建出来的资源,以及在externals选项中的资源都会被缓存。
Object: 包含三个数组或正则的配置对象(main, additional, optional),它们都是可选的,且默认为空。
默认:all。
externals: []
允许开发者指定一些外部资源(比如CDN引用,或者不是通过webpack生成的资源)。配合Caches的additional项,能够实现缓存外部资源的功能。
默认:null。
举例:['fonts/roboto.woff']。
ServiceWorker: Object | null | false
events:布尔值。允许 runtime 接受来自 Service Worker 的消息,默认值为 false。
navigateFallbackURL:当一个 URL 请求从缓存或网络都无法被获取时,将会重定向到该选项所指向的 URL
updateStrategy:'all' | 'changed'
指定了缓存策略选择全部更新,另外一种是增量更新changed

项目配置

import * as OfflinePluginRuntime from 'offline-plugin/runtime';

OfflinePluginRuntime.install({
  onUpdateReady: () => {
    // 资源下载完毕后,调用applyUpdate即skipwaiting()方法
    if (window.confirm('发现新版本,是否更新?')) {
      OfflinePluginRuntime.applyUpdate();
    }
  },
  onUpdated: () => {
    console.log('SW Event:', 'onUpdated')
    window.swUpdate = true
  }
});