PWA和serviceWorker 监听请求与缓存资源

相比起(一)做一些webApp配置,缓存与拦截请求这一块才是我最想了解的。

参考的仍然是(一)的文章,还有一些别的:
mdn-Service Worker API
预缓存方案
PWA学习手册
Service Worker简易教程-推荐看

web应用缓存资源

web应用能做到离线可用,主要就是看serviceWorker在有网的时候先缓存资源,离线后再利用缓存里的资源显示给用户。

可以把Service Worker简单理解为一个独立于前端页面,在后台运行的进程。因此,它不会阻塞浏览器脚本的运行,同时也无法直接访问浏览器相关的API(例如:DOM、localStorage等)。此外,即使在离开你的Web App,甚至是关闭浏览器后,它仍然可以运行。它就像是一个在Web应用背后默默工作的勤劳小蜜蜂,处理着缓存、推送、通知与同步等工作。所以,要学习PWA,绕不开的就是Service Worker。

注册serviceworker

看了很多知识之后,自信满满地开始写代码,流程大概酱紫:先register注册到自己网站上,然后缓存几张图片看看,再拦截请求看看,再缓存请求看看。

但是一开始注册sw就出问题了。

一开始:我把sw.js放在src/utils目录下,代码如下:

javascript 复制代码
export const registServiceWorker = () => {
  if ('serviceWorker' in navigator) {
    // 注册service worker
    navigator.serviceWorker
      .register('./sw.js')
      .then(function (res) {
        console.log('--SW register--');
        console.log(res);
      })
      .catch(function (err) {
        console.log(err);
      });
  }
};

在页面入口Layout.js中使用,放在componentDidMount中。

javascript 复制代码
import * as ServiceWorker from '@/utils/sw';

ServiceWorker.registServiceWorker();

然后就报错啦

sw.js:74 DOMException: Failed to register a ServiceWorker for scope ('http://localhost:3000/') with script ('http://localhost:3000/sw.js'): The script resource is behind a redirect, which is disallowed.

查询了一下原因是酱紫的:

Service Worker注册时有一个 作用范围(scope)的概念。例如,一个注册在https://www.sample.com/list路径下的Service Worker,其作用的范围只能是它本身与它的子路径。

意思是我的sw文件放在src/utils页面下,管不到页面路径/,而且sw文件在'http://localhost:3000/sw.js'这个路径下找不到,所以:我们的sw.js最后还是要放到公共的文件夹中,scope设定的路径是当前要生效的页面路径。

比如:我把sw.js文件放在public下的static文件夹下,生效页面为/list,则register要这么写:navigator.serviceWorker.register('/static/sw.js',{scope:'/list/'})

然后我就修改了一下:把register方法仍然留在utils里,layout中引入注册,其他的sw线程要执行的代码挪到public中。这时候我们再打开/list页面就可以看到sw开起来了。

javascript 复制代码
// /utils/sw.js
export const registServiceWorker = () => {
  if ('serviceWorker' in navigator) {
    // 注册service worker
    navigator.serviceWorker
      .register('/sw.js',{scope:'/'})
      .then(function (res) {
        console.log('--SW register--');
        console.log(res);
      })
      .catch(function (err) {
        console.error(err);
      });
  }
};

在public/sw.js中加上其他的事件监听,简单的静态资源缓存和fetch请求拦截。

javascript 复制代码
/**
 * service worker
 */
const cacheName = 'cacheName'; // 静态资源缓存
const apiCacheName = 'apiCache'; // 接口请求缓存
const cacheFiles = [
  // 静态资源缓存文件名
  '/',
  './index.html',
  '/test.jpg',
];

const cacheRequestUrls = [
  // 需要缓存的xhr请求
  '/api/getData?',
];

// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', function (e) {
  console.log('Service Worker 状态: install');
  const cacheOpenPromise = caches.open(cacheName).then(function (cache) {
    return cache.addAll(cacheFiles);
  });
  e.waitUntil(cacheOpenPromise);
});

self.addEventListener('fetch', function (e) {
  console.log('现在正在请求:' + e.request.url);

  // 判断当前请求是否需要缓存
  const needCache = cacheRequestUrls.some(function (url) {
    return e.request.url.indexOf(url) > -1;
  });

  if (needCache) {
    // 需要缓存
    // 使用fetch请求数据,并将请求结果clone一份缓存到cache
    // 此部分缓存后在browser中使用全局变量caches获取
    caches.open(apiCacheName).then(function (cache) {
      return fetch(e.request).then(function (response) {
        cache.put(e.request.url, response.clone());
        return response;
      });
    });
  } else {
    // 非api请求,直接查询cache
    // 如果有cache则直接返回,否则通过fetch请求
    e.respondWith(
      caches
        .match(e.request)
        .then(function (cache) {
          return cache || fetch(e.request);
        })
        .catch(function (err) {
          console.log(err);
          return fetch(e.request);
        })
    );
  }
});

