service worker項目實戰一:離線緩存實現

前言

學習本教程以前,須要對 nodejsnpm 至少有基礎的瞭解,service worker涉及到緩存,因此對http緩存也應該有基礎的瞭解。javascript

關於 service worker 更多的基礎知識能夠參考這兒,本文着重講解離線緩存實現方案。css

本文github地址,若是對service worker有必定的瞭解,能夠直接看具體實現。html

知識點歸納

  • service worker簡介
  • 基於nodejs實現一個簡單的靜態資源服務器
  • 基本的離線緩存實現
  • 離線緩存的更新(重點)
  • 離線緩存和在線服務的結合

service worker簡介

service worker翻譯成中文叫作 服務工做線程,它是一個註冊在指定源和路徑下的事件驅動worker, service worker控制的頁面或者網站可以攔截並修改訪問和資源請求,細粒度地緩存資源,因此用service worker也可以在網絡不可用的狀況下實現離線緩存。java

service worker獨立於主線程以外的worker上下文,所以它不能訪問DOM,因此不會形成阻塞,它設計爲徹底異步,同步API(如XHRlocalStorage)不能在service worker中使用。node

service worker只能由HTTPS承載,畢竟修改網絡請求的能力暴露給中間人攻擊會很是危險。在Firefox瀏覽器的用戶隱私模式,service worker不可用,另外本地環境localhost也能夠用service workerwebpack

項目目錄結構

├── README.md
├── package.json
├── public
│   └── favicon.ico
├── server.js
├── src
│   ├── index.css
│   ├── index.html
│   └── index.js
├── sw
│   └── service-worker.js
└── yarn.lock
複製代碼

新建文件夾,命名爲service-worker。定位到該目錄,執行npm init一路回車便可。 安裝 serve-faviconnodemonexpress:nginx

npm install serve-favicon express nodemon --save-dev
或者:
yarn add serve-favicon express nodemon --dev
複製代碼

而後在項目根目錄下面建立sw src public 目錄,並建立對應的文件。git

基於nodejs實現一個簡單的靜態資源服務器

實際項目中咱們應該使用nginx等來做爲靜態資源服務器,本示例中簡單使用nodejs來實現。github

server.jsweb

const path = require("path");
const express = require("express");
const favicon = require('serve-favicon');
const app = express();
app.get("*", (req, res, next) => {
  console.log(req.url);
  next();
})
app.use(favicon(path.join(__dirname, "public", "favicon.ico")));
const options = {
  setHeaders (res, filePath, stat) {
    res.set({
      "cache-control": "no-cache"
    })
  }
}
app.use(express.static("src", options));
app.use(express.static("sw", {
  maxAge: 0
}))
app.listen("9000", () => {
  console.log("server start at localhost:9000");
})
複製代碼

在server.js文件中,把src目錄下的靜態資源緩存類型設置爲協商緩存,客戶端每次獲取資源都會向服務器驗證文件有效性來確認是否使用本地緩存;sw下面的server-worker.js則設置爲永久性緩存爲0,也就是不緩存,客戶端每次都會向服務器獲取完整的資源,server-worker.js必定不能緩存

在package.json中添加啓動命令

"start": "nodemon server.js"
複製代碼

而後啓動服務器

基本的離線緩存實現

先隨便寫一點內容到index.html和index.css中。

src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>service-worker</title>
  <link rel="stylesheet" href="./index.css">
</head>
<body>
  <div class="box">
    <div>hello service worker v1 </div>
  </div>
  <script src="./index.js"></script>
</body>
</html>
複製代碼

src/index.css

.box {
  text-align: center;
  color: red;
}
複製代碼

而後在index.js中實現service worker的註冊邏輯,主流瀏覽器對service worker的支持度能夠在 caniuse 中查看。

src/index.js

function serviceRegister () {
  const { serviceWorker } = navigator;
  if (!serviceWorker) {
    console.warn("your browser not support serviceWorker");
    return;
  }
  window.addEventListener("load", async () => {
    let sw = null;
    const regisration = await serviceWorker.register("./service-worker.js");
    sw = regisration.installing || regisration.waiting || regisration.active;
    sw && sw.addEventListener("statechange", (e) => {
      const { state } = e.target;
      console.log(state);
    });
  });
}
serviceRegister();
複製代碼

首先從navigator中獲取serviceWorker,而後進行支持度檢測。當瀏覽器資源加載完成以後,調用serviceWorker.register(url, {scope: "xxxx"})註冊server worker,url爲service workder內容的js文件路徑,scope爲service worker的控制域,默認值是基於當前的location也就是 /,能夠自定義修改。

serviceWorker.register函數返回一個regisration,由於worker是異步註冊的,調用register以後不知道處於註冊以後的哪一個階段,因此只能用regisration中的installing、waiting、active去獲取註冊實例,而後給實例添加statechange事件監聽狀態改變。

