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会每天更新一次。

相关推荐
Hellyc2 小时前
用户查询优惠券之缓存击穿
java·redis·缓存
鼠鼠我捏,要死了捏4 小时前
缓存穿透与击穿多方案对比与实践指南
redis·缓存·实践指南
汤姆大聪明11 小时前
Redis 持久化机制
数据库·redis·缓存
kk在加油13 小时前
Redis数据安全性分析
数据库·redis·缓存
hcvinh1 天前
CANDENCE 17.4 进行元器件缓存更新
学习·缓存
墨着染霜华1 天前
Caffeine的tokenCache与Spring的CaffeineCacheManager缓存区别
java·spring·缓存
weixin_438335401 天前
Redis:分组与设备在 Redis 中缓存存储设计
redis·缓存·bootstrap
秋也凉2 天前
redis的命令集合
数据库·redis·缓存
R-sz2 天前
java内存缓存实现 与 redis缓存实现 (ConcurrentHashMap 应用)
java·redis·缓存
好心的小明2 天前
【王树森推荐系统】召回11:地理位置召回、作者召回、缓存召回
人工智能·缓存·推荐系统·推荐算法