可以看到页面上的截图如下:

install的生命周期在register之前执行,执行完成后返回的res是一个ServiceWorkerRegistration 对象。(这个对象可以用来挂载webApp push推送事件)

比如:

javascript 复制代码
    function subscribeUserToPush(registration, publicKey) {
        var subscribeOptions = {
            userVisibleOnly: true,
            applicationServerKey: window.urlBase64ToUint8Array(publicKey)
        }; 
        return registration.pushManager.subscribe(subscribeOptions).then(function (pushSubscription) {
            console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
            return pushSubscription;
        });
    }

fetch拦截请求的能力也把我们的请求路径都打印出来了:

Service Worker的生命周期和全局对象和API

这部分放在另一篇文章里:https://blog.csdn.net/SaRAku/article/details/137561198

缓存静态资源

通常情况下,service Worker 会在其 install 或 fetch 事件处理程序中将资源添加到缓存中。

参考:
PWA常见的缓存策略
Service Worker存储的限制是多少?你的PWA能够存储多少内容?
在做缓存之前,我们要先考虑好缓存容量、过期机制、更新机制。

我们首先需要一个缓存资源列表,当Service Worker被install或者fetch时,会将该列表内的资源缓存进cache。

在 service worker 的 install 事件处理程序中(预缓存):当 service worker 被安装时,浏览器会在 service worker 的全局作用域中触发一个名为 install (en-US) 的事件。此时,service worker 可以预缓存资源,从网络获取它们并将它们存储在缓存中。

在 install 事件处理程序中,我们将缓存操作的结果传递给事件的 waitUntil() 方法。这意味着如果由于任何原因缓存失败,service worker 的安装就会失败:反过来,如果安装成功,service worker 就可以确定资源已添加到缓存中。

javascript 复制代码
const cacheName = "MyCache_1";
const precachedResources = ["/", "/app.js", "/style.css"];
// 写法1
// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', e => {
    var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
        return cache.addAll(cacheFiles);
    });
    e.waitUntil(cacheOpenPromise);
});

// 写法2
async function precache() {
  const cache = await caches.open(cacheName);
  return cache.addAll(precachedResources);
}

self.addEventListener("install", (event) => {
  event.waitUntil(precache());
});

拦截请求

想要拦截请求,使用监听方法'fetch'即可,这样我们就能监听到所有的fetch请求。
实例方法-FetchEvent.respondWith()

respondWith(response)

FetchEvent 接口的 respondWith() 方法阻止浏览器默认的 fetch 操作,并且允许由你自己为 Response 提供一个 promise。

在大多数情况下,你可以提供接收方理解的任何形式的响应。例如,如果是由 初始化的请求,起响应主体必须是图像数据。出于安全考虑,这里有一些全局的规则:

  • 只有当 fetchEvent.request 对象的 mode 是"no-cors",你才能返回 type 为"opaque"的 Response 对象。
  • 只有当 fetchEvent.request 对象的 mode 是"manual",你才能返回 type 为"opaqueredirect"的 Response 对象。
  • 如果 fetchEvent.request 对象的 mode 是"same-origin",你无法返回 type 为"cors"的 Response 对象。

从 Firefox 59 开始,在 service worker 中向 FetchEvent.respondWith() 提供 Response 时,Response.url 的值将作为最终解析的 URL 传输给被拦截的网络请求。如果 Response.url 值是空的字符串,那么 FetchEvent.request.url (en-US) 将被用作最终的 URL。

我们通过调用事件的 respondWith() 方法来返回资源。如果我们没有为某个请求调用 respondWith(),那么该请求将像 service worker 没有拦截它一样发送到网络。因此,如果一个请求没有预缓存,它就直接从网络获取。

javascript 复制代码
const cacheName = "MyCache_1";
const precachedResources = ["/", "/app.js", "/style.css"];

async function cacheFirst(request) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open("MyCache_1");
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    return Response.error();
  }
}

self.addEventListener("fetch", (event) => {
  if (precachedResources.includes(url.pathname)) {
    event.respondWith(cacheFirst(event.request));
  }
});

