Service Worker

注意在isSecureContextfalse的環(huán)境下嗜逻,拿不到navigator.serviceWorker

1敷鸦、可以攔截請求玛界,并允許我們自己設(shè)置響應(yīng):

respondWith
new Response

// serviceworker.js

self.onfetch = event => {
    console.log('攔截到:', event.request.url);

    const myResponseBody = new Blob();
    // myResponseBody返回可以是text照皆、json急鳄、二進(jìn)制等谤民,需要同時(shí)設(shè)置響應(yīng)頭中與之相匹配的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也可以傳一個(gè)Promise實(shí)例來延遲返回

event.respondWith(new Promise(resolve => {
    setTimeout(() => {
        resolve(new Response('ok'))
    }, 1000);
}));

1.1 攔截的范圍 scope

navigator.serviceWorker.register('./sw.js', { scope: './' })

不管這個(gè)sw.js是被哪個(gè)window引入進(jìn)來的,被攔截的范圍跟scope有關(guān)(scope需要在sw.js所在的路徑范圍之內(nèi))

當(dāng)html地址和scope匹配疾宏,則會(huì)被攔截张足,被攔截的html下發(fā)送的fetch、XHR坎藐、link为牍、script、img顺饮、都能攔截

1.2 在devTools中查看和關(guān)閉

image.png

1.3 生命周期

  • 注冊 → 安裝 → 等待 → 激活


    image.png

→ 第一次激活這個(gè)sw時(shí)吵聪,它監(jiān)聽不到本頁面的請求,只有新打開或刷新才可以兼雄。
→ 為了讓它立即接管所有的客戶端吟逝,使用self.clients.claim() ,表示在激活時(shí)立即讓當(dāng)前的sw管之前未被接管的clients
→ activate 事件通常發(fā)生在以下情況:① sw首次安裝后激活 ② sw發(fā)生了更新赦肋,跳過或已完成等待階段

  • 更新:
    當(dāng)sw.js更新時(shí)块攒,如下圖#170安裝后等待(這個(gè)過程會(huì)觸發(fā)170的install)
    但只有等到 #169不再控制任何client,則激活170(觸發(fā)170的activate)
    self.skipWaiting()會(huì)跳過這個(gè)等待階段佃乘,立即激活新的(新sw在激活之前囱井,client仍然受老sw的控制)
image.png
  • 點(diǎn)擊stop


    image.png

2、利用它可以攔截響應(yīng)這個(gè)特性趣避,我們可以做什么

  1. 流式下載文件
  2. 監(jiān)控資源加載錯(cuò)誤(或超時(shí))庞呕,進(jìn)行錯(cuò)誤上報(bào)
  3. 配合Cache Storage做離線緩存

2.1 用處一:流式下載文件

  • 步驟:
  1. 創(chuàng)建一個(gè)TransformStream
  2. 用文件名創(chuàng)建一個(gè)下載鏈接,向serviceworker發(fā)送下載鏈接和TransformStreamreadable程帕,sw中用map存下來
  3. iframe去加載這個(gè)下載鏈接(會(huì)被swonfetch攔截住练,sw拿到url,從自己的map中取出readable流作為響應(yīng)愁拭,此時(shí)彈出文件保存框)
  4. TransformStreamwritable返回出去
  5. 外部會(huì)獲取到一個(gè)可寫流讲逛,fetch url,并把結(jié)果寫入這個(gè)可寫流(當(dāng)這個(gè)可寫流沒有close時(shí)岭埠,這個(gè)以可讀流為響應(yīng)的請求會(huì)一直處于pending狀態(tài))
var writableStream = await getWritableStream('wxm.jpg'); // 獲取到一個(gè)可寫流
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('下載結(jié)束');
            writer.close();
            return;
        }
        loadedSize += result.value.length;
        console.log('下載進(jìn)度:', parseInt(loadedSize / totalSize * 100));

        // 把fetch的結(jié)果不斷寫入這個(gè)可寫流中
        writer.write(result.value).catch(error => {
            writer.close();
        });
        return reader.read().then(handleResult);
    });
});

  • 要下載的圖片路徑的拼接:
    雖然xhr蔚鸥、fetch等請求都能被service worker攔截到,但是只有iframe许赃、location止喷、window.open這種方式發(fā)出的請求,才能彈出文件保存彈窗图焰。而后者只有與scope相匹配時(shí)才能被攔截到启盛。
    所以組裝下載鏈接時(shí),要加scope前綴技羔。例如:需要下載一個(gè)文件名為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); // 組裝一個(gè)下載鏈接
    const readableStream = ts.readable
    sw.postMessage({ downloadUrl, fileName, readableStream }, [readableStream]); // 讓sw內(nèi)部先把這個(gè)readableStream存下來
    makeIframe(downloadUrl); // 用iframe加載這個(gè)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() // 安裝完成后并不進(jìn)入等待階段社裆,而是立即激活新的service worker
})

