1. 程式人生 > >漸進式web應用開發---service worker (二)

漸進式web應用開發---service worker (二)

閱讀目錄

  • 1. 建立第一個service worker 及環境搭建
  • 2. 使用service worker 對請求攔截
  • 3. 從web獲取內容
  • 4. 捕獲離線請求
  • 5. 建立html響應
  • 6. 理解 CacheStorage快取
  • 7. 理解service worker生命週期
  • 8. 理解 service worker 註冊過程
  • 9. 理解更新service worker
  • 10. 理解快取管理和清除快取
  • 11. 理解重用已快取的響應
回到頂部

1. 建立第一個service worker 及環境搭建

在上一篇文章,我們已經講解了 service worker 的基本原理,請看上一篇文章 . 從這篇文章開始我們來學習下 service worker的基本知識。

在講解之前,我們先來搭建一個簡單的專案,和之前一樣,首先我們來看下我們整個專案目錄結構,如下所示:

|----- 專案
|  |--- public
|  | |--- js               # 存放所有的js
|  | | |--- main.js        # js入口檔案
|  | |--- style            # 存放所有的css
|  | | |--- main.styl      # css 入口檔案
|  | |--- index.html       # index.html 頁面
|  | |--- images
|  |--- package.json
|  |--- webpack.config.js
|  |--- node_modules

如上目錄結構就是我們專案的最簡單的目錄結構。我們先來看下我們各個目錄的檔案程式碼。

index.html 程式碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>service worker 實列</title>
</head>
<body>
  <div id="app">22222</div>
</body>
</html>

其他的檔案程式碼目前可以忽略不計,基本上沒有什麼程式碼。因此我們在專案的根目錄下 執行  npm run dev 後,就會啟動我們的頁面。如下執行的頁面。如下所示:

如上就是我們的頁面簡單的整個專案的目錄架構了,現在我們來建立我們的第一個 service worker了。

一:建立我們的第一個service worker

首先從當前頁面註冊一個新的service worker, 因此在我們的 public/js/main.js 新增如下程式碼:

// 載入css樣式
require('../styles/main.styl');

console.log(navigator);

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(function(registration){
      console.log("Service Worker registered with scope: ", registration.scope);
    }).catch(function(err) {
      console.log("Service Worker registered failed:", err);
    });
}

如上程式碼,首先在我們的chrome下列印 console.log(navigator); 看到如下資訊:

切記:要想支援service worker 的話,在本地必須以 http://localhost:8081/ 或 http://127.0.0.1:8081 來訪問頁面,在線上必須以https來訪問頁面,否則的話,瀏覽器是不支援的,因為http訪問會涉及到service worker安全性問題。 比如我在本地啟動的是以 http://0.0.0.0:8081/ 這樣來訪問頁面的話,瀏覽器是不支援 service worker的。如下所示:

如上程式碼,使用了if語句判斷了瀏覽器是否支援 service worker, 如果支援的話,我們會使用 navigator.serviceWorker.register 方法註冊了我們的 service worker,該方法接收2個引數,第一個引數是我們的指令碼的URL,第二個引數是作用域(晚點會講到)。

如上register方法會返回一個promise物件,如果promise成功,說明註冊service worker 成功,就會執行我們的then函式程式碼,否則的話就會執行我們的catch內部程式碼。

如上程式碼我們的 navigator.serviceWorker.register('/sw.js'), 註冊了一個 sw.js,因此我們需要在我們的 專案的根目錄下新建一個 sw.js 。目前它沒有該目錄檔案,然後我們繼續重新整理頁面,可以看到它進入了catch語句程式碼;如下所示:

現在我們需要在 我們的專案根目錄下新建 sw.js.

