漸進式web應用開發--擁抱離線優先(三)


閱讀目錄

一:什么是離線優先?

傳統的web應用完全依賴於服務器端,比如像很早以前jsp,php,asp時代,所有的數據,內容和應用邏輯都在服務器端,客戶端僅僅做一些html內容渲染到頁面上去。但是隨着技術在不斷的改變,現在很多業務邏輯也放在前端,前后端分離,前端是做模板渲染工作,后端只做業務邏輯開發,只提供數據接口。但是我們的web前端開發在數據層這方面來講還是依賴於服務器端。如果網絡中斷或服務器接口掛掉了,都會影響數據頁面展示。因此我們需要使用離線優先這個技術來更優雅的處理這個問題。

擁抱離線優先的真正含義是:盡管應用程序的某些功能在用戶離線時可能不能正常使用,但是更多的功能應該保持可用狀態。

離線優先它可以優雅的處理這些異常情況下問題,當用戶離線時,用戶正在查看數據可能是之前的數據,但是仍然可以訪問之前的頁面,之前的數據不會丟失,這就意味着用戶可以放心使用某些功能。那么要做到離線時候還可以訪問,就需要我們緩存哦。

二:常用的緩存模式

在為我們的網站使用緩存之前,我們需要先熟悉一些常見的緩存設計模式。如果我們要做一個股票K線圖的話,因為股票數據是實時更新的,因此我們需要實時的去請求網絡最新的數據(當然實時肯定使用websocket技術,而不是http請求,我這邊是假如)。只有當網絡請求失敗的時候,我們再從緩存里面去讀取數據。但是對於股票K線圖中的一些圖標展示這樣的,因為這些圖標是一般不會變的,所以我們更傾向於使用緩存里面的數據。只有在緩存里面找不到的情況下,再從網絡上請求數據。

所以有如下幾種緩存模式:

1. 僅緩存
2. 緩存優先,網絡作為回退方案。
3. 僅網絡。
4. 網絡優先,緩存作為回退方案。
5. 網絡優先,緩存作為回退方案, 通用回退。

1. 僅緩存

什么是僅緩存呢?僅緩存是指 從緩存中響應所有的資源請求,如果緩存中找不到的話,那么請求就會失敗。那么僅緩存對於靜態資源是實用的。因為靜態資源一般是不會發生變化,比如圖標,或css樣式等這些,當然如果css樣式發生改變的話,在后綴可以加上時間戳這樣的。比如 base.css?t=20191011 這樣的,如果時間戳沒有發生改變的話,那么我們直接從緩存里面讀取。
因此我們的 sw.js 代碼可以寫成如下(注意:該篇文章是在上篇文章基礎之上的,如果想看上篇文章,請點擊這里

self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.match(event.request)
  )
});

如上代碼,直接監聽 fetch事件,該事件能監聽到頁面上所有的請求,當有請求過來的時候,它使用緩存里面的數據依次去匹配當前的請求,如果匹配到了,就拿緩存里面的數據,如果沒有匹配到,則請求失敗。

2. 緩存優先,網絡作為回退方案

該模式是:先從緩存里面讀取數據,當緩存里面沒有匹配到數據的時候,service worker才會去請求網絡並返回。

代碼變成如下:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  )
});

如上代碼,使用fetch去監聽所有請求,然后先使用緩存依次去匹配請求,不管是匹配成功還是匹配失敗都會進入then回調函數,當匹配失敗的時候,我們的response值就為 undefined,如果為undefined的話,那么就網絡請求,否則的話,從拿緩存里面的數據。

3. 僅網絡

傳統的web模式,就是這種模式,從網絡里面去請求,如果網絡不通,則請求失敗。因此代碼變成如下:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request)
  )
});

4. 網絡優先,緩存作為回退方案。

先從網絡發起請求,如果網絡請求失敗的話,再從緩存里面去匹配數據,如果緩存里面也沒有找到的話,那么請求就會失敗。

因此代碼如下:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request);
    })
  )
});

5. 網絡優先,緩存作為回退方案, 通用回退

該模式是先請求網絡,如果網絡失敗的話,則從緩存里面讀取,如果緩存里面讀取失敗的話,我們提供一個默認的顯示給頁面展示。

比如顯示一張圖片。如下代碼:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request).then(function(response) {
        return response || caches.match("/xxxx.png");
      })
    });
  )
});

三:混合與匹配,創造新模式

上面是我們五種緩存模式。下面我們需要將這些模式要組合起來使用。

1. 緩存優先,網絡作為回退方案, 並更新緩存。

對於不經常改變的資源,我們可以先緩存優先,網絡作為回退方案,第一次請求完成后,我們把請求的數據緩存起來,下次再次執行的時候,我們先從緩存里面讀取。

因此代碼如下:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.open("cache-name").then(function(cache) {
      return cache.match(event.request).then(function(cachedResponse){
        return cachedResponse || fetch(event.request).then(function(networkResponse){
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
      })
    })
  )
});

如上代碼,我們首先打開緩存,然后使用請求匹配緩存,不管匹配成功了還是匹配失敗了,都會進入then回調函數,如果匹配到了,說明緩存里面有對應的數據,那么直接從緩存里面返回,如果緩存里面 cachedResponse 值為undefined,沒有的話,那么就重新使用fetch請求網絡,然后把請求的數據 networkResponse 重新返回回來,並且克隆一份 networkResponse 放入緩存里面去。

2. 網絡優先,緩存作為回退方案,並頻繁更新緩存

如果一些經常要實時更新的數據的話,比如百度上的一些實時新聞,那么都需要對網絡優先,緩存作為回退方案來做,那么該模式下首先會從網絡中獲取最新版本,當網絡請求失敗的時候才回退到緩存版本,當網絡請求成功的時候,它會將當前返回最新的內容重新賦值給緩存里面去。這樣就保證緩存永遠是上一次請求成功的數據。即使網絡斷開了,還是會使用之前最新的數據的。

因此代碼可以變成如下:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.open("cache-name").then(function(cache) {
      return fetch(event.request).then(function(networkResponse) {
        cache.put(event.request, networkResponse.clone());
        return networkResponse;
      }).catch(function() {
        return caches.match(event.request);
      });
    })
  )
});

如上代碼,我們使用fetch事件監聽所有的請求,然后打開緩存后,我們先請網絡請求,請求成功后,返回最新的內容,此時此刻同時把該返回的內容克隆一份放入緩存里面去。但是當網絡異常的情況下,我們就匹配緩存里面最新的數據。但是在這種情況下,如果我們第一次網絡請求失敗后,由於第一次我們沒有做緩存,因此緩存也會失敗,最后就會顯示失敗的頁面了。

3. 緩存優先,網絡作為回退方案,並頻繁更新緩存

對於一些經常改變的資源文件,我們可以先緩存優先,然后再網絡作為回退方案,也就是說先緩存里面找到,也總會從網絡上請求資源,這種模式可以先使用緩存快速響應頁面,同時會重新請求來獲取最新的內容來更新緩存,在我們用戶下次請求該資源的時候,那么它就會拿到緩存里面最新的數據了,這種模式是將快速響應和最新的響應模式相結合。

因此我們的代碼改成如下:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open("cache-name").then(function(cache) {
      return cache.match(event.request).then(function(cachedResponse) {
        var fetchPromise = fetch(event.request).then(function(networkResponse) {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return cachedResponse || fetchPromise;
      });
    })
  )
});

如上代碼,我們首先打開一個緩存,然后我們試圖匹配請求,不管是否匹配成功,我們都會進入then函數,在該回調函數內部,會先重新請求一下,請求成功后,把最新的內容返回回來,並且以此同時把該請求數的數據克隆一份出來放入緩存里面去。最后把請求的資源文件返回保存到 fetchPromise 該變量里面,最后我們先返回緩存里面的數據,如果緩存里面沒有數據,我們再返回網絡fetchPromise 返回的數據。

