「讀書筆記」第四版JavaScript高級程序設計(第二十七章)

前言

也不徹底是筆記,也作了一些本身的補充

javascript是單線程的嗎?

javascript是單線程的,可是javascript能夠把工做嫁接給獨立的線程。同時不影響單線程模型(不能操做DOM)。javascript

每打開一個網頁就至關於一個沙盒,每個頁面都有本身獨立的內容。工做者線程至關於一個徹底獨立的二級子環境。在子環境中不能與依賴單線程模型API交互(DOM操做),可是能夠與父環境並行執行代碼。css

工做者線程與線程的區別

  1. 工做者線程的底層實現原理就是線程
  2. 工做者線程能夠併發
  3. 工做者線程與主線程之間可使用SharedArrayBuffer實現共享內存,在js中可使用Atomics實現併發控制。
  4. 工做者線程不共享所有的內存
  5. 工做者線程與主線程可能不在一個進程中
  6. 建立工做者線程的開銷也很大(工做者線程應該用於一些長期運行的任務)

工做者線程的類型

  1. ServiceWorker 服務工做者線程
  2. SharedWorker 共享工做者線程
  3. WebWorker,Worker,專用工做者線程

WorkerGlobalScope

在工做者線程中,沒有window對象,全局對象是WorkerGlobalScope的子類的實例html

  • 專用工做者線程全局對象是DedicatedWorkerGlobalScope子類的實例
  • 共享工做者線程全局對象是SharedWorkerGlobalScope的實例
  • 服務工做者是ServiceWorkerGlobalScope的實例

專用工做者線程 Worker or WebWorker

建立專用工做者線程

<!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對象

建立的Worker對象在工做線程終止前,是不會被垃圾回收機制回收的
  • onerror, 父上下文監聽工做者線程的錯誤
  • onmessage, 父上下文監聽工做者線程發送的消息
  • onmessageerror, 父上下文監聽工做者線程發送消息產生的錯誤(好比消息沒法被反序列化)
  • postMessage(),父上下文向工做者線程發送消息
  • terminate(),終止工做者線程

DedicatedWorkerGlobalScope

工做者線程中全局做用域是DedicatedWorkerGlobalScope對象的實例,能夠經過self關鍵字訪問全局對象git

  • self.postMessage, 向父上下文發送消息
  • self.close, 關閉線程
  • self.importScripts, 引入其餘腳本

專用工做者線程的生命週期

生命週期分爲初始化,活動,終止。但父上下文是沒法區分工做者線程的狀態。調用Worker後,雖然worker對象可用,可是worker不必定初始化完畢。可能存在延遲。若是不調用close或者terminate,工做者線程會一直存在,垃圾回收機制也不會回收worker對象。可是調用close和terminate是有一些區別的。若是工做者線程關聯的網頁被關閉,工做者線程也會被終止。github

  1. 在工做者線程的內部,調用close,工做者線程不會當即結束,並且在本次宏任務執行完成後結束。
  2. 在父上下文調用terminate,工做者線程會當即結束。
// 專用工做者線程
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事件捕獲到

專用工做者線程的通訊

postMessage

以前已經在demo中給出例子,這裏再也不贅述

MessageChannel

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進行通信,使用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

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);
}

關於postMessage的第二個參數

以前可使用 worker.postMessage(null, [channel.port2]) 發送channel接口的時候。工做線程的onmessage事件的參數,會接收ports,可是換成其餘數據是接收不到的。postMessage應該是對channel的數據作了特殊的處理。

SharedArrayBuffer

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

注意:

  1. Safari不支持SharedWorker
  2. SharedWorke中的console不必定能打印在父上下文的控制檯中。(我在Chrome中實驗了一下,共享工做者線程中的console,確實不會出如今頁面的控制檯中)

共享工做者線程和建立,安全限制和專用工做者線程都是相同的,共享工做者線程,能夠看做是專用工做者線程的擴展。

SharedWorker能夠被多個同源的上下文(同源的網頁標籤)訪問。SharedWorker的消息接口和專用工做者線程也略有不一樣。

SharedWorker,沒辦法使用行內的worker, 由於經過URL.createObjectURL, 是瀏覽器內部的URL, 沒法在其餘標籤頁使用。

SharedWorker的惟一標示

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對象的屬性

  • onerror 監聽共享工做者線程對象上的拋出的錯誤
  • port 與共享工做者線程通信的接口,SharedWorker會隱式的建立,用於與父上下文通訊