注意:我們的sw.js檔案路徑放到了專案的根目錄下,這就意味著 serviceworker 和網站是同源的,因此在 專案的根目錄下的所有請求都可以代理的。如果我們把 sw.js 放入到我們的 public 目錄下的話,那麼就意味這我們只能代理public下的網路請求了。但是我們可以在註冊service worker的時候傳入一個scope選項,用來覆蓋預設的service worker的作用域。比如如下:

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

如上兩條命令是完全相同的作用域了。

下面我們的兩條命令將註冊兩個不同的作用域,因為他們是放在兩個不同的目錄下。如下程式碼:

navigator.serviceWorker.register('/sw.js', {scope: '/xxx'});
navigator.serviceWorker.register('/sw22.js', {scope: '/yyy'});

現在在我們的專案根目錄下有sw.js 這個檔案,因此這個時候我們再重新整理下我們的頁面,就可以看到打印出訊息出來了 Service Worker registered with scope:  http://localhost:8081/ ,說明註冊成功了。如下所示:

下面我們繼續在我們的 專案的根目錄 sw.js 新增程式碼如下:

console.log(self);

self.addEventListener("fetch", function(e) {
  console.log("Fetch request for: ", e.request.url);
});

如上程式碼,我們首先可以列印下self是什麼,在service worker中,self它是指向service worker本身的。我們首先在控制檯中看看self到底是什麼,我們可以先打印出來看看,如下所示:

如上程式碼,我們的service worker添加了一個事件監聽器,這個監聽器會監聽所有經過service worker的fetch事件,並允許我們的回撥函式,我們修改sw.js後,我們並沒有看到控制檯列印我們的訊息,因此我們先來簡單的學習下我們的service worker的生命週期。

當我們修改sw.js 程式碼後,這些修改並沒有在重新整理瀏覽器之後立即生效,這是因為原先的service worker依然處於啟用狀態,但是我們新註冊的 service worker 仍然處於等待狀態,如果這個時候我們把原先第一個service worker停止掉就可以了,為什麼有這麼多service worker,那是因為我重新整理下頁面,瀏覽器會重新執行我們的main.js 程式碼中註冊 sw.js 程式碼,因此會有很多service worker,現在我們要怎麼做呢?我們開啟我們的chrome控制檯,切換到 Application 選項,然後禁用掉我們的第一個正處於啟用狀態下的service worker 即可,如下圖所示:

禁用完成後,我們再重新整理下頁面即可看到我們console.log(self);會打印出來了,說明已經生效了。

但是我們現在是否注意到,我們的監聽fetch事件,第一次重新整理的時候沒有打印出 console.log("Fetch request for: ", e.request.url); 這個資訊,這是因為service worker第一次是註冊,註冊完成後,我們才可以監聽該事件,因此當我們第二次以後重新整理的時候,我們就可以使用fetch事件監聽到我們頁面上所有的請求了,我們可以繼續第二次重新整理後,在控制檯我們可以看到如下資訊,如下圖所示:

下面為了使我們更能理解我們的整個目錄架構,我們再來看下我們整個目錄結構變成如下樣子:

|----- 專案
|  |--- public
|  | |--- js               # 存放所有的js
|  | | |--- main.js        # js入口檔案
|  | |--- style            # 存放所有的css
|  | | |--- main.styl      # css 入口檔案
|  | |--- index.html       # index.html 頁面
|  | |--- images
|  |--- package.json
|  |--- webpack.config.js
|  |--- node_modules
|  |--- sw.js
回到頂部

2. 使用service worker 對請求攔截

 我們第二次以後重新整理的時候,我們可以監聽到我們頁面上所有的請求,那是不是也意味著我們可以對這些請求進行攔截,並且修改程式碼,然後返回呢?當然可以的,因此我們把sw.js 程式碼改成如下這個樣子:程式碼如下所示:

self.addEventListener("fetch", function(e) {
  if (e.request.url.includes("main.css")) {
    e.respondWith(
      new Response(
        "#app {color:red;}",
        {headers: { "Content-Type": "text/css" }}
      )
    )
  }
});

