也不徹底是筆記,也作了一些本身的補充
javascript是單線程的,可是javascript能夠把工做嫁接給獨立的線程。同時不影響單線程模型(不能操做DOM)。javascript
每打開一個網頁就至關於一個沙盒,每個頁面都有本身獨立的內容。工做者線程至關於一個徹底獨立的二級子環境。在子環境中不能與依賴單線程模型API交互(DOM操做),可是能夠與父環境並行執行代碼。css
在工做者線程中,沒有window對象,全局對象是WorkerGlobalScope的子類的實例html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> // 在本地調試,須要使用絕對路徑 const worker = new Worker('./worker.js') console.log(worker) </script> </body> </html>
工做者線程的腳本文件,只能和父級同源。(可是在工做者線程中,可使用importScripts加載其餘源的腳本)java
建立的Worker對象在工做線程終止前,是不會被垃圾回收機制回收的
工做者線程中全局做用域是DedicatedWorkerGlobalScope對象的實例,能夠經過self關鍵字訪問全局對象git
生命週期分爲初始化,活動,終止。但父上下文是沒法區分工做者線程的狀態。調用Worker後,雖然worker對象可用,可是worker不必定初始化完畢。可能存在延遲。若是不調用close或者terminate,工做者線程會一直存在,垃圾回收機制也不會回收worker對象。可是調用close和terminate是有一些區別的。若是工做者線程關聯的網頁被關閉,工做者線程也會被終止。github
// 專用工做者線程 self.postMessage('a') self.close() self.postMessage('b') // 父上下文 const worker = new Worker('./worker.js') worker.onmessage = ({ data }) => { console.log('data:', data); } // consloe // data: a // data: b
// 工做者線程 self.onmessage = ({data}) => console.log(data); // 父上下文 const worker = new Worker(location.href + '/worker.js') // 定時器等待線程初始化完成 setTimeout(() => { worker.postMessage('a') worker.terminate() worker.postMessage('b') }, 1000); // consloe // a
專用工做者線程能夠經過Blob對象的URL在行內建立,而不須要遠程的js文件。web
const workerStr = ` self.onmessage = ({data}) => { console.log('data:', data); } `; const workerBlob = new Blob([workerStr]); const workerBlobUrl = URL.createObjectURL(workerBlob); const worker = new Worker(workerBlobUrl); // data: abc worker.postMessage('abc');
父上下文的函數,也能夠傳遞給專用工做者線程,而且在專用工做者線程中執行。可是父上下文的函數中不能使用閉包的變量,以及全局對象。算法
const fn = () => '父上下文的函數'; // 將fn轉爲字符串的形式,而後自執行 const workerStr = ` self.postMessage( (${fn.toString()})() ) ` const workerBlob = new Blob([workerStr]); const workerBlobUrl = URL.createObjectURL(workerBlob); const worker = new Worker(workerBlobUrl); worker.onmessage = ({ data }) => { // 父上下文的函數 console.log(data) }
const a = 'Hi' // error, Uncaught ReferenceError: a is not defined const fn = () => `${a}, 父上下文的函數`; const workerStr = ` self.postMessage( (${fn.toString()})() ) ` const workerBlob = new Blob([workerStr]); const workerBlobUrl = URL.createObjectURL(workerBlob); const worker = new Worker(workerBlobUrl); worker.onmessage = ({ data }) => { // 父上下文的函數 console.log(data) }
工做者線程中,可使用importScripts加載執行腳本。importScripts加載的js會按照順序執行。全部導入的腳本會共享做用域,importScripts不會同源的限制。api
我通過測試,在父上下文中使用 onerror 監聽錯誤,是能夠捕獲到importScripts加載的非同源腳本的錯誤,而且有具體的錯誤信息。數組
// 父上下文 const worker = new Worker('http://127.0.0.1:8080/worker.js') window.onerror = (error) => { // Uncaught ReferenceError: a is not defined console.log(error) } // 工做者線程 importScripts('http://127.0.0.1:8081/worker.js') // 工做者線程中importScripts加載的腳本 const fn = () => { console.log(a) } setTimeout(() => fn(), 3000);
工做線程還能夠繼續建立工做者線程。可是多個工做者線程會帶來額外的開銷。而且頂級工做者線程,和子工做者線程,必須和父上下文在同一個源中。
try……catch, 沒法捕獲到線程中的錯誤,可是在父上下文中,可使用onerror事件捕獲到
以前已經在demo中給出例子,這裏再也不贅述
MessageChannel API有兩個端口,若是父上下文須要實現與工做線程的通信, 父上下文須要將端口傳到工做者線程中
// 父上下文 const channel = new MessageChannel() const worker = new Worker('http://127.0.0.1:8080/worker.js') // 將端口2發送給工做者線程中 worker.postMessage(null, [channel.port2]); setTimeout(() => { // 經過MessageChannel發送消息 channel.port1.postMessage('我是父上下文') }, 2000)
// 工做線程 let channelPort = null self.onmessage = ({ ports }) => { if (!channelPort) { channelPort = ports[0] self.onmessage = null // 經過channelPort監聽消息 channelPort.onmessage = ({ data }) => { console.log('父上下文的數據:', data); } } }
同源腳本可使用BroadcastChannel進行通信,使用BroadcastChannel必須注意的是,若是父上下文在工做線程初始化完成以前,就發送消息,工做線程初始化完成後,是接受不到消息的。消息不會存在消息隊列中。
// 父上下文 const channel = new BroadcastChannel('worker') const worker = new Worker('http://127.0.0.1:8080/worker.js') // 等待工做線程初始化完畢 setTimeout(() => { channel.postMessage('消息') }, 2000)
// 工做線程 const channel = new BroadcastChannel('worker') channel.onmessage = ({ data }) => { console.log(data) }
Channel Messaging API 能夠用在 "文檔主體與iframe","兩個iframe之間","使用SharedWorker的兩個文檔",或者兩個"worker"之間進行通許。
使用postMessage發送數據的時候,瀏覽器後臺會對數據(除了Symbol以外的類型)進行拷貝。雖然結構化克隆算法對循環引用的問題作了兼容處理,可是對於複雜對象結構化克隆算法有性能損耗。
將數據的全部權。由父級上下文轉讓給工做線程。或者由工做線程轉讓給父級上下文。轉移後,數據就會在以前的上下文中被抹去。postMessage的第二個參數,是可選參數,是一個數組,數組的數據須要被轉讓全部權的數據。
// 父上下文 const worker = new Worker('http://127.0.0.1:8080/worker.js') const buffer = new ArrayBuffer(30) // 30 console.log('發送以前:', buffer.byteLength) // 等待工做線程初始化完畢 setTimeout(() => { worker.postMessage(buffer, [buffer]) // 0 console.log('發送以後:', buffer.byteLength) }, 2000)
// 工做線程 self.onmessage = ({ data }) => { // 30 console.log('工做線程接受以後', data.byteLength); }
以前可使用 worker.postMessage(null, [channel.port2]) 發送channel接口的時候。工做線程的onmessage事件的參數,會接收ports,可是換成其餘數據是接收不到的。postMessage應該是對channel的數據作了特殊的處理。
SharedArrayBuffer能夠在父上下文和工做線程中共享,SharedArrayBuffer和ArrayBuffer的api相同,不能直接被操做須要視圖。
// 父上下文 const worker = new Worker('http://127.0.0.1:8080/worker.js') const sharedBuffer = new SharedArrayBuffer(10) const view = new Int8Array(sharedBuffer); view[0] = 1; // 1 console.log('發送以前:', view[0]); worker.postMessage(sharedBuffer) setTimeout(() => { // 打印出,2 console.log('發送以後:', view[0]) }, 2000)
// 工做者線程 self.onmessage = ({ data }) => { const view = new Int8Array(data); view[0] = '2' }
並行線程共享資源,會有資源徵用的隱患。可使用Atomics解決,Atomics與SharedArrayBuffer能夠查看第二十章的筆記。
開啓新的工做者線程開銷很大,可開啓保持固定數量的線程。線程在忙碌時不接受新任務,線程空閒後接收新任務。這些長期開啓的線程,被稱爲線程池。
線程池中線程的數量,能夠參考電腦cpu的線程數, navigator.hardwareConcurrency, 將cpu的線程數設置線程池的上限。
下面的是數中的封裝,我在github中也沒有找到太熱門的庫封裝的線程池可用,https://github.com/andywer/th... 已是5年前更新的了。
注意:
共享工做者線程和建立,安全限制和專用工做者線程都是相同的,共享工做者線程,能夠看做是專用工做者線程的擴展。
SharedWorker能夠被多個同源的上下文(同源的網頁標籤)訪問。SharedWorker的消息接口和專用工做者線程也略有不一樣。
SharedWorker,沒辦法使用行內的worker, 由於經過URL.createObjectURL, 是瀏覽器內部的URL, 沒法在其餘標籤頁使用。
worker每次new都會返回一個新的worker實例,SharedWorker只會在不存在相同標示的狀況下返回新的實例。SharedWorker的標示能夠是worker文件的路徑, 文檔源。
// 只會實例化一個共享工做者線程 new SharedWorker('http://127.0.0.1:8080/worker.js') new SharedWorker('http://127.0.0.1:8080/worker.js')
可是若是咱們給相同源的SharedWorker,不一樣的標識,瀏覽器會任務它們是不一樣的共享工做者線程
// 實例化二個共享工做者線程 new SharedWorker('http://127.0.0.1:8080/worker.js', { name: 'a' }) new SharedWorker('http://127.0.0.1:8080/worker.js', { name: 'a' })
在不一樣頁面,只要標示相同,建立的SharedWorker都是同一個連接
SharedWorker對象的屬性
共享工做者線程中的全局對象是SharedWorkerGlobalScope的實例,全局實例上的屬性和方法
專用工做者線程只和一個頁面綁定,而共享工做者線程只要還有一個上下文連接,它就不會被回收。共享工做者對象沒法經過terminate關閉,由於共享工做者線程沒有terminate方法,瀏覽器會負責管理共享工做者線程的連接。
發生connect事件時,SharedWorker構造函數會隱式的建立MessageChannel,並把其中一個port轉移給共享工做者線程的ports數組中。
? 可是共享線程與父上下文的啓動關閉不是對稱的。每次打開會創建連接,connect事件中的ports數組中port數量會加1,可是頁面被關閉,SharedWorker沒法感知。
好比不少頁面連接了SharedWorker,如今一部分如今關閉了,SharedWorker並不知道那些頁面關閉,因此ports數組中,存在被關閉的頁面的port,這些死端口會污染ports數組。
書中給出的方法是,能夠在頁面銷燬以前的beforeunload事件中,通知SharedWorker清除死端口。
// 父頁面 const worker = new SharedWorker('http://127.0.0.1:8080/worker.js') worker.port.onmessage = ({ data }) => { // 打印 data: 2 console.log('data:', data); } worker.port.postMessage([1, 1]);
// 共享工做者線程 const connectedPorts = new Set(); self.onconnect = ({ports}) => { if (!connectedPorts.has(ports[0])) { connectedPorts.add(ports[0]) ports[0].onmessage = ({ data }) => { ports[0].postMessage(data[0] + data[1]) } } };
分享線程生成id,標示接口,併發送給頁面。在頁面beforeunload,將id發送給分享工做者線程中,分享工做者線程清除死端口。
// 父頁面1 const worker = new SharedWorker('http://127.0.0.1:8080/worker.js') let portId = null worker.port.onmessage = ({ data }) => { if (typeof data === 'string' && data.indexOf('uid:') > -1) { // 記錄接口的id portId = data.split(':')[1]; } else { console.log('接口的數量:', data); } } window.addEventListener('beforeunload', (event) => { worker.port.postMessage(`刪除:${portId}`); });
// 父頁面2 const worker = new SharedWorker('http://127.0.0.1:8080/worker.js') let portId = null worker.port.onmessage = ({ data }) => { if (typeof data === 'string' && data.indexOf('uid:') > -1) { // 記錄接口的id portId = data.split(':')[1]; } else { console.log('接口的數量:', data); } } window.addEventListener('beforeunload', (event) => { worker.port.postMessage(`刪除:${portId}`); });
// 分享工做者線程 const uuid = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // 記錄接口的map const connectedPortsMap = new Map(); self.onconnect = ({ports}) => { if (!connectedPortsMap.has(ports[0])) { const uid = uuid(); connectedPortsMap.set(uid, ports[0]) // 向頁面發送接口的id,這個id用於刪除接口 ports[0].postMessage(`uid:${uid}`); ports[0].onmessage = ({ data }) => { if (typeof data === 'string' && data.indexOf('刪除:') > -1) { const portId = data.split(':')[1]; // 刪除死接口 connectedPortsMap.delete(portId); } } } }; setInterval(() => { // 發送接口的數量 connectedPortsMap.forEach((value) => { value.postMessage(connectedPortsMap.size) }) }, 3000)
? 感受這個章節翻譯的有點差,不少話讀的很彆扭,不流暢。並且不少章節都沒有給出示例代碼,我不少章節都手敲了一遍例子代碼,放在文章李
是瀏覽器中的代理服務器線程,能夠攔截請求或者緩存響應,頁面能夠在無網的環境下使用。與共享工做者線程相似,多個頁面共享一個服務工做者線程。服務工做者線程中,服務工做者線程可使用Notifications API、Push API、Background Sync API。爲了使用Push API服務工做者線程能夠在瀏覽器或者標籤頁關閉後,繼續等待推送的事件。
服務工做者線程,經常使用於網絡請求的緩存層和啓用推送通知。服務工做者線程,能夠把Web應用的體驗變爲原生應用程序同樣。
瀏覽器用於顯示桌面通知的API, 下面是例子
// 檢查是否容許發送通知 // 若是已經容許直接發送通知 if (Notification.permission === "granted") { let notification = new Notification('西爾莎羅南', { body: '西爾莎羅南?' }); } else if (Notification.permission !== "denied") { // 若是尚未容許發送通知,咱們請求用戶容許 Notification.requestPermission().then(function (permission) { // 若是用戶接授權限,咱們就能夠發起一條消息 if (permission === "granted") { let notification = new Notification('西爾莎羅南', { body: '西爾莎羅南?' }); } }) }
Push API實現了Web接受服務器推送消息的能力。Push API具體的實施代碼,能夠看個人這個例子, 實現了一個簡單的推送。
過程,客戶端生成訂閱信息,發送給服務端保存。服務端端能夠根據須要,在合適的時候,使用訂閱信息向客戶端發送推送。
https://github.com/peoplesing...
服務工做者線程,用於按期更新數據的API。
? 原本想實驗以一下這個API,可是註冊定時任務時,提示「DOMException: Permission denied.」錯誤,暫時沒有解決。
服務工做者線程沒有全局的構造函數,經過 navigator.serviceWorker 建立,銷燬,服務工做者線程
與共享工做者線程同樣,在沒有時建立新的連接,若是線程已存在,連接到已存在的線程上。
// 建立服務工做者線程 navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js');
registerf返回一個Promise對象,在同一頁面首次調用register後,後續調用register沒有任何返回。
navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(() => { console.info('註冊成功') }).catch(() => { console.error('註冊失敗') })
若是服務工做者線程用於管理緩存,服務工做線程應該在頁面中提早註冊。不然,服務工做者線程應該在load事件中完成註冊。
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(() => { console.info('註冊成功') }).catch(() => { console.error('註冊失敗') }) }); }
ServiceWorkerContainer對象是瀏覽器對服務工做者線程的頂部封裝,ServiceWorkerContainer能夠在客戶端中經過navigator.serviceWorker訪問
const btn = document.getElementById('btn') btn.onclick = () => { navigator.serviceWorker.register('./sw2.js') } navigator.serviceWorker.oncontrollerchange = () => { console.log('觸發controllerchange事件') // sw2 console.log(navigator.serviceWorker.controller) } navigator.serviceWorker.register('./worker.js')
// sw1 console.log('hehe')
// sw2 self.addEventListener('install', async () => { // 強制進入已激活的狀態 self.skipWaiting(); }) self.addEventListener('activate', async () => { // 強制接管客戶端 self.clients.claim(); })
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { let sw1; let sw2; navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(sw => { console.log(sw); sw1 = sw; }) navigator.serviceWorker.ready.then((sw) => { console.log(sw); sw2 = sw; }) setTimeout(() => { // true console.log(sw1 === sw2) }, 1000) }); }
ServiceWorkerRegistration,表示成功註冊的服務工做者線程。能夠經過register返回的Promise中訪問到。在同一頁面調用register,若是URL相同,返回的都是同一個ServiceWorkerRegistration對象。
navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(sw => { // ServiceWorkerRegistration對象 console.log(sw); })
如何獲取ServiceWorker對象?有兩種如下的途徑
ServiceWorker對象繼承Work,可是不包含terminate方法
若是在非HTTPS的協議下,navigator.serviceWorker是undefined。window.isSecureContext能夠判斷當前上下文是否安全。
服務工做者線程內部,全局對象是ServiceWorkerGlobalScope的實例。ServiceWorkerGlobalScope繼承WorkerGlobalScope,所以擁有它的屬性和方法。線程內部經過self訪問全局的上下文。ServiceWorkerGlobalScope的實例的作了如下的擴展。
專用工做者線程,和共享工做者線程只有一個onmessage事件做爲輸入,但服務工做者線程能夠接受多種事件
// worker.js在根目錄線下 navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js') // http://127.0.0.1:8080下的全部請求都會被攔截 fetch('http://127.0.0.1:8080/foo.js'); fetch('http://127.0.0.1:8080/foo/fooScript.js'); fetch('http://127.0.0.1:8080/baz/bazScript.js'); // worker.js在foo目錄下 navigator.serviceWorker.register('http://127.0.0.1:8080/foo/worker.js'}) // foo目錄下的請求會被攔截 fetch('/foo/fooScript.js') // 其餘路徑的請求不會被攔截 fetch('/foo.js') fetch('/baz/bazScript.js')
若是想排除某個路徑下的請求,可使用末尾帶斜槓的路徑
// foo路徑下的請求,都不會被攔截 navigator.serviceWorker.register( 'http://127.0.0.1:8080/worker.js', { scope: '/foo/' } )
經過 self.caches 訪問 CacheStorage 對象。CacheStorage時字符串和Cache對象的映射。CacheStorage在頁面或者其餘工做者線程中,均可以訪問使用。
// 訪問緩存,若是沒有緩存則會建立 self.caches.open(key)
CacheStorage也擁有相似Map的API,好比has,delete,keys(),可是它們都是返回Promise的
分別返回匹配的第一個Response
(async () => { const request = new Request('https://www.foo.com') const response1 = new Response('fooResponse1') const response2 = new Response('fooResponse2') const v1 = await caches.open('v1') await v1.put(request, response1) const v2 = await caches.open('v2') await v2.put(request, response2) const matchCatch = await caches.match(request) const matchCatchText = await matchCatch.text() // true console.log(matchCatchText === 'fooResponse1') })();
CacheStorage對象是字符串和Cache對象的映射。Cache對象則是Request對象或者URL字符串,和Response對象之間的映射。
Cache也擁有delete(), keys()等方法,這些方法都是返回Promise的
(async () => { const request1 = new Request('https://www.foo.com'); const response1 = new Response('fooResponse'); const cache = await caches.open('v1') await cache.put(request1, response1) const keys = await cache.keys() // [Request] console.log(keys) })()
(async () => { const request1 = new Request('https://www.foo.com?a=1&b=2') const request2 = new Request('https://www.bar.com?a=1&b=2', { method: 'GET' }) const response1 = new Response('fooResponse') const response2 = new Response('barResponse') const v3 = await caches.open('v3') await v3.put(request1, response1) await v3.put(request2, response2) const matchResponse = await v3.match(new Request('https://www.foo.com'), { ignoreMethod: true, // 忽略匹配GET或者POST方法 ignoreSearch: true, // 忽略匹配查詢字符串 }); const matchResponseText = await matchResponse.text() // fooResponse console.log(matchResponseText) })();
catch對象的key,value使用的是Request, Response對象的clone方法建立的副本
(async () => { const request = new Request('https://www.foo.com'); const response = new Response('fooResponse'); const cache = await caches.open('v1') await cache.put(request, response) const keys = await cache.keys() // false console.log(keys[0] === request) })();
獲取存儲空間,以及目前以用的空間
navigator.storage.estimate()
一開始註冊服務工做者時,頁面將在下一次加載以前才使用它。有兩種方法能夠提早控制頁面
// 頁面 navigator.serviceWorker.register('./worker.js').then((registration) => { setTimeout(() => { fetch('/aa') }, 2000) }).catch(() => { console.log('註冊失敗') });
// sw self.addEventListener('fetch', () => { // sw沒有控制客戶端,因此沒法攔截fetch請求,拋出錯誤 throw new Error('呵呵') })
第一種解決方法, 使用claim強制得到控制權,可是可能會形成版本資源不一致
self.addEventListener('activate', async () => { self.clients.claim(); }) self.addEventListener('fetch', () => { // 能夠拋出錯誤 throw new Error('呵呵') })
第二種解決方法,刷新頁面
navigator.serviceWorker.register('./worker.js').then((registration) => { setTimeout(() => { fetch('/aa') }, 3000) registration.addEventListener('updatefound', () => { const sw = registration.installing; sw.onstatechange = () => { console.log('sw.state', sw.state) if (sw.state === 'activated') { console.log('刷新頁面') // 刷新頁面後能夠拋出錯誤 window.location.reload(); } } }) }).catch(() => { console.log('註冊失敗') });
服務工做者線程,最重要的就是保持一致性(不會存在a頁面使用v1版本的服務工做者線程,b頁面使用v2版本的服務工做者線程)。
數據一致性,
調用 navigator.serviceWorker.register() 會進入已解析的狀態,可是該狀態沒有事件,也沒有對應的ServiceWorker.state的值。
在客戶端能夠經過檢查registration.installing是否被設置爲了ServiceWorker實例,判斷是否在安裝中的狀態。當服務工做者線程到達安裝中的狀態時,會觸發onupdatefound事件。
navigator.serviceWorker.register('./sw1.js').then((registration) => { registration.onupdatefound = () => { console.log('我已經達到了installing安裝中的狀態') } console.log(registration.installing) });
在服務工做者線程的內部,能夠經過監聽install事件,肯定安裝中的狀態。
在install事件中,能夠用來填充緩存,可使用waitUntil的方法,waitUntil方法接受一個Promise,只有Promise返回resolve時,服務工做者線程的狀態纔會向下一個狀態過渡。
self.addEventListener('install', (event) => { event.waitUntil(async () => { const v1 = await caches.open('v1') // 緩存資源完成後,才過渡到下一個狀態 v1.addAll([ 'xxxx.js', 'xxx.css' ]) }) })
在客戶端能夠經過檢查registration.waiting是否被設置爲了ServiceWorker實例,判斷是不是已安裝的狀態。若是瀏覽器中沒有以前版本的的ServiceWorker,新安裝的ServiceWorker會直接跳過這個狀態,進入激活中的狀態。不然將會等待。
navigator.serviceWorker.register('./worker.js').then((registration) => { console.log(registration.waiting) });
若是有已安裝的ServiceWorker,可使用self.skipWaiting,強制工做者線程進入活動的狀態
若是瀏覽器中沒有以前版本的ServiceWorker,則新的服務工做者線程會直接進入這個狀態。若是有其餘服務者線程,能夠經過下面的方法,使新的服務者線程進入激活中的狀態
const btn = document.getElementById('btn'); navigator.serviceWorker.register('./sw1.js').then((registration) => { // 第一次加載沒有活動的(以前版本)服務工做者進程, waiting直接跳過因此爲null console.log('waiting:', registration.waiting); // 當前激活的是sw1的服務工做者線程 console.log('active:', registration.active); }); btn.onclick = () => { navigator.serviceWorker.register('./sw2.js').then((registration) => { // 加載新版本的服務工做者線程,觸發更新加載 // 由於以前已經有在活動的服務工做者線程了,waiting狀態的是sw2的線程 console.log('waiting:', registration.waiting); // 激活狀態的是sw1的線程 console.log('active:', registration.active); }) }
在客戶端中能夠大體經過判斷registration.active是否爲ServiceWorker的實例判斷。(active爲ServiceWorker的實例,多是是激活狀態或者激活中的狀態)
在服務工做者線程中,能夠經過添加activate事件處理來判斷,該事件處理程序經常使用於刪除以前的緩存
const CATCH_KEY = 'v1' self.addEventListener('activate', async (event) => { const keys = await caches.keys(); keys.forEach((key) => { if (key !== CATCH_KEY) { caches.delete(key) } }); })
注意:activate事件發生,並不意味着頁面受控。可使用clients.claim()控制不受控的客戶端。
在客戶端中能夠大體經過判斷registration.active是否爲ServiceWorker的實例判斷。(active爲ServiceWorker的實例,能夠是激活狀態或者激活中的狀態)
或者能夠經過查看registration.controller屬性,controller屬性返回已激活ServiceWorker的實例。當新的服務工做者線程控制客戶端時,會觸發navigator.serviceWorker.oncontrollerchange事件
或者navigator.serviceWorker.ready返回的Promise爲resolve時,工做者線程也是已激活的狀態。
const btn = document.getElementById('btn'); navigator.serviceWorker.register('./sw1.js').then((registration) => { // 已激活的線程sw1 console.log('activated', navigator.serviceWorker.controller) }); btn.onclick = () => { navigator.serviceWorker.register('./sw2.js').then((registration) => { // 在等待的線程sw2 console.log('waiting', registration.waiting) // 已激活的線程sw1 console.log('activated', navigator.serviceWorker.controller) }) }
服務工做者會被瀏覽器銷燬並回收資源
下面操做會觸發更新檢查:
若是更新檢查發現差別,瀏覽器會使用新腳本初始化新的工做者線程,新的工做者線程將會達到installed的狀態。而後會等待。除非使用self.skipWaiting(), 強制進入激活中的狀態。或者原有的服務工做者線程客戶端數量變爲0(標籤頁都被關閉)在下一次導航事件新工做者線程進入激活中的狀態。
刷新頁面不會讓更新服務工做者線程進入激活狀態並取代已有的服務工做者線程。好比,有個打開的頁面,其中有一個服務工做者線程正在控制它,而一個更新服務工做者線程正在已安裝狀態中等待。客戶端在頁面刷新期間會發生重疊,即舊頁面尚未卸載,新頁面已加載了。所以,現有的服務工做者線程永遠不會讓出控制權,畢竟至少還有一個客戶端在它的控制之下。爲此,取代現有服務工做者線程惟一的方式就是關閉全部受控頁面。
使用updateViaCache能夠控制,服務工做者線程的緩存
navigator.serviceWorker.register('/serviceWorker.js', { updateViaCache: 'none' });
// 頁面 navigator.serviceWorker.onmessage = ({ data }) => { console.log('服務者線程發送的消息:', data); } navigator.serviceWorker.register('./worker.js').then((registration) => { console.log('註冊成功') }).catch(() => { console.log('註冊失敗') });
// sw self.addEventListener('install', async () => { self.skipWaiting(); }); self.addEventListener('activate', async () => { self.clients.claim(); const allClients = await clients.matchAll({ includeUncontrolled: true }); let homeClient = null; for (const client of allClients) { const url = new URL(client.url); if (url.pathname === '/') { homeClient = client; break; } } homeClient.postMessage('Hello') });
self.onfetch = (fetchEvent) => { fetchEvent.respondWith(fetch(fetchEvent.request)); };
fetchEvent.respondWith 接受Promise,返回Respose對象,
self.onfetch = (fetchEvent) => { fetchEvent.respondWith(caches.match(fetchEvent.request)); };
self.onfetch = (fetchEvent) => { fetchEvent.respondWith(fetch(fetchEvent.request).catch(() => { return caches.match(fetchEvent.request) })); };
self.onfetch = (fetchEvent) => { fetchEvent.respondWith( caches.match(fetchEvent.request).then((response) => { return response || fetch(fetchEvent.request).then(async (res) => { // 網絡返回成功後,將網絡返回的資源,緩存到本地 const catch = await catchs.open('CatchName') await catch.put(fetchEvent.request, res) return res; }) }) ); };
在服務者線程加載時就應該緩存資源,在緩存,和網絡都失效時候,返回通用的後備。