上方代码中:

  1. caches.match() 是一个语法糖。等效于在每个缓存上调用 cache.match()(按照 caches.keys() 的顺序)直到返回 Response。
  2. 当我们将 networkResponse 添加到缓存时,我们必须克隆响应并将副本添加到缓存中,同时返回原始响应。这是因为 Response 对象是可以流传输的,所以只能读取一次。

拦截图片资源

现在如果我们想要拦截图片资源的请求,判断它们请求失败的时候换上一张缓存中的兜底图片,参考下方的代码:

javascript 复制代码
self.addEventListener('fetch', e => {
    if (/\.png|jpeg|jpg|gif/i.test(e.request.url)) {
        e.respondWith(
            fetch(e.request).then(response => {
                return response;
            }).catch(err => {
                // 请求错误时使用占位图
                return caches.match(placeholderPic).then(cache => cache);
            })
        );
        return;
    }

缓存刷新

事实上,缓存刷新是一个非常需要注意的点。

它可能引起的错误belike:https://github.com/umijs/umi/issues/4803。

terminated终止状态一般触发条件由下面几种方式
  • 关闭浏览器一段时间
  • 手动清除serviceworker
  • 在sw安装时直接跳过waiting阶段

如果service-worker.js内容有更新,当访问网站页面时浏览器获取了新的文件,逐字节比对/sw.js文件不同时他会认为有更新,于是会安装新的文件并触发install文件,但是此时已经处于激活状态的旧的service worker还在运行,新的service worker完成安装后会进入waiting状态。直到所有已打开的页面都关闭,旧的service worker自动更新,新的service worker才会在接下来重新打开的页面里生效。

skipWating

方案一:在installing阶段跳过旧的sw注册,即直接执行新的sw,在activate阶段删除旧的

javascript 复制代码
var version = '0.0.1';
// 跳过等待,直接进入active
this.addEventListener('install', funciton (event) {
    event.waitUntil(self.skipWaiting())
})
this.addEventListener('activate', function (event) {
    event.waitUntil(
        Promise.all([
            // 更新客户端
            self.clients.claim(),
            // 清理旧版本
            caches.keys().then((cacheList) => {
                return Promise.all(
                    cacheList.map((cacheName) => {
                        if (cacheName !== 'my-test-cache-v1') {
                            return caches.delete(cacheName)
                        }
                    })
                )
            })
        ])
    )
})

这个方案在如下场景时可能出现新旧版本的问题:

  1. 一个页面index.html已经安装了old_sw
  2. 用户打开这个页面,所有网络请求都通过了old_sw进行处理,页面加载完成
  3. 因为service worker具有异步安装的特性,一般在浏览器空闲时,他会去执行那句navigator.serviceWorker.register。这时候浏览器发现了有个new_sw,于是安装让他等待
  4. 但是由于new_sw在install阶段有self.skipWaiting(),所以浏览器强制退出了old_sw,让new_sw马上激活并控制页面
  5. 用户如果在index.html后续操作有网络请求,就由new_sw处理
  6. 很明显,同一个页面,前半部分是由old_sw控制,而后半部分由new_sw控制。就可能导致两者行为不一致从而出现未知错误

手动更新 - register时 update

javascript 复制代码
var version = '1.0.1'
navigator.serviceWorker.register('/sw.js')
    .then((reg) => {
        if(localStorage.getItem('sw_version') !== version) {
            reg.update()
                .then(() => {
                    localStorage.setItem('sw_version', version)
                })
        }
    })

自动更新

Service Worker 的特殊之处除了由浏览器触发更新之外,还应用了特殊的缓存策略: 如果该文件已 24 小时没有更新,当Update 触发时会强制更新。这意味着最坏情况下Service Worker会每天更新一次。

相关推荐
材料苦逼不会梦到计算机白富美1 小时前
golang分布式缓存项目 Day 1
分布式·缓存·golang
Java 第一深情1 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
HBryce242 小时前
缓存-基础概念
java·缓存
想要打 Acm 的小周同学呀9 小时前
LRU缓存算法
java·算法·缓存
hlsd#9 小时前
go 集成go-redis 缓存操作
redis·缓存·golang
镰刀出海9 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
奶糖趣多多11 小时前
Redis知识点
数据库·redis·缓存
CoderIsArt12 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
ketil2717 小时前
Redis - String 字符串
数据库·redis·缓存
生命几十年3万天19 小时前
redis时间优化
数据库·redis·缓存