如上程式碼,我們監聽fetch事件,並檢查每個請求的URL是否包含我們的 main.css 字串,如果包含的話,service worker 會動態的建立一個Response物件,在響應中包含了自定義的css,並使用這個物件作為我們的響應,而不會向遠端伺服器請求這個檔案。效果如下所示:

回到頂部

3. 從web獲取內容

在第二點中,我們攔截main.css ,我們通過指定內容和頭部,從零開始建立了一個新的響應物件,並使用它作為響應內容。但是service worker 更廣泛的用途是響應來源於網路請求。我們也可以監聽圖片。

我們在我們的index.html頁面中新增一張圖片的程式碼;如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>service worker 實列</title>
</head>
<body>
  <div id="app">22222</div>
  <img src="/public/images/xxx.jpg" />
</body>
</html>

然後我們在頁面上瀏覽下,效果如下:

現在我們通過service worker來監聽該圖片的請求,然後替換成另外一張圖片,sw.js 程式碼如下所示:

self.addEventListener("fetch", function(e) {
  if (e.request.url.includes("/public/images/xxx.jpg")) {
    e.respondWith(
      fetch("/public/images/yyy.png")
    );
  }
});

然後我們繼續重新整理下頁面,把之前的service worker禁用掉,繼續重新整理頁面,我們就可以看到如下效果:

和我們之前一樣,我們監聽fetch事件,這次我們查詢的字串是 "/public/images/xxx.jpg",當檢測到有這樣的請求的時候,我們會使用fetch命令建立一個新的請求,並把第二張圖片作為響應返回,fetch它會返回一個promise物件。

注意:fetch方法的第一個引數是必須傳遞的,可以是request物件,也可以是包含一個相對路徑或絕對路徑的URL的字串。比如如下:

// url 請求
fetch("/public/images/xxx.jpg");

// 通過request物件中的url請求
fetch(e.request.url);

// 通過傳遞request物件請求,在這個request物件中,除了url,可能還包含額外的頭部資訊,表單資料等。
fetch(e.request);

fetch它還有第二個引數,可以包含是一個物件,物件裡面是請求的選項。比如如下:

fetch("/public/images/xxx.jpg", {
  method: 'POST',
  credentials: "include"
});

如上程式碼,對一個圖片發起了一個post請求,並在頭部中包含了cookie資訊。fetch會返回一個promise物件。

回到頂部

4. 捕獲離線請求

我們現在有了上面的service worker的基礎知識後,我們來使用service worker檢測使用者何時處於離線狀態,如果檢測到使用者是處於離線狀態,我們會返回一個友好的錯誤提示,用來代替預設的錯誤提示。因此我們需要把我們的sw.js 程式碼改成如下所示:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return new Response(
        "歡迎來到我們的service worker應用"
      )
    })
  );
});

如上程式碼我們監聽並捕獲了所有的fetch事件,然後使用另一個完全相同的fetch操作進行相應,我們的fetch它是返回的是一個promise物件,因此它有catch失敗時候來捕獲異常的,因此當我們重新整理頁面後,然後再切換到離線狀態時候,我們可以看到頁面變成如下提示了,如下圖所示:

回到頂部

5. 建立html響應

 如上響應都是對字串進行響應的,但是我們也可以拼接html返回進行響應,比如我們可以把sw.js 程式碼改成如下:

var responseContent = `<html>
  <body>
    <head>
      <meta charset="UTF-8">
      <title>service+worker異常</title>
      <style>body{color:red;font-size:18px;}</style>
    </head>
    <h2>service worker異常的處理</h2>
  </body>
`

self.addEventListener("fetch", function(event) {
  console.log(event.request);
  event.respondWith(
    fetch(event.request).catch(function() {
      return new Response(
        responseContent,
        {headers: {"Content-Type": "text/html"}}
      )
    })
  );
});

然後我們繼續重新整理頁面,結束掉第一個service worker應用,可以看到如下所示:

如上程式碼,我們先定義了給離線使用者的html內容,並將其賦值給 responseContent變數中,然後我們新增一個事件監聽器,監聽所有的fetch事件,我們的回撥函式就會被呼叫,它接收一個 event事件物件作為引數,隨後我們呼叫了該事件物件的 respondWith 方法來響應這個事件,避免其觸發預設行為。

respondWith方法接收一個引數,它可以是一個響應物件,也可以是一段通過promise得出響應物件的程式碼。

如上我們呼叫fetch,並傳入原始請求物件,不僅僅是URL,它還包括頭部資訊、cookie、和請求方法等,如上我們的列印的 console.log(event.request); 如下所示:

fetch方法它返回的是一個promise物件,如果使用者和伺服器線上正常的話,那麼他們就返回頁面中正常的頁面,那麼這個時候promise物件會返回完成狀態。如下我們把利息勾選框去掉,再重新整理下頁面,可以看到如下資訊:

如上頁面正常返回了,它就不會進入promise中catch異常的情況下,但是如果離線狀態或者異常的情況下,那麼就會進入catch異常程式碼。
因此catch函式就會被呼叫到。

回到頂部

6. 理解 CacheStorage快取

在前面的學習當中,當用戶離線的時候,我們可以向他們的頁面顯示自定義的html內容,而不是瀏覽器的預設錯誤提示,但是這樣也不是最好的處理方式,我們現在想要做的是,如果使用者線上的時候,我們以正常的index.html內容顯示給使用者,包括html內容,圖片和css樣式等,當用戶離線的時候,我們的目標還是想顯示index.html中的內容,圖片和css樣式等這樣的顯示給使用者,也就是說,不管是線上也好,離線也好,我們都喜歡這樣顯示內容給使用者訪問。因此如果我們想要實現這麼一個功能的話,我們需要在使用者線上訪問的時候,使用快取去拿到檔案,然後當用戶離線的時候,我們就顯示快取中的檔案和內容即可實現這樣的功能。

什麼是CacheStorage?

CacheStorage 是一種全新的快取層,它擁有完全的控制權。既然CacheStorage可以快取,那麼我們現在要想的問題是,我們什麼時候進行快取呢?到目前為止,我們先來看下 service worker的簡單的生命週期如下:

安裝中 ----> 啟用中 -----> 已啟用

我們之前是使用 service worker 監聽了fetch事件,那麼該事件只能夠被啟用狀態的service worker所捕獲,但是我們目前不能在該事件中來快取我們的檔案。我們需要監聽一個更早的事件來快取我們的service worker頁面所依賴的檔案。

因此我們需要使用 service worker中的install事件,在每個service worker中,該事件只會發生一次。即首次在註冊之後以及啟用之前,該事件會發生。在service worker接管頁面並開始監聽fetch事件之前,我們通過該事件進行監聽,因此就可以很好的快取我們所有離線可用的檔案。

如果安裝有問題的話,我們可以在install事件中取消安裝service worker,如果在快取時出現問題的話,我們可以終止安裝,因為當用戶重新整理頁面後,瀏覽器會在使用者下次訪問頁面時再次嘗試安裝service worker。通過這種方式,我們可以有效的為service worker建立安裝依賴,也就是說在service worker安裝並激活之前,我們必須下載並快取這些檔案。

因此我們現在把 sw.js 全部程式碼改成如下程式碼:

// 監聽install的事件
self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open("cacheName").then(function(cache) {
      return cache.add("/public/index.html");
    })
  )
});

如上程式碼,我們為install事件添加了事件監聽器,在新的service worker註冊之後,該事件會立即在其安裝階段被呼叫。