如上就是我們3種常見的模式。下面我們就需要來規划我們的緩存策略了。

四:規划緩存策略

在我們之前講解的demo中(https://www.cnblogs.com/tugenhua0707/p/11148968.html), 都是基於網絡優先,緩存作為回退方案模式的。我們之前使用這個模式給用戶體驗還是挺不錯的,首先先請求網絡,當網絡斷開的時候,我們從緩存里面拿到數據。
這樣就不會使頁面異常或空白。但是上面我們已經了解到了緩存了,我們可以再進一步優化了。

我們現在可以使用離線優先的方式來構建我們的應用程序了,對應我們項目經常會改變的資源我們優先使用網絡請求,如果網絡不可以用的話,我們使用緩存里面的數據。

首先還是看下我們項目的整個目錄結構如下:

|----- 項目
|  |--- 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

我們的首頁 index.html 代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>service worker 實列</title>
  <link rel="stylesheet" href="/main.css" />
</head>
<body>
  <div id="app">22222</div>
  <img src="/public/images/xxx.jpg" />
  <script type="text/javascript" src="/main.js"></script>
</body>
</html>

首頁是由靜態的index.html 組成的,它一般很少會隨着版本的改變而改變的,它頁面中會請求多個圖片,請求多個css樣式,和請求多個js文件。在index.html中所有的靜態資源文件(圖片、css、js)等在我們的service worker安裝過程中會緩存下來的,那么這些資源文件適合的是 "緩存優先,網絡作為回退方案" 模式來做。這樣的話,頁面加載會更快。

但是index.html呢?這個頁面一般情況下很少改變,我們一般會想到 "緩存優先,網絡作為回退方案" 來考慮,但是如果該頁面也改動了代碼呢?我們如果一直使用緩存的話,那么我們就得不到最新的代碼了,如果我們想我們的index.html拿到最新的數據,我們不得不重新更新我們的service worker,來獲取最新的緩存文件。但是我們從之前的知識點我們知道,在我們舊的service worker 釋放頁面的同時,新的service worker被激活之前,頁面也不是最新的版本的。必須要等第二次重新刷新頁面的時候才會看到最新的頁面。那么我們的index.html頁面要如何做呢?

1) 如果我們使用 "緩存優先,網絡作為回退方案" 模式來提供服務的話,那么這樣做的話,當我們改變頁面的時候,它就有可能不會使用最新版本的頁面。

2)如果我們使用 "網絡優先,緩存作為回退方案 " 模式來做的話,這樣確實可以通過請求來顯示最新的頁面,但是這樣做也有缺點,比如我們的index.html頁面沒有改過任何東西的話,也要從網絡上請求,而不是從緩存里面讀取,導致加載的時間會慢一點。

3) 使用 緩存優先,網絡作為 回退方案,並頻繁更新緩存模式。該模式總是從緩存里面讀取 index.html頁面,那么它的響應時間相對來說是非常快的,並且從緩存里面讀取頁面后,我們同時會請求下,然后返回最新的數據,我們把最新的數據來更新緩存,因此我們下一次進來頁面的時候,會使用最新的數據。

因此對於我們的index.html頁面,我們適合使用第三種方案來做。

因此對於我們這個簡單的項目來講,我們可以總結如下:

1. 使用 "緩存優先,網絡作為回退方案,並頻繁更新緩存" 模式來返回index.html文件。
2. 使用 "緩存優先,網絡作為回退方案" 來返回首頁需要的所有靜態文件。

因此我們可以使用上面兩點,來實現我們的緩存策略。

五:實現緩存策略

現在我們來更新下我們的 sw.js 文件,該文件來緩存我們index.html,及在index.html使用到的所有靜態資源文件。

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>

js/main.js 代碼變為如下:

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

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