那當我註冊成功時,怎樣查看我註冊的sw呢?直接打開 chrome://inspect/#service-workers 就能夠查看,在當前瀏覽器中,正在註冊的 SW。另外,還有一個 chrome://serviceworker-internals,用來查看當前瀏覽器中,全部註冊好的sw。另外,在當前頁面下打開chrome控制檯,切換到application選項,下面有個Service Workers二級選項,能夠查看當前的sw狀態,下面的Application Cache能夠查看當前緩存的版本號。

瀏覽器打開控制檯,而後打開localhost:9000,發能夠看到log輸出的sw的週期狀態改變

installed
activating
activated
複製代碼

可見第一次註冊sw時候,sw會走完installed、activating、activated三個週期,在控制檯切換到application/ Service Workers能夠看到當前的sw狀態以下圖:

此時service-worker.js中什麼內容都沒有,咱們往裏面添加一部份內容:

service-worker.js

const _this = this;
const version = "v1";
const cacheFile = [
  "/",
  "/index.html",
  "index.css",
  "index.js"
]
this.addEventListener("install", (event) => { 
  event.waitUntil(
    caches
    .open(version)
    .then((cache) => {
      return cache.addAll(cacheFile)
    })
  )
})
this.addEventListener("fetch", async (event) => {
  const { request } = event;
  event.respondWith(
    caches
      .match(request.clone())
      .catch(() => {
        return fetch(request.clone()).catch()
      })
  );
});
this.addEventListener("activate", (event) => {
  const wihleList = [version];
  event.waitUntil(
    caches
      .keys()
      .then((keyList) => {
        return Promise.all(
          keyList.map((key) => {
            if (!wihleList.includes(key)) {
              return caches.delete(key);
            }
          })
        )
      })
  )
});
複製代碼

在server-worker線程裏面,給當前worker添加install、fetch、activate事件。在install事件回掉函數中添加緩存版本號和具體的緩存文件。fetch事件攔截客戶端請求,咱們首先從緩存中讀取內容,若是緩存中找不到則繼續向服務器請求。activate中對非當前cache version的緩存進行清理。

service-worker修改完成以後,刷新瀏覽器,切換到控制檯能夠看到當前sw狀態如圖:

嗯,大概的意思就是,老的sw還在服役中,頁面的控制權還在老的sw上,新的sw已經安裝完成,可是處於未激活狀態,沒有取得頁面的控制權利。關於sw的更新後面會繼續討論,咱們手動點擊skipWaiting,讓新的sw當即激活,而後取得頁面控制權。

注意:在開發調試過程當中,咱們常常須要在控制檯對sw進行 skipWaiting、unRegister、delete cache等操做,可是你不要指望用戶會這麼作,因此在進行線上版本發佈時候,必定要慎重,緩存搞的錯亂了,用戶加載的內容出錯了,但是嚴重故障...

手動點擊skipWaiting後,看到新的sw已經生效了。刷新瀏覽器,在network選項中能夠看到緩存內容的請求已經被攔截了,從sw緩存中獲取了。在chrome控制檯中,把network狀態改爲offline,再次刷新瀏覽器,嗯,雖然沒網了,可是仍是能夠從本地緩存讀取內容。

至此service-worker離線緩存基本原理算是搞清楚了,基本功能也實現了,可是若是要覺得這就能夠用到生產環境中去,那絕對出大事,由於使用了sw應用的更新但是頗有玄妙的。

離線緩存的更新

加入了service-worker的應用中,更新分爲sw更新和頁面資源更新。

service-worker更新

接着上面的例子,每次service-worker.js更新後,客戶端會從新安裝新的sw,而後等待老的sw控制的頁面都關閉或者整個瀏覽器都關閉,新的sw纔會激活。在sw進程中,提供了skipWaiting 方法,能夠跳過新的sw等待狀態,直接激活,取得做用域下面的頁面控制權。

src/service-worker.js

...
const version = "v2";
...
this.addEventListener("install", (event) => { 
  this.skipWaiting();
  ...
})
複製代碼

添加skipWaiting(),同時把緩存 version 改成v2,把瀏覽器控制檯network改成online,再次刷新瀏覽器。查看控制檯的application,sw已經沒有了skipWaiting狀態,新的sw安裝完成以後當即被激活了。使用skipWaiting以後也會產生一系列問題:

  • 瀏覽器在後臺主動更新的sw,同時sw會緩存新的文件,下一次用戶訪問url時候,直接獲取新的緩存。若是更新sw是由用戶訪問url觸發的,因爲sw註冊和安裝更新是獨立線程異步的,則此時咱們的url打開時候,仍是由老舊的sw控制,因此獲取的也是老舊的資源

修改src/index.html