如上我們的service worker 依賴於 "/public/index.html", 我們需要驗證它是否成功快取,然後我們才能認為它安裝成功,並激活新的 service worker,因為需要非同步獲取檔案並快取起來,所以我們需要延遲install事件,直到非同步事件完成,因此我們這邊使用了waitUntil,waitUntil會延長事件存在的時間,直到promise成功,我們才會呼叫then方法後面的函式。
在waitUntil函式中,我們呼叫了 caches.open並傳入了快取的名稱為 "cacheName". caches.open 開啟並返回一個現有的快取,如果沒有找到該快取,我們就建立該快取並返回他。最後我們執行then裡面的回撥函式,我們使用了 cache.add("/public/index.html").這個方法將請求檔案放入快取中,快取的鍵名是 "/public/index.html"。

2. 從CacheStorage中取回請求

上面我們使用 cache.add 將頁面的離線版本儲存到 CacheStorage當中,現在我們需要做的事情是從快取中取回並返回給使用者。

因此我們需要在sw.js 中新增fetch事件程式碼,新增如下程式碼:

// 監聽fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match("/public/index.html");
    })
  )
});

如上程式碼和我們之前的fetch事件程式碼很相似,我們這邊使用 caches.match 從CacheStorage中返回內容。

注意:match方法可以在caches物件上呼叫,這樣會在所有快取中尋找,也可以在某個特定的cache物件上呼叫。如下所示:

// 在所有快取中尋找匹配的請求
caches.match('/public/index.html');

// 在特定的快取中尋找匹配的請求
cache.open("cacheName").then(function(cache) {
  return cache.match("/public/index.html");
});

match方法會返回一個promise物件,並且向resolve方法傳入在快取中找到的第一個response物件,當找不到任何內容的時候,它的值是undefined。也就是說,即使match找不到對應的響應的時候,match方法也不會被拒絕。如下所示:

caches.match("/public/index.html").then(function(response) {
  if (response) {
    return response;
  }
});

3. 在demo中使用快取

在如上我們已經把sw.js 程式碼改成如下了:

// 監聽install的事件
self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open("cacheName").then(function(cache) {
      return cache.add("/public/index.html");
    })
  )
});

// 監聽fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match("/public/index.html");
    })
  )
});

當我們第一次訪問頁面的時候,我們會監聽install事件,對我們的 "/public/index.html" 進行快取,然後當我們切換到離線狀態的時候,我們再次重新整理可以看到我們的頁面只是快取了 index.html頁面,但是頁面中的css和圖片並沒有快取,如下所示:

現在我們要做的事情是,我們需要對我們所有頁面的上的css,圖片,js等資原始檔進行快取,因此我們的sw.js 程式碼需要改成如下所示:

var CACHE_NAME = "cacheName";
var CACHE_URLS = [
  "/public/index.html",
  "/main.css",
  "/public/images/xxx.jpg"
];

self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(CACHE_URLS);
    })
  )
});

// 監聽fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match(e.request).then(function(response) {
        if (response) {
          return response;
        } else if (e.request.headers.get("accept").includes("text/html")) {
          return caches.match("/public/index.html");
        }
      })
    })
  )
});

如上程式碼,我們設定了兩個變數,第一個變數 CACHE_NAME 是快取的名稱,第二個變數是一個數組,它包含了一份需要儲存的URL列表。

然後我們使用了 cache.addAll()方法,它接收的引數是一個數組,它的作用是把數組裡面的每一項存入快取當中去,當然如果任何一個快取失敗的話,那麼它返回的promise會被拒絕。

我們監聽了install事件後對所有的資原始檔進行了快取後,當用戶處於離線狀態的時候,我們使用fetch的事件來監聽所有的請求。當請求失敗的時候,我們會呼叫catch裡面的函式來匹配所有的請求,它也是返回一個promise物件,當匹配成功後,找到對應的項的時候,直接從快取裡面讀取,如果沒有找到的話,就直接返回 "/public/index.html" 的內容作為代替。為了安全起見,我們在返回"/public/index.html" 之前,我們還進行了一項檢查,該檢查確保請求是包含 text/html的accept的頭部,因此我們就不會返回html內容給其他的請求型別。比如圖片,樣式請求等。

