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


閱讀目錄

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源碼查看


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM