注意在isSecureContext为false的环境下,拿不到navigator.serviceWorker
1、可以拦截请求,并允许我们自己设置响应:
// serviceworker.js
self.onfetch = event => {
console.log('拦截到:', event.request.url);
const myResponseBody = new Blob();
// myResponseBody返回可以是text、json、二进制等,需要同时设置响应头中与之相匹配的content-type
const myResponse = new Response(myResponseBody,
{
headers: new Headers({
"content-type": "text/plain; charset=UTF-8",
"my-header": "wxm"
}),
status: 200,
statusText: 'YES!'
})
event.respondWith(myResponse);
};
image.png
respondWith也可以传一个Promise实例来延迟返回
event.respondWith(new Promise(resolve => {
setTimeout(() => {
resolve(new Response('ok'))
}, 1000);
}));
1.1 拦截的范围 scope
navigator.serviceWorker.register('./sw.js', { scope: './' })
不管这个sw.js是被哪个window引入进来的,被拦截的范围跟scope有关(scope需要在sw.js所在的路径范围之内)
当html地址和scope匹配,则会被拦截,被拦截的html下发送的fetch、XHR、link、script、img、都能拦截
1.2 在devTools中查看和关闭
image.png
1.3 生命周期
-
注册 → 安装 → 等待 → 激活
image.png
→ 第一次激活这个sw时,它监听不到本页面的请求,只有新打开或刷新才可以。
→ 为了让它立即接管所有的客户端,使用self.clients.claim() ,表示在激活时立即让当前的sw管之前未被接管的clients
→ activate 事件通常发生在以下情况:① sw首次安装后激活 ② sw发生了更新,跳过或已完成等待阶段
- 更新:
当sw.js更新时,如下图#170安装后等待(这个过程会触发170的install)
但只有等到#169不再控制任何client,则激活170(触发170的activate)
而self.skipWaiting()会跳过这个等待阶段,立即激活新的(新sw在激活之前,client仍然受老sw的控制)
image.png
-
点击stop
image.png
2、利用它可以拦截响应这个特性,我们可以做什么
- 流式下载文件
- 监控资源加载错误(或超时),进行错误上报
- 配合Cache Storage做离线缓存
2.1 用处一:流式下载文件
- 步骤:
- 创建一个
TransformStream - 用文件名创建一个下载链接,向
serviceworker发送下载链接和TransformStream的readable,sw中用map存下来 - 用
iframe去加载这个下载链接(会被sw的onfetch拦截,sw拿到url,从自己的map中取出readable流作为响应,此时弹出文件保存框) - 把
TransformStream的writable返回出去 - 外部会获取到一个可写流,
fetch url,并把结果写入这个可写流(当这个可写流没有close时,这个以可读流为响应的请求会一直处于pending状态)
var writableStream = await getWritableStream('wxm.jpg'); // 获取到一个可写流
var writer = writableStream.getWriter();
fetch('/mrp/common/images/big.jpg').then(res => {
const totalSize = parseInt(res.headers.get('content-length'));
console.log('资源大小:', totalSize);
let loadedSize = 0;
const reader = res.body.getReader();
reader.read().then(function handleResult(result) {
// console.log({ done: result.done });
if (result.done) {
console.log('下载结束');
writer.close();
return;
}
loadedSize += result.value.length;
console.log('下载进度:', parseInt(loadedSize / totalSize * 100));
// 把fetch的结果不断写入这个可写流中
writer.write(result.value).catch(error => {
writer.close();
});
return reader.read().then(handleResult);
});
});
-
要下载的图片路径的拼接:
虽然xhr、fetch等请求都能被service worker拦截到,但是只有iframe、location、window.open这种方式发出的请求,才能弹出文件保存弹窗。而后者只有与scope相匹配时才能被拦截到。
所以组装下载链接时,要加scope前缀。例如:需要下载一个文件名为wxm.jpg的图片,scope为http://localhost:4000/mrp/baseInfoV2/SPU/,则拼接后的地址为:http://localhost:4000/mrp/baseInfoV2/SPU/wxm.jpg
function getDownloadUrl(scope, fileName) {
return scope + '/' + fileName
}
- 代码:
async function getWritableStream(fileName) {
fileName = encodeURIComponent(fileName);
let [sw, scope] = await registerSw(); // 先注册sw,无则注册,有则返回
const ts = new TransformStream();
const downloadUrl = getDownloadUrl(scope, fileName); // 组装一个下载链接
const readableStream = ts.readable
sw.postMessage({ downloadUrl, fileName, readableStream }, [readableStream]); // 让sw内部先把这个readableStream存下来
makeIframe(downloadUrl); // 用iframe加载这个downloadUrl
return ts.writable;
}
function registerSw() {
return navigator.serviceWorker.getRegistration('/mrp/baseInfoV2/SPU').then(swReg => {
// scope必须是在sw.js的路径以下
return swReg || navigator.serviceWorker.register('/mrp/sw.js', {
scope: '/mrp/baseInfoV2/SPU'
})
}
).then(swReg => {
let scope = swReg.scope;
const swRegTmp = swReg.installing || swReg.waiting
return swReg.active ? [swReg.active, scope] : new Promise(resolve => {
swRegTmp.addEventListener('statechange', fn = () => {
if (swRegTmp.state === 'activated') {
swRegTmp.removeEventListener('statechange', fn)
resolve([swReg.active, scope]);
}
})
})
})
}
// 在sw.js中拦截
self.addEventListener('install', () => {
self.skipWaiting() // 安装完成后并不进入等待阶段,而是立即激活新的service worker
})
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim())
// event.waitUntil 表示延缓activate事件的完成,直到其中的Promise被解决
// `self.clients.claim()` 表示在激活时立即让当前的Service Worker 接管之前未被接管的clients
// 这样做可以确保新的Service Worker 立即控制所有客户端,而不需要等到下一次加载页面时才能生效
})
const map = new Map();
self.onmessage = event => {
map.set(event.data.downloadUrl, {
fileName: event.data.fileName,
readableStream: event.data.readableStream
});
}
self.onfetch = event => {
const url = event.request.url
const saved = map.get(url)
if (!saved) return null
map.delete(url)
let { fileName, readableStream } = saved;
const responseHeaders = new Headers({
'Content-Type': 'application/octet-stream; charset=utf-8',
'Content-Disposition': "attachment; filename*=UTF-8''" + fileName,
// To be on the safe side, The link can be opened in a iframe.
// but octet-stream should stop it.
'Content-Security-Policy': "default-src 'none'",
'X-Content-Security-Policy': "default-src 'none'",
'X-WebKit-CSP': "default-src 'none'",
'X-XSS-Protection': '1; mode=block',
'Cross-Origin-Embedder-Policy': 'require-corp'
})
event.respondWith(new Response(readableStream, { headers: responseHeaders }))
};
2.2 用处二:监控资源加载错误(或超时),进行错误上报
const MAX = 1000; // 不能超过1s
const onFetch = event => {
const url = event.request.url
event.ajaxStart = Date.now(); // 记录请求开始
event.timeoutTimer = setTimeout(() => { // 设置一个定时器来报告请求超时
console.log('请求超时了:', url);
}, MAX);
event.respondWith(fetch(event.request).then(response => {
// 如果没超时,就清除定时器
if (Date.now() - event.ajaxStart <= MAX) {
clearTimeout(event.timeoutTimer);
}
return response;
}));
}
2.3 用处三:window.caches
将一些资源请求的response存到caches中,可以作为请求失败的兜底。
从缓存中取出response并作为响应 |
cache Storage中没有缓存,从services worker中fetch | 到服务端的请求 |
|---|---|---|
-
image.png
|
-
image.png
|
服务端.png
|
caches里面可以包含多个Cache实例(下图中名为v1和v2),每个Cache实例可以存储多个【Request和Response实例的键值对】,以对网络请求进行缓存
image.png

image.png
image.png
服务端.png











网友评论