我們之前建立一個html響應的時候,必須將其 Content-Type的值定義為 text/html,方便瀏覽器可以正確的響應識別它是html型別的,那麼為什麼我們這邊沒有定義呢,而直接返回了呢?那是因為我們的 cache.addAll()請求並快取的是一個完整的response物件,該物件不僅包含了響應體,還包含了伺服器返回的任何響應頭。

注意:使用 caches.match(e.request) 來查詢快取中的條目會存在一個陷阱。

比如使用者可能不會總是以相同的url來訪問我們的頁面,比如它會從其他的網站中跳轉到我們的頁面上來,比如後面帶了一些引數,比如:"/public/index.html?id=xxx" 這樣的,也就是說url後面帶了一些查詢字串等這些欄位,如果我們還是和之前一樣進行匹配,是匹配不到的,因此我們需要在match方法中新增一個物件選項;如下所示:

caches.match(e.request, {ignoreSearch: true});

這樣的,通過 ignoreSearch 這個引數,通知match方法來忽略查詢字串。

現在我們先請求下我們的頁面,然後我們勾選離線複選框,再來檢視下我們的頁面效果如下所示:

如上可以看到,我們在離線的狀態下,頁面顯示也是正常的。

回到頂部

7. 理解service worker生命週期

 service worker 的生命週期如下圖所示:

installing(正在安裝)

當我們使用 navigator.serviceWorker.register 註冊一個新的 service worker的時候,javascript程式碼就會被下載、解析、並進入安裝狀態。如果安裝成功的話,service worker 就會進入 installed(已安裝)的狀態。但是如果在安裝過程中出現錯誤,指令碼將被永久進入 redundant(廢棄中)。

installed/waiting(已安裝/等待中)

一旦service worker 安裝成功了,就會進入 installed狀態,一般情況中,會馬上進入 activating(啟用中)狀態。除非另一個正在啟用的 service worker 依然在被控制中。在這種情況下它會維持在 waiting(等待中)狀態。

activating(啟用中)

在service worker啟用並接管應用之前,會觸發 activate 事件。

activated(已啟用)

一旦 service worker 被激活了,它就準備好接管頁面並監聽功能性事件(比如fetch事件)。

redundant(廢棄)

如果service worker在註冊或安裝過程中失敗了,或者被新的版本代替,就會被置為 redundant 狀態,處於這種 service worker將不再對應用產生任何影響。

注意:service worker的狀態和瀏覽器的任何一個視窗或標籤頁都沒有關係的,也就是說 如果service worker是activated(已啟用)狀態的話,它就會保持這個狀態。

回到頂部

8. 理解 service worker 註冊過程

 當我們第一次訪問我們的網站的時候(我們也可以通過刪除service worker後重新整理頁面的方式進行模擬),頁面就會載入我們的main.js 程式碼,如下所示:

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
    console.log("Service Worker registered with scope: ", registration.scope);
  }).catch(function(err) {
    console.log("Service Worker registered failed:", err);
  });
}

那麼我們的應用就會註冊service worker,那麼service worker的檔案將會被下載,然後就開始安裝,install事件就會被觸發,並且在service worker整個生命週期中只會觸發一次,然後觸發了我們的函式,將呼叫的時機記錄在控制檯中。service worker隨後就會進入 installed狀態。然後立即就會變成 activating 狀態。這個時候,我們的另一個函式就會被觸發,因此 activate事件就會把狀態記錄到控制檯中。最後,service worker 進入 activated(已啟用)狀態。現在service worker被啟用狀態了。

但是當我們的service worker 正在安裝的時候,我們的頁面已經開始載入並且渲染了。也就是說 service worker 變成了 active狀態了。因此就不能控制頁面了,只有我們再重新整理頁面的時候,我們的已經被啟用的service worker才能控制頁面。因此我們就可以監聽和操控fetch事件了。

