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目录下,代码如下:

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中。

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开起来了。

// /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请求拦截。

/**
 * 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推送事件)
比如:

    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 就可以确定资源已添加到缓存中。

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 没有拦截它一样发送到网络。因此,如果一个请求没有预缓存,它就直接从网络获取。

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 对象是可以流传输的,所以只能读取一次。

拦截图片资源

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

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阶段删除旧的

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

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

  • 30
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在uni-app中使用PWA(Progressive Web App,渐进式网络应用程序)可以提升应用的离线使用能力、推送通知功能以及提供类似原生应用的体验。以下是如何在uni-app项目中使用PWA的步骤: 1. **注册Service Worker**: Service Worker是实现PWA的关键技术之一,它是一个运行在浏览器后台的独立线程,负责缓存资源、提供服务工作线程等。要在uni-app中使用PWA,首先需要在项目中注册Service Worker。 2. **配置manifest.json**: PWA通常需要一个配置文件`manifest.json`来定义应用的名称、图标、主题颜色等元数据,以及启动画面、显示模式等。这个文件对于将网页添加到主屏幕以及应用的安装和启动至关重要。 3. **优化离线体验**: 利用Service Worker缓存机制,可以预先缓存应用的资源文件,包括HTML、CSS、JavaScript以及多媒体资源等,以便在没有网络连接的情况下依然能够加载和使用应用。 4. **添加推送通知功能**: PWA支持推送通知,可以在用户不打开应用的情况下发送消息提醒。这需要使用到Web Push API,并且通常还需要一个服务器来管理推送订阅和消息发送。 5. **测试和调试**: 由于Service Worker必须在HTTPS环境下工作,因此在本地测试时需要通过HTTPS服务器或者使用localhost来绕过这一限制。 6. **发布和部署**: 完成开发和测试后,需要将应用部署到支持HTTPS的服务器上,以便用户可以从网站安装PWA到他们的设备上。 此外,在使用uni-app开发PWA时,可以参考一些已有的案例和教程,这样可以更快地了解整个流程和最佳实践。同时,也要注意测试在不同设备和浏览器上的兼容性,确保应用能够在各种环境下都表现良好。 总之,通过上述步骤,您可以在uni-app项目中集成PWA,从而为用户提供更加丰富和便捷的应用体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值