共享工做者線程中的全局對象是SharedWorkerGlobalScope的實例,全局實例上的屬性和方法

  • onconnect,當共享工做者線程創建連接時會觸發, 參數包含ports數組,port能夠把消息穿回給父上下文。sharedWorker.port.onmessage, sharedWorker.port.start(), 都會觸發onconnect事件。
  • close
  • importScripts
  • name

共享工做者線程的生命週期

專用工做者線程只和一個頁面綁定,而共享工做者線程只要還有一個上下文連接,它就不會被回收。共享工做者對象沒法經過terminate關閉,由於共享工做者線程沒有terminate方法,瀏覽器會負責管理共享工做者線程的連接。

連接共享工做者線程

發生connect事件時,SharedWorker構造函數會隱式的建立MessageChannel,並把其中一個port轉移給共享工做者線程的ports數組中。

? 可是共享線程與父上下文的啓動關閉不是對稱的。每次打開會創建連接,connect事件中的ports數組中port數量會加1,可是頁面被關閉,SharedWorker沒法感知。

好比不少頁面連接了SharedWorker,如今一部分如今關閉了,SharedWorker並不知道那些頁面關閉,因此ports數組中,存在被關閉的頁面的port,這些死端口會污染ports數組。

書中給出的方法是,能夠在頁面銷燬以前的beforeunload事件中,通知SharedWorker清除死端口。

SharedWorke示例

示例一

// 父頁面
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應用的體驗變爲原生應用程序同樣。

Notification API 基礎

瀏覽器用於顯示桌面通知的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 基礎

Push API實現了Web接受服務器推送消息的能力。Push API具體的實施代碼,能夠看個人這個例子, 實現了一個簡單的推送。

過程,客戶端生成訂閱信息,發送給服務端保存。服務端端能夠根據須要,在合適的時候,使用訂閱信息向客戶端發送推送。

https://github.com/peoplesing...

Background Sync API 基礎

服務工做者線程,用於按期更新數據的API。

? 原本想實驗以一下這個API,可是註冊定時任務時,提示「DOMException: Permission denied.」錯誤,暫時沒有解決。

ServiceWorkerContainer

服務工做者線程沒有全局的構造函數,經過 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對象是瀏覽器對服務工做者線程的頂部封裝,ServiceWorkerContainer能夠在客戶端中經過navigator.serviceWorker訪問

  • navigator.serviceWorker.oncontrollerchange, 在註冊新的服務工做者線程時觸發(準確的說是在註冊新版本的服務工做者線程,並接管頁面時觸發)
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();
})
  • onerror,服務工做者線程,發生錯誤時觸發
  • onmessage,監聽服務工做者線程發送的消息,收到消息時觸發
  • ready,返回Promise,內容是已經激活的ServiceWorkerRegistration對象
  • controller,返回當前頁面的serviceWorker對象,若是沒有激活的服務工做者線程返回null
  • register(), 建立更新ServiceWorkerRegistration對象
  • getRegistration(scope), 返回Promise, 內容與與ServiceWorkerContainer關聯,而且與scope(路徑)匹配的ServiceWorkerRegistration對象。
  • getRegistrations(), 返回Promise, 內容是與ServiceWorkerContainer關聯的全部ServiceWorkerRegistration對象
  • startMessage(), 開始接受服務工做者線程經過postMessage派發的消息,若是不使用startMessage,Client.postMessage()派發的消息會等待DOMContentLoaded事件以後被調度。startMessage能夠儘早的調度。(使用ServiceWorkerContainer.onmessage時,消息會自動發送,不須要startMessage)
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對象 (註冊對象)

ServiceWorkerRegistration,表示成功註冊的服務工做者線程。能夠經過register返回的Promise中訪問到。在同一頁面調用register,若是URL相同,返回的都是同一個ServiceWorkerRegistration對象。

navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(sw => {
  // ServiceWorkerRegistration對象
  console.log(sw);
})
  • onupdatefound, 線程安裝中的狀態觸發
  • scope, 返回服務工做者線程的路徑
  • navigationPreload,返回與註冊對象關聯的NavigationPreloadManager對象
  • pushManager,返回註冊對象關聯的pushManager對象(主要用於註冊消息,方便服務端推送消息)
  • installing,返回狀態爲installing的服務工做者線程
  • waiting,返回狀態爲waiting的服務工做者線程
  • active,若是有則返回狀態 activating(激活中) 或 active(活動)的服務工做者線程
  • showNotifications,顯示通知,能夠配置title和body
  • update,更新ServiceWorker
  • unregister,取消ServiceWorker