回到頂部

9. 理解更新service worker

 我們首先來修改下 sw.js 程式碼,改成如下:

self.addEventListener("fetch", function(e) {
  if (e.request.url.includes("main.css")) {
    e.respondWith(
      new Response(
        "#app {color:red;}",
        {headers: { "Content-Type": "text/css" }}
      )
    )
  }
});

然後我們再重新整理頁面,發現頁面中的顏色並沒有改變,為什麼呢?但是我們service worker明明控制了頁面,但是頁面為什麼沒有生效?
我們可以開啟chrome開發者工具中 Application -> Service Worker 來理解這段程式碼的含義:如下圖所示:

如上圖所示,頁面註冊了多個service worker,但是隻有一個在控制頁面,舊的service worker是啟用的,而新的service worker仍處於等待狀態。

每當頁面載入一個啟用的service worker,就會檢查 service worker 指令碼的更新。如果檔案在當前的service worker註冊之後發生了修改,新的檔案就會被註冊和安裝。安裝完成後,它並不會替換原先的service worker,而是會保持 waiting 狀態。它將會一直保持等待狀態,直到原先的service worker作用域中的每個標籤和視窗關閉。或者導航到一個不再控制範圍內的頁面。但是我們可以關閉原先的service worker, 那麼原先的service worker 就會變成廢棄狀態。然後我們新的service worker就會被啟用。因此我們可以如下圖所示:

但是如果我們想修改完成後,不結束原來的service worker的話,想改動程式碼,重新整理一下就生效的話,我們需要把 Update on reload這個複選框勾上即可生效,比如如下所示:

注意:

那麼為什麼安裝完成新的service worker 不能實時生效呢?比如說安裝新的service worker不能控制新的頁面呢?原先的service worker 控制原先的頁面呢?為什麼瀏覽器不能跟蹤多個service worker 呢?為什麼所有的頁面都必須由單一的service worker所控制呢?

我們可以設想下如下這麼一個場景,如果我們釋出了一個新版本的service worker,並且該service worker的install事件會從快取中刪除 update.json 該檔案,並新增 update2.json檔案作為代替,並且修改fetch事件,讓其在請求使用者資料的時候,返回新的檔案,如果多個service worker控制了不同的頁面,那麼舊的service worker控制的頁面可能會在快取中搜索舊的 update.json 檔案,但是該檔案又被刪除了,那麼就會導致該應用奔潰。因此我們需要被確保開啟所有的標籤頁或視窗由一個service worker控制的話,就可以避免類似的問題發生。

回到頂部

10. 理解快取管理和清除快取

為什麼需要管理快取呢?

我們首先把我們的sw.js 程式碼改成原先的如下程式碼:

var CACHE_NAME = "cacheName";
var CACHE_URLS = [
  "/public/index.html",
  "/main.css",
  "/public/images/xxx.jpg"
];

self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(CACHE_URLS);
    })
  )
});

// 監聽fetch事件
self.addEventListener("fetch", function(e) {
  e.respondWith(
    fetch(e.request).catch(function() {
      return caches.match(e.request).then(function(response) {
        if (response) {
          return response;
        } else if (e.request.headers.get("accept").includes("text/html")) {
          return caches.match("/public/index.html");
        }
      })
    })
  )
});

如上程式碼,我們的service worker會在安裝階段下載並快取所需要的檔案。如果希望它再次下載並快取這些檔案的話,我們就需要觸發另一個安裝事件。在sw.js中的,我們把 CACHE_NAME 名字改下即可。比如叫 var CACHE_NAME = "cacheName2"; 這樣的。

通過如上給快取名稱新增版本號,並且每次修改檔案時自增它,可以實現兩個目的。

