構建 Web 應用之 Service Worker 初探

此次體驗一種新的博客風格,咱們長話短說,針針見「血」。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 可有效加快重複訪問網絡應用的速度。
  • 擁有攔截請求、消息推送、靜默更新、地理圍欄等服務。
  • 能夠在客戶端經過 indexedDB API 保存持久化信息。

Service Worker 大量使用 Promise 對象。

由於一般 Service Worker 會等待響應後繼續,並根據響應返回一個成功或者失敗的操做。Promise 很是適合這種場景。

零、Service Worker 的生命週期。

所謂生命週期,包括 Service Worker 的註冊、安裝、激活、控制和銷燬時的所有過程。咱們須要對 Service Worker 的生命週期有所瞭解。

  • 先決條件:

    • 瀏覽器支持:Service Worker。
    • 在 localhost 域或 HTTPS 域下運行:介於咱們可以經過使用 Service Worker 劫持鏈接、編撰以及過濾響應來進行權限較高的操做。

  • 註冊:註冊過程獨立於網頁,先在頁面執行註冊,以後在瀏覽器後臺啓動安裝步驟。
  • 安裝:一般須要緩存某些靜態資源。當全部文件已成功緩存,則安裝完畢。若是任何文件下載失敗或緩存失敗,則安裝失敗,沒法激活。
  • 激活:管理就緩存的絕佳機會。激活後它將會對做用域頁面實時控制,不過首次註冊該服務工做線程的頁面須要再次加載纔會受其控制。
  • 控制時:處於兩種狀態之一:

    • ①、終止以節省內存;
    • ②、監聽獲取 fetch 和消息 message 事件。
  • 銷燬:由瀏覽器決定,所以儘可能不要留存全局變量。

初始安裝時的簡化聲明週期

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-workerschrome://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.jsmain.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...

相關文章
相關標籤/搜索