使用ServiceWorker對象

如何獲取ServiceWorker對象?有兩種如下的途徑

  1. ServiceWorkerRegistration對象的active屬性
  2. ServiceWorkerContainer對象的controller屬性

ServiceWorker對象繼承Work,可是不包含terminate方法

  • onstatechange,ServiceWorker.state變化時觸發
  • scriptURL,返回註冊的工做者線程的完整URL,相對路徑會被解析爲完整的路徑
  • state,返回工做者線程的狀態,installing,installed,activating,activated,redundant

ServiceWorker的安全限制

  1. 受到同源的限制
  2. 只能用於HTTPS的協議,127.0.0.1或者localhost下可使用HTTP協議。

若是在非HTTPS的協議下,navigator.serviceWorker是undefined。window.isSecureContext能夠判斷當前上下文是否安全。

ServiceWorkerGlobalScope

服務工做者線程內部,全局對象是ServiceWorkerGlobalScope的實例。ServiceWorkerGlobalScope繼承WorkerGlobalScope,所以擁有它的屬性和方法。線程內部經過self訪問全局的上下文。ServiceWorkerGlobalScope的實例的作了如下的擴展。

  • caches,返回CacheStorage對象
  • clients,返回工做者線程的Clients接口
  • registration,返回服務工做者線程的註冊對象ServiceWorkerRegistration
  • skipWaiting,強制工做者線程進入活動的狀態
  • fetch,在 服務工做者線程中,用於發起網絡請求的方法

專用工做者線程,和共享工做者線程只有一個onmessage事件做爲輸入,但服務工做者線程能夠接受多種事件

  • self.onintall, 服務工做者線程進入安裝的時觸發(activating以前的狀態)
  • self.onactive, 服務工做者線程進入激活狀態時觸發 (activating以後的狀態)
  • self.onfetch, 服務工做者線程能夠攔截頁面的fetch請求
  • self.onmessage, 服務工做者線程接收postMessage發送的消息時觸發
  • self.onnotificationclick,用戶點擊 ServiceWorkerRegistration.showNotification(),生成的通知觸發
  • self.onnotificationclose,用戶關閉 ServiceWorkerRegistration.showNotification(),生成的通知觸發
  • self.onpush,接受到服務端推送的消息時觸發

服務工做者線程做用域的限制

// 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/'
  }
)

服務工做者線程緩存

  1. 服務工做者線程的不會自動緩存任何請求
  2. 服務工做線程不會到期自動失效
  3. 緩存必須手動的更新和刪除
  4. 緩存版本必須手動管理
  5. 緩存策略爲LRU,當緩存的數據超過瀏覽器的限制時

CacheStorage

經過 self.caches 訪問 CacheStorage 對象。CacheStorage時字符串和Cache對象的映射。CacheStorage在頁面或者其餘工做者線程中,均可以訪問使用。

// 訪問緩存,若是沒有緩存則會建立
self.caches.open(key)

CacheStorage也擁有相似Map的API,好比has,delete,keys(),可是它們都是返回Promise的

match,matchAll

分別返回匹配的第一個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')
})();

Cache

CacheStorage對象是字符串和Cache對象的映射。Cache對象則是Request對象或者URL字符串,和Response對象之間的映射。

  • put, put(Request, Response)添加緩存,返回Promise
  • add(request), 使用add發送fetch請求,會緩存響應
  • addAll(request[]), 會對數組中的每一項調用add。

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)
})()
  • matchAll,返回匹配的Response數組
  • match,返回匹配的Response對象