sw.js 代碼變成如下:

var CACHE_NAME = "cacheName";

var CACHE_URLS = [
  "/public/index.html",      // html文件
  "/main.css",               // css 樣式表
  "/public/images/xxx.jpg",  // 圖片
  "/main.js"                 // js 文件 
];

// 監聽 install 事件,把所有的資源文件緩存起來
self.addEventListener("install", function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(CACHE_URLS);
    })
  )
});

// 監聽fetch事件,監聽所有的請求

self.addEventListener("fetch", function(event) {
  var requestURL = new URL(event.request.url);
  console.log(requestURL);
  if (requestURL.pathname === '/' || requestURL.pathname === "/index.html") {
    event.respondWith(
      caches.open(CACHE_NAME).then(function(cache) {
        return cache.match("/index.html").then(function(cachedResponse) {
          var fetchPromise = fetch("/index.html").then(function(networkResponse) {
            cache.put("/index.html", networkResponse.clone());
            return networkResponse;
          });
          return cachedResponse || fetchPromise;
        })
      })
    )
  } else if (CACHE_URLS.includes(requestURL.href) || CACHE_URLS.includes(requestURL.pathname)) {
    event.respondWith(
      caches.open(CACHE_NAME).then(function(cache) {
        return cache.match(event.request).then(function(response) {
          return response || fetch(event.request);
        });
      })
    )
  } 
});

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

如上代碼中的fetch事件,var requestURL = new URL(event.request.url);console.log(requestURL); 打印信息如下所示:

如上我們使用了 new URL(event.request.url) 來決定如何處理不同的請求。且可以獲取到不同的屬性,比如host, hostname, href, origin 等這樣的信息到。

如上我們監聽 fetch 事件中所有的請求,判斷 requestURL.pathname 是否是 "/" 或 "/index.html", 如果是index.html 頁面的話,對於 index.html 的來說,使用上面的原則是:使用 "緩存優先,網絡作為回退方案,並頻繁更新緩存", 所以如上代碼,我們首先打開我們的緩存,然后使用緩存匹配 "/index.html",不管匹配是否成功,都會進入then回調函數,然后把緩存返回,在該函數內部,我們會重新請求,把請求最新的內容保存到緩存里面去,也就是說更新我們的緩存。當我們第二次訪問的時候,使用的是最新緩存的內容。

如果我們請求的資源文件不是 index.html 的話,我們接着會判斷下,CACHE_URLS 中是否包含了該資源文件,如果包含的話,我們就從緩存里面去匹配,如果緩存沒有匹配到的話,我們會重新請求網絡,也就是說我們對於頁面上所有靜態資源文件話,使用 "緩存優先,網絡作為回退方案" 來返回首頁需要的所有靜態文件。

因此我們現在再來訪問我們的頁面的話,如下所示:

如上所示,我們可以看到,我們第一次請求的時候,加載index.html 及 其他的資源文件,我們可以從上圖可以看到 加載時間的毫秒數,雖然從緩存里面讀取第一次數據后,但是由於我們的index.html 總是會請求下,把最新的資源再返回回來,然后更新緩存,因此我們可以看到我們第二次加載index.html 及 所有的service worker中的資源文件,可以看到第二次的加載時間更快,並且當我們修改我們的index.html 后,我們刷新下頁面后,第一次還是從緩存里面讀取最新的數據,當我們第二次刷新的時候,頁面才會顯示我們剛剛修改的index.html頁面的最新頁面了。因此就驗證了我們之前對於index.html 處理的邏輯。

使用 緩存優先,網絡作為 回退方案,並頻繁更新緩存模式。該模式總是從緩存里面讀取 index.html頁面,那么它的響應時間相對來說是非常快的,並且從緩存里面讀取頁面后,我們同時會請求下,然后返回最新的數據,我們把最新的數據來更新緩存,因此我們下一次進來頁面的時候,會使用最新的數據。
github簡單的demo


免責聲明!

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



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