<div>hello service worker v3 </div>
複製代碼

修改sw/service-worker.js

const version = "v3";
複製代碼

刷新瀏覽器,新的sw已經激活了,但是頁面上仍是hello service worker v1,再次刷新才能是hello service worker v3。

解決方案:註冊service worker時候,添加controllerchange事件,當監聽到新的sw取得頁面控制權時候,主動刷新頁面或者以消息形式通知用戶刷新瀏覽器

很明顯這種方法雖然可行,可是直接被否決,主動刷新頁面太暴力,通知用戶刷新瀏覽器結果不可控。

  • 老的sw和新的sw可能不太同樣,替換以後容易引發不少未知錯誤,這種錯誤不可控且很難復現

  • sw沒有更新,可是資源更新了,用戶訪問url仍是走的老的sw緩存

修改src/index.html

<div>hello service worker v4 </div>
複製代碼

刷新瀏覽器,頁面內容仍是hello service worker v3。

離線緩存和在線服務的結合

鑑於離線緩存更新帶來的各類問題,我思考許久,同時也看了service worker在其餘著名企業的產品上的應用,最終思考出了一個方案:在sw控制的頁面中,優先使用在線資源,sw充當本地和線上服務器的代理服務器角色;當在線資源獲取出錯(服務器宕機,網絡不可用等狀況),則使用sw本地緩存

html文件優先從線上獲取,獲取以後設置緩存,以便下一次獲取html出錯時候再從本地緩存獲取的是上一次獲取的文件。總之,當可以獲取到服務器資源時候,咱們要保證html一直是最新的,由於html裏面的各類js,css資源等,都會添加版本號,因此這些js、css的資源請求也會是正確的對應的版本號,除了html文件以外的資源,咱們優先從本地獲取,本地獲取不到再從線上獲取而且緩存請求,以便下一次使用(實際項目中應該用請求uri和本身的cachefile也就是本身的緩存文件列表進行比較判斷是否該緩存,本地cache storage空間優先,不可能緩存全部的非html請求)。

sw/service-worker.js

function setCache (req, res) {
  caches
    .open(version)
    .then((cache) => {
      cache.put(req, res);
    })
}
this.addEventListener("fetch", async (event) => {
  const { request } = event;
  if (request.headers.get("Accept").indexOf("text/html") !== -1) {
    event.respondWith(
      fetch(request.clone())
      .then((response) => {
        if (response) {
          setCache(request.clone(), response.clone())
          return response.clone();
        }
        return caches.match(request.clone());
      }).catch((e) => {
        return caches.match(request.clone());
      })
    )
    return;
  }
  event.respondWith(
    caches
      .match(request.clone())
      .then((response) => {
        if (response) {
          return response;
        }
        return (
          fetch(request.clone())
          .then((fetchResponse) => {
            // 對於非html資源,實際項目中不該該緩存全部的請求,畢竟本地cache storage有限
            // 應該對請求的資源和cacheFile進行對比,若是匹配則緩存,若是不匹配,
            // 則此處fetch僅僅充當服務請求中轉的做用
            setCache(request.clone(), fetchResponse.clone());
            return fetchResponse.clone();
          })
        );
      }).catch((e) => {
        console.log(e);
      })
  );
});
複製代碼

修改service-worker.js以後,咱們在控制檯把sw給unregister了,而且把cache storage裏面的緩存清理掉,而且把html中的內容改成hello service worker v1,再把service-worker.js中的verson改成v1,而後刷新頁面,至關於用戶第一次訪問url進行sw註冊。

以後就能夠進行各類操做了,好比html內容修改、sw裏面的緩存version修改或者sw其餘內容修改、或者斷網操做,或者本身清除cache storage請求等,每次操做以後刷新瀏覽器,看看效果而且好好思考結果和爲何吧!

注意:

本示例的index.js index.css沒有添加版本號,若是要修改裏面的內容,修改以後得加個版本號,要否則獲取的是緩存中的老舊資源

在開發調試過程當中,咱們常常須要在控制檯對sw進行 skipWaiting、unRegister、delete cache等操做,要在多個場景下去觀察和思考sw的狀態和改變,而且根據本身的業務設計出合理的緩存方案

基本的離線緩存方案實現了,可是僅僅仍是demo階段,實際應用中,好比咱們有三方cdn,並且本身的項目通過諸如webpack等工具打包以後各類資源都帶版本號,每次都手動修改service-worker.js那是不現實的,並且很是容易出錯。serviceWorker結合webpack等工具應用到生產環境上,還有須要作更多的事情。

下一篇將講述 serviceWorker 結合 webpack 自動生成離線緩存應用。

本文github地址,若是有幫助歡迎star,若是您能提出寶貴意見建議或者issue那就更好了。

相關文章
相關標籤/搜索