(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()

Client

  • id,客戶端的全局惟一標示
  • type,客戶端的類型
  • url,客戶端的URL
  • postMessage,向單個客戶端發送消息
  • claim,強制工做者線程控制做用域下全部的客戶端。當一個ServiceWorker被初始註冊時,頁面在下次加載以前不會使用它。 claim() 方法會當即控制這些頁面。

? 關於服務工做者線程控制客戶端的問題

一開始註冊服務工做者時,頁面將在下一次加載以前才使用它。有兩種方法能夠提早控制頁面

// 頁面
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版本的服務工做者線程)。

  1. 代碼一致性,服務工做者線程在全部標籤頁都會同一個版本的代碼
  2. 數據一致性,

    • 服務工做者線程提前失敗:語法錯誤,資源加載失敗,都會致使服務工做者線程加載是比
    • 服務工做者線程激進更新:當加載的服務工做者線程,或者服務工做者線程內部依賴的資源,有任何差別,都會致使安裝新版本的服務工做者線程(新安裝的工做者線程會進入installed態)
    • 未激活服務工做者線程消極活動,在使用register安裝服務工做者線程後,服務工做者線程會安裝,但不會被激活(除非全部受到以前版本的控制的標籤頁被關閉,或者調用self.skipWaiting方法)
    • 活動的服務工做者線程粘連,只要至少有一個客戶端與關聯到活動的服務工做者線程,瀏覽器 就會在該源的全部頁面中使用它。對於新版本的服務工做者線程會一直等待。

生命週期

  1. 已解析(parsed)
  2. 安裝中 (installing)
  3. 已安裝(installed)
  4. 激活中(activating)
  5. 已激活(activated)
  6. 已失效(redundant)

已解析 parsed

調用 navigator.serviceWorker.register() 會進入已解析的狀態,可是該狀態沒有事件,也沒有對應的ServiceWorker.state的值。

安裝中 installing

在客戶端能夠經過檢查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'
        ])
    })
})

已安裝 installed

在客戶端能夠經過檢查registration.waiting是否被設置爲了ServiceWorker實例,判斷是不是已安裝的狀態。若是瀏覽器中沒有以前版本的的ServiceWorker,新安裝的ServiceWorker會直接跳過這個狀態,進入激活中的狀態。不然將會等待。

navigator.serviceWorker.register('./worker.js').then((registration) => {
    console.log(registration.waiting)
});

若是有已安裝的ServiceWorker,可使用self.skipWaiting,強制工做者線程進入活動的狀態

激活中狀態 activating

若是瀏覽器中沒有以前版本的ServiceWorker,則新的服務工做者線程會直接進入這個狀態。若是有其餘服務者線程,能夠經過下面的方法,使新的服務者線程進入激活中的狀態

  1. self.skipWaiting(), 強制進入激活中的狀態
  2. 原有的服務工做者線程客戶端數量變爲0(標籤頁都被關閉)在下一次導航事件新工做者線程進入激活中的狀態。
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);
    })
}

image.png

在客戶端中能夠大體經過判斷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()控制不受控的客戶端。

已激活的狀態 activated

在客戶端中能夠大體經過判斷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)
    })
}

已失效的狀態 redundant

服務工做者會被瀏覽器銷燬並回收資源

更新服務工做者線程

下面操做會觸發更新檢查:

  1. 使用navigator.serviceWorker.register(),加載不一樣URL, 會檢查
  2. 發生了push,fetch事件。而且至少24小時沒有更新檢查。
  3. 瀏覽器導航到了到服務工做者線程做用域中的一個頁面。

若是更新檢查發現差別,瀏覽器會使用新腳本初始化新的工做者線程,新的工做者線程將會達到installed的狀態。而後會等待。除非使用self.skipWaiting(), 強制進入激活中的狀態。或者原有的服務工做者線程客戶端數量變爲0(標籤頁都被關閉)在下一次導航事件新工做者線程進入激活中的狀態。

刷新頁面不會讓更新服務工做者線程進入激活狀態並取代已有的服務工做者線程。好比,有個打開的頁面,其中有一個服務工做者線程正在控制它,而一個更新服務工做者線程正在已安裝狀態中等待。客戶端在頁面刷新期間會發生重疊,即舊頁面尚未卸載,新頁面已加載了。所以,現有的服務工做者線程永遠不會讓出控制權,畢竟至少還有一個客戶端在它的控制之下。爲此,取代現有服務工做者線程惟一的方式就是關閉全部受控頁面。

updateViaCache

使用updateViaCache能夠控制,服務工做者線程的緩存

  • none,服務工做者線程的腳本以及importScripts引入都不會緩存
  • all,全部文件都會被http緩存
  • imports,服務工做者線程的腳本不會被緩存,importScripts的文件會被http緩存
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')
});

攔截fetch請求

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;
      })
    })
  );
};

通用後備

在服務者線程加載時就應該緩存資源,在緩存,和網絡都失效時候,返回通用的後備。

參考

相關文章
相關標籤/搜索