self.addEventListener('activate', event => {
    event.waitUntil(self.clients.claim())
    // event.waitUntil 表示延緩activate事件的完成拙绊,直到其中的Promise被解決
    // `self.clients.claim()` 表示在激活時(shí)立即讓當(dāng)前的Service Worker 接管之前未被接管的clients
    // 這樣做可以確保新的Service Worker 立即控制所有客戶端,而不需要等到下一次加載頁面時(shí)才能生效
})

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 用處二:監(jiān)控資源加載錯(cuò)誤(或超時(shí))泳秀,進(jìn)行錯(cuò)誤上報(bào)

const MAX = 1000; // 不能超過1s
const onFetch = event => {
    const url = event.request.url
    event.ajaxStart = Date.now(); // 記錄請求開始
    event.timeoutTimer = setTimeout(() => { // 設(shè)置一個(gè)定時(shí)器來報(bào)告請求超時(shí)
        console.log('請求超時(shí)了:', url);
    }, MAX);
    event.respondWith(fetch(event.request).then(response => {
        // 如果沒超時(shí)标沪,就清除定時(shí)器
        if (Date.now() - event.ajaxStart <= MAX) {
            clearTimeout(event.timeoutTimer);
        }
        return response;
    }));
}

2.3 用處三:window.caches

將一些資源請求的response存到caches中,可以作為請求失敗的兜底嗜傅。

從緩存中取出response并作為響應(yīng) cache Storage中沒有緩存金句,從services worker中fetch 到服務(wù)端的請求
-
image.png
-
image.png
服務(wù)端.png

caches里面可以包含多個(gè)Cache實(shí)例(下圖中名為v1v2),每個(gè)Cache實(shí)例可以存儲多個(gè)【Request和Response實(shí)例的鍵值對】吕嘀,以對網(wǎng)絡(luò)請求進(jìn)行緩存

image.png

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末违寞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子偶房,更是在濱河造成了極大的恐慌趁曼,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棕洋,死亡現(xiàn)場離奇詭異挡闰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)掰盘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門摄悯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人庆杜,你說我怎么就攤上這事射众。” “怎么了晃财?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵叨橱,是天一觀的道長典蜕。 經(jīng)常有香客問我,道長罗洗,這世上最難降的妖魔是什么愉舔? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮伙菜,結(jié)果婚禮上轩缤,老公的妹妹穿的比我還像新娘。我一直安慰自己贩绕,他們只是感情好火的,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著淑倾,像睡著了一般馏鹤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娇哆,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天湃累,我揣著相機(jī)與錄音,去河邊找鬼碍讨。 笑死治力,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勃黍。 我是一名探鬼主播宵统,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼溉躲!你這毒婦竟也來了榜田?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤锻梳,失蹤者是張志新(化名)和其女友劉穎箭券,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疑枯,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辩块,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了荆永。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片废亭。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖具钥,靈堂內(nèi)的尸體忽然破棺而出豆村,到底是詐尸還是另有隱情,我是刑警寧澤骂删,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布掌动,位于F島的核電站四啰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏粗恢。R本人自食惡果不足惜柑晒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望眷射。 院中可真熱鬧匙赞,春花似錦、人聲如沸妖碉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽欧宜。三九已至脾猛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鱼鸠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工羹铅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚀狰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓职员,卻偏偏與公主長得像麻蹋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子焊切,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容