此次體驗一種新的博客風格,咱們長話短說,針針見「血」。javascript
在深刻 Service Worker 以前,咱們須要快速回顧以下基礎。php
誕生之初,JavaScript 是單線程的。css
進程有私有的虛擬地址空間、代碼、數據和其它系統資源,進程申請建立和使用的系統資源會隨其終止而銷燬。線程運行在進程之中,系統建立進程以後就開始啓動執行進程的主線程,並隨主線程的退出而終止。html
JavaScript 做爲瀏覽器腳本語言,爲方便準確無誤的操做 DOM,誕生之初便採用了單線程的方式。舉個例子,若多線程同時分別刪除和修改同一個 DOM,咱們很難預知其執行結果。java
但單線程中,必須經過異步和回調來優化耗時操做。git
咱們在網頁上提交一個表單,並不但願在提交後頁面卡頓,一直等待服務端返回的提交結果。這時咱們須要能在單線程中發送異步請求,點擊提交表單後能夠先在頁面進行其餘操做。github
Ajax 讓咱們能夠向後端發送異步請求,同時不影響用戶在界面中繼續操做。當 Ajax 接收到服務端的響應以後,便經過回調函數執行以後的操做。一個典型的異步 Ajax 實戰場景以下:chrome
// 生成可發送同步/異步請求的 XMLHttpRequest 對象實例 var oReq = new XMLHttpRequest(); // open 方法初始化請求方法、地址,第三個參數 true 聲明進行異步請求 oReq.open("GET", "http://www.jianshu.com/", true); // 請求的整個過程當中有五種狀態,且同一時刻只能存在一種狀態: // 1. 未打開 // 2. 未發送 // 3. 已獲取響應體 // 4. 正在下載響應體 // 5. 請求完成 // 當請求狀態發生改變時,觸發 onreadystatechange 會被調用 oReq.onreadystatechange = function (oEvent) { // 若是已經開始下載響應體了 if (oReq.readyState === 4) { // 若是響應體成功下載,而且服務端返回 200 狀態碼 if (oReq.status === 200) { // 打印響應信息 console.log(oReq.responseText); } else { console.log("Error", oReq.statusText); } } }; // send 方法發送請求,因爲此請求是異步的,該方法馬上返回 oReq.send(null);
當咱們的多個請求須要依賴於上一個請求的服務端響應時,回調函數中 Ajax 的層級逐步提升,可維護性極度降低,這就是回調地獄。編程
I Promise U that I`ll Marry U!!!後端
Promise 由 ES6 標準原生支持。正如題名,Promise 做出諾言,也要所以承擔成功(fulfilled)或失敗(rejected)的結果,以便解決回調地獄問題:
// 生成一個 Promise 實例,傳入有特定的兩個參數的匿名函數 // Promise 初始狀態是 pending // resolve 被調用時,將 Promise 狀態改成成功(fulfilled) // reject 被調用時,將 Promise 狀態改成失敗(rejected) // 該匿名函數拋出錯誤時,Promise 狀態爲失敗(rejected) var a = new Promise(function(resolve, reject) { // setTimeout() 模擬異步請求,成功後執行 resolve() 方法 setTimeout(function() { resolve('1') }, 2000) }) a.then(function(val){ // then() 有兩個函數做爲參數,onfulfilled 和 onrejected // 當 Promise 狀態爲 fulfilled 時,調用 then 的 onfulfilled 方法 // 當 Promise 狀態爲 rejected 時,調用 then 的 onrejected 方法 console.log(val) // then() 方法返回 Promise 對象實例,因此可被鏈式調用 return new Promise(function(resolve, reject) { setTimeout(function() { resolve('2') }, 2000) }) }) .then(function(val) { // 鏈式調用的第二個環節,處理上一個環節返回的 Promise 對象 console.log(val) })
Promise 對象的生命週期以下圖。
除了異步編程,咱們還能夠有 Web Worker。
經過異步編程,咱們的頁面能夠邊響應用戶的下一步操做邊等待服務端的迴應,再也不擁有阻塞感,但 JavaScript 的單線程問題並無獲得相應的解決。經過 HTML 5 標準支持的 Web Worker,咱們能夠爲 JavaScript 建立運行在後臺的額外線程,並被多個頁面共享。
在一個簡單的 Web Worker 實例中,main.js 和 task.js 的源碼以下。
// main.js // 實例化 Worker 對象,其實質爲新建立的工做線程在主線程的引用 var worker = new Worker("task.js") // postMessage 方法與新建立的工做線程通訊 worker.postMessage({ id:1, msg:'Hello World' }); // 當 Worker 線程返回數據時,onmessage 回調函數執行 worker.onmessage = function(message) { var data = message.data; console.log(JSON.stringify(data)) // terminate 方法終止 worker 線程的運行 worker.terminate() }; // 當 Worker 線程出錯時,onerror 回調函數執行 // error 參數中封裝了錯誤對象的文件名、出錯行號和具體錯誤信息 worker.onerror = function(error) { console.log(error.filename, error.lineno, error.message) }
// task.js onmessage = function(message) { var data = message.data data.msg = 'Hi from task.js' postMessage(data) }
在 Chrome 瀏覽器裏,以上代碼必須運行在 Web 容器如 Apache 中。同時,WebKit 內核加載並執行 Worker 線程的流程以下圖所示。
上述知識點的詳盡博客盡請期待,您能夠先查閱其它資料進行補充。
Service Worker 基於 Web Worker 事件驅動。
Service Worker 一樣能夠在瀏覽器後臺掛起新線程,來緩解 JavaScript 的單線程問題。而且,咱們能夠用 Service Worker 攔截網絡請求進行本地緩存或請求轉發,至關於充當服務端與瀏覽器、瀏覽器與 Web 應用程序之間的代理服務器。
Service Worker 帶來了速度,極大的提升了用戶體驗。
Service Worker 大量使用 Promise 對象。
由於一般 Service Worker 會等待響應後繼續,並根據響應返回一個成功或者失敗的操做。Promise 很是適合這種場景。
零、Service Worker 的生命週期。
所謂生命週期,包括 Service Worker 的註冊、安裝、激活、控制和銷燬時的所有過程。咱們須要對 Service Worker 的生命週期有所瞭解。
先決條件:
控制時:處於兩種狀態之一:
1、註冊 Service Worker。
當瀏覽器對 Service Worker 提供原生支持時,咱們即可以在頁面加載後註冊指定的 JavaScript 文件,並運行在後臺線程之中,如下代碼是這一過程的實例。
<!DOCTYPE html> <html> <head> <title>ServiceWorker</title> </head> <body> <h1>Hello World!</h1> <script> // 檢查瀏覽器是否對 serviceWorker 有原生支持 if ('serviceWorker' in navigator) { // 有原生支持時,在頁面加載後開啓新的 Service Worker 線程,從而優化首屏加載速度 window.addEventListener('load', function() { // register 方法裏第一個參數爲 Service Worker 要加載的文件;第二個參數 scope 可選,用來指定 Service Worker 控制的內容的子目錄 navigator.serviceWorker.register('./ServiceWorker.js').then(function(registration) { // Service Worker 註冊成功 console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function(err) { // Service Worker 註冊失敗 console.log('ServiceWorker registration failed: ', err); }); }); } </script> </body> </html>
這裏經過 php 內置命令監聽項目目錄,便能看到 Service Worker 註冊成功。同時,在 Chrome 瀏覽器裏,能夠訪問 chrome://inspect/#service-workers
和 chrome://serviceworker-internals/
來檢查 Service Worker 是否已經啓用。
2、安裝 Service Worker。
安裝階段,咱們能夠執行任何任務。這裏咱們逐步打開緩存、緩存文件和確認全部須要的資產是否緩存。ServiceWorker.js
中的實例安裝代碼以下:
var CACHE_NAME = 'my-site-cache-v1'; var urlsToCache = [ '/', '/styles/main.css', '/script/main.js' ]; self.addEventListener('install', function(event) { // Perform install steps event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('Opened cache'); return cache.addAll(urlsToCache); }) ); });
這要求咱們在與項目根目錄下創建 main.js
和 main.css
空文件。咱們能夠在 Chrome 開發者工具裏的「Application」菜單的「Cache Storage」中看到相應的緩存。而且在圖中的「Service Workers」選項卡中看到正在運行的 Service Workers。
且從上面的代碼能夠看到,經過 Service Worker 對象加載的文件擁有全局變量 caches 等,而且 self 關鍵字指向這個對象自己。cache 使咱們能夠存儲網絡響應發來的資源,而且根據它們的請求來生成 key。這個 API 和瀏覽器的標準的緩存工做原理很類似,且會持久存在,直到咱們釋放主動空間——咱們擁有所有的控制權。
3、激活 Service Worker。
當 Service Worker 安裝成功後,便被激活,這時可實時控制做用域中的全部網站,進行緩存文件等操做。不過首次使用 Service Worker 的頁面須要再次加載纔會受其控制。
4、控制 Service Worker
如下列舉幾個常見的 Service Worker 應用場景。
1. 文件緩存
self.addEventListener('fetch', function(event) { event.respondWith( // 如下方法檢視請求,並從服務工做線程所建立的任何緩存中查找緩存的結果。 caches.match(event.request) .then(function(response) { console.log(event.request) console.log(caches) // 若是發現匹配的響應,則返回緩存的值 if (response) { return response; } return fetch(event.request); } ) ); });
經過上述文件緩存過程,咱們能夠告訴 Service Worker 如何使用這些緩存文件,並經過 fetch 事件來捕獲。fetch 事件只會在瀏覽器準備請求 Service Worker 控制的資源時纔會被觸發。這些資源包括了指定的 scope 內的文檔,和這些文檔內引用的其餘任何資源。
2. 多頁面傳遞消息
咱們能夠打開多個 https://nzv3tos3n.qnssl.com/m... 測試頁面來進行測試,效果以下。
其中,index.js
源碼爲:
(function () { if (navigator.serviceWorker) { // 獲取頁面 DOM 元素 var msgIpt = document.getElementById('ipt'), showArea = document.getElementById('show'), sendBtn = document.getElementById('sendBtn'); navigator.serviceWorker.register('service-worker3.js'); navigator.serviceWorker.addEventListener('message', function (event) { // 接受數據,並填充在 DOM 中 showArea.innerHTML = showArea.innerHTML + ('<li>' + event.data.message + '</li>'); }); sendBtn.addEventListener('click', function () { // 綁定點擊事件,點擊後發送數據 navigator.serviceWorker.controller.postMessage(msgIpt.value); msgIpt.value = ''; }); } })();
3. 更新 Service Worker
每次用戶導航至使用 Service Worker 的站點時,瀏覽器會嘗試在後臺從新下載該腳本文件。這時新的 Service Worker 將會在後檯安裝,並在第二次訪問時獲取控制權,爲了避免與新的 Service Worker 緩存的文件衝突,咱們可使用相似 caches.open('v2')
語句來建立新的緩存目錄。
this.addEventListener('install', function(event) { event.waitUntil( // 建立新的緩存目錄,並指定 caches.open('v2').then(function(cache) { return cache.addAll([ '/sw-test/', '/sw-test/index.html', … ]); }); ); });
當新的 Service Worker 激活,記得刪除 v1 緩存目錄,代碼以下。
this.addEventListener('activate', function(event) { // 聲明緩存白名單,該名單內的緩存目錄不會被生成 var cacheWhitelist = ['v2']; event.waitUntil( // 傳給 waitUntil() 的 promise 會阻塞其餘的事件,直到它完成 // 確保清理操做會在第一次 fetch 事件以前完成 caches.keys().then(function(keyList) { return Promise.all(keyList.map(function(key) { if (cacheWhitelist.indexOf(key) === -1) { return caches.delete(key); } })); }) ); });
4. 預緩存
Service Worker 也能夠在後臺主動發送請求,優化用戶體驗,圖片來源於《餓了麼的 PWA 升級實踐》。
5. Service Worker 支持的全部事件
5、銷燬 Service Worker
瀏覽器決定是否銷燬 Service Worker。在無痕瀏覽中,當頁面關閉時相應的 Service Worker 會被銷燬,所以儘可能不要在代碼中留存全局變量。能夠訪問 chrome://inspect/#service-workers和 chrome://serviceworker-internals/ 來檢查 Service Worker 是否已經停用。
困擾 Web 用戶多年的難題——丟失網絡鏈接,從 APPCache 到 Service Worker,解決辦法一直在完善。Service Worker 開啓的服務工做線程,對如何步入 Web 應用開發之旅,提供了很棒的切入角度。
那麼,如何從本文開始,更好的學習 Service Worker?結合更多其它技術博客與 Service Worker 的 API 文檔會更好。本文圖片素材、寫做思路多取源於此。
接口列表 | |
---|---|
Cache | CacheStorage |
Client | Clients |
ExtendableEvent | FetchEvent |
InstallEvent | Navigator.serviceWorker |
NotificationEvent | PeriodicSyncEvent |
PeriodicSyncManager | PeriodicSyncRegistration |
ServiceWorker | ServiceWorkerContainer |
ServiceWorkerGlobalScope | ServiceWorkerRegistration |
SyncEvent | SyncManager |
SyncRegistration | WindowClient |
本文全部關於 Web Worker、Service Worker 的代碼,持續更新在個人 gist 中:
https://gist.github.com/hyler...
- Hello,我是韓亦樂,現任本科軟工男一枚。軟件工程專業的一路學習中,我有不少感悟,也享受持續分享的過程。若是想了解更多或能及時收到個人最新文章,歡迎訂閱個人我的微信號:韓亦樂。個人簡書我的主頁中,有個人訂閱號二維碼和 Github 主頁地址;[個人知乎主頁]中也會堅持產出,歡迎關注。
- 本文內部編號經由個人 Github 相關倉庫統一管理;本文可能發佈在多個平臺但僅在上述倉庫中長期維護;本文同時採用【知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議】進行許可。