美文网首页
Service Worker

Service Worker

作者: 欢西西西 | 来源:发表于2024-02-27 17:06 被阅读0次

注意在isSecureContextfalse的环境下,拿不到navigator.serviceWorker

1、可以拦截请求,并允许我们自己设置响应:

respondWith
new Response

// 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、利用它可以拦截响应这个特性,我们可以做什么

  1. 流式下载文件
  2. 监控资源加载错误(或超时),进行错误上报
  3. 配合Cache Storage做离线缓存

2.1 用处一:流式下载文件

  • 步骤:
  1. 创建一个TransformStream
  2. 用文件名创建一个下载链接,向serviceworker发送下载链接和TransformStreamreadablesw中用map存下来
  3. iframe去加载这个下载链接(会被swonfetch拦截,sw拿到url,从自己的map中取出readable流作为响应,此时弹出文件保存框)
  4. TransformStreamwritable返回出去
  5. 外部会获取到一个可写流,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的图片,scopehttp://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实例(下图中名为v1v2),每个Cache实例可以存储多个【Request和Response实例的键值对】,以对网络请求进行缓存

image.png

相关文章

网友评论

      本文标题:Service Worker

      本文链接:https://www.haomeiwen.com/subject/unevadtx.html