相比起(一)做一些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));
}
});
上方代码中:
- caches.match() 是一个语法糖。等效于在每个缓存上调用 cache.match()(按照 caches.keys() 的顺序)直到返回 Response。
- 当我们将 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)
}
})
)
})
])
)
})
这个方案在如下场景时可能出现新旧版本的问题:
- 一个页面index.html已经安装了old_sw
- 用户打开这个页面,所有网络请求都通过了old_sw进行处理,页面加载完成
- 因为service worker具有异步安装的特性,一般在浏览器空闲时,他会去执行那句navigator.serviceWorker.register。这时候浏览器发现了有个new_sw,于是安装让他等待
- 但是由于new_sw在install阶段有self.skipWaiting(),所以浏览器强制退出了old_sw,让new_sw马上激活并控制页面
- 用户如果在index.html后续操作有网络请求,就由new_sw处理
- 很明显,同一个页面,前半部分是由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会每天更新一次。