1)修改快取名稱後,瀏覽器就知道安裝新的service worker來代替舊的service worker了,因此會觸發install事件,因此會導致檔案被下載並存儲在快取中。

2)它為每一個版本的service worker都建立了一份單獨的快取。即使我們更新了快取,在使用者關閉所有頁面之前,舊的service worker依然是啟用的。舊的service worker可能會用到快取中的某些檔案,而這些檔案又是可以被新的service worker所修改的,通過讓每個版本的service worker所擁有自己的快取,就可以確保不會出現其他的異常情況。

如何清除快取呢?

caches.delete(cacheName); 該方法接收一個快取名字作為第一個引數,並刪除對應的快取。

caches.keys(); 該方法是獲取所有快取的名稱,並且返回一個promsie物件,其完成的時候會返回一個包含快取名稱的陣列。如下所示:

caches.keys().then(function(cacheNames){
  cacheNames.forEach(function(cacheName){
    caches.delete(cacheName);
  });
});

如何快取管理呢?

在service worker生命週期中,我們需要實現如下目標:
1)每次安裝 service worker,我們都需要建立一份新的快取。
2)當新的service worker啟用的時候,就可以安全刪除過去的service worker 建立的所有快取。

因此我們在現有的程式碼中,新增一個新的事件監聽器,監聽 activate 事件。

self.addEventListener("activate", function(e) {
  e.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (CACHE_NAME !== cacheName && cacheName.startWith("cacheName")) {
            return caches.delete(cacheName);
          }
        })
      )
    })
  )
});
回到頂部

11. 理解重用已快取的響應

如上我們的帶版本號快取已經實現了,為我們提供了一個非常靈活的方式來控制我們的快取,並且保持最新的快取。但是快取內的實現是非常低下的。

比如每次我們建立一個新的快取的時候,我們會使用 cache.add() 或 cache.addAll() 這樣的方法來快取應用需要的所有檔案。但是,如果使用者已經在本地擁有了 cacheName 這個快取的話,那如果這個時候我們建立 cacheName2 這個快取的話,我們發現我們建立的 cacheName2 需要快取的檔案 在 cacheName 已經有了,並且我們發現這些檔案是永遠不會被改變的。如果我們重新快取這些檔案的話,就浪費了寶貴的頻寬和時間從網路再次下載他們。

為了解決如上的問題,我們需要如下做:

如果我們建立一個新的快取,我們首先要遍歷一份不可變的檔案列表,然後從現有的快取中尋找他們,並直接複製到新的快取中。因此我們的sw.js 程式碼變成如下所示:

var CACHE_NAME = "cacheName";
var immutableRequests = [
  "/main.css",
  "/public/images/xxx.jpg"
];

var mutableRequests = [
  "/public/index.html"
];

self.addEventListener("install", function(e) {
  e.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      var newImmutableRequests = [];
      return Promise.all(
        immutableRequests.map(function(url) {
          return caches.match(url).then(function(response) {
            if (response) {
              return cache.put(url, response);
            } else {
              newImmutableRequests.push(url);
              return Promise.resolve();
            }
          });
        })
      ).then(function(){
        return cache.addAll(newImmutableRequests.concat(mutableRequests));
      })
    })
  )
});

如上程式碼。

1)immutableRequests 陣列中包含了我們知道永遠不會改變的資源URL,這些資源可以安全地在快取之間複製。

2)mutableRequests 中包含了每次建立新快取時,我們都需要從網路中請求的url。

如上程式碼,我們的 install 事件會遍歷所有的 immutableRequests,並且在所有現有的快取中尋找他們。如果被找到的話,都會使用cache.put()複製到新的快取中。如果沒有找到該資源的話,會被放入到新的 newImmutableRequests 陣列中。

一旦所有的請求被檢查完畢,程式碼就會使用 cache.addAll()來快取 mutableRequests 和 newImmutableRequests 中所有的URL。

github原始碼檢視