HTTP/1.x 及 Service Worker 快取實踐小結

哈哈哈look發表於2018-12-17

在錯縱複雜的網路環境下,如何將頁面快速得傳遞給使用者是前端們的職責,而在此之後,如何減少網路傳輸的花費同樣值得我們關注。本文以 HTTP/1.x 和 Service Worker 快取兩個方面,就如何減少網路傳輸成本為目標,探討下筆者最近對於快取的實踐,權當拋磚引玉 ?

HTTP 快取

The performance of web sites and applications can be significantly improved by reusing previously fetched resources. Web caches reduce latency and network traffic and thus lessen the time needed to display a representation of a resource. By making use of HTTP caching, Web sites become more responsive.

根據 MDN 定義可知道,快取是對已獲取資源的重新利用,是提升 WEB 效能的重要指標。根據是否和 Server 進行互動,HTTP 快取分為兩類:

  • 強制快取
  • 協商快取

強制快取是無需和 Server 進行互動,直接在 Client 進行快取。 而協商快取需要和 Server 互動來判斷是否重用快取。

HTTP 快取首部有以下幾種:

  • Expires
  • Cache-Control
  • ETag/If-None-Match
  • Last-Modified/If-Modified-Since

Expires

語法:Expires: <http-date>

Expires 通過設定一個時間戳,控制快取的過期時間點。但缺點是客戶端時間和伺服器時間可能不一致,無法保證快取的同步性。

此外,如果存在 Cache-Control 首部並設定了max-age指令,Expires 首部將被忽略。

Cache-Control

語法:`Cache-Control: [public | private | no-cache | only-if-cached],max-age=|s-maxage=|max-stale[=]|min-fresh=][,must-revalidate|proxy-revalidate|immutable][,no-store|no-transform]

具體配置細節見 MDN,屬於強制快取,不再贅述。這裡只講下自己實踐所用到的設定項。

  • public | private
  • max-age=<seconds>
  • no-cache | no-store | must-revalidate

publicprivate 定義了快取的共享性,分為共享(public)與私有(private)快取。共享快取儲存的響應能夠被多個使用者使用,私有快取只能用於單獨使用者。 共享快取可存在於 ISP、閘道器或 CDN 的節點上,能很大程度快取熱門資源,減少網路擁堵與延遲,但存在中間人攻擊的風險,故存在private快取 —— 只快取在使用者的瀏覽器端,不會被共享。可根據自己的業務需求,選擇是私有還是共享的。

max-age=<seconds> 規定了快取時長,以秒為單位。從開始接收到資源為時間點,在接下來的 max-age 時間內使用快取。理論上來說可以長期快取,但帶來的問題是瀏覽器快取的臃腫,根據 RFC2616 最長時常設為一年較為合適,即 Cache-Control: max-age=31536000

no-cacheno-storemust-revalidateno-cache 規定使用快取之前時一定要經過驗證,比如驗證 ETag/ Last-Modified 等; no-store 直接禁止瀏覽器以及所有中間快取儲存任何版本的返回響應,每次使用者請求該資產時,都會向伺服器傳送請求,並下載完整的響應;must-revalidate 快取必須在使用之前驗證舊資源的狀態,並且不可使用過期資源。

ETag/If-None-Match

ETag 是對資源的一個特殊標誌符,能唯一確定資源。語法:

ETag: [W/]"<etag_value>"
複製程式碼

W/表明了資源是否採用弱型別驗證器進行比較,其較為容易生成但不利於比較。"<etag_value>" 是對資源的唯一標誌符,其值是一串 ASCII 字串。生成規則沒有一定的要求,但常採用的生成演算法是內容的 hash 值加上內容的最後修改時間。

當響應頭部包含 ETag 時,下次請求時瀏覽器會自動帶上 If-None-Match: <last_etag_value> 首部,用來驗證資源是否過期。 如果已過期,則以 HTTP 200 返回新的內容響應並帶上新的 ETag。如果資源未過期,則返回 HTTP 304 告知瀏覽器資源未過期可以繼續使用。

Last-Modified/If-Modified-Since

語法:Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

顧名思義,Last-Modified/If-Modified-Since 是根據內容最後的修改時間來判斷是否採用快取的方法。但由於最小時間單位為秒,對於要求時間比較精細的資源可能不太適用。

快取優先順序

HTTP/1.x 快取首部的優先順序: Cache-Control > Expires > ETag/If-None-Match > Last-Modified/If-Modified-Since, 即在同時設定了上述首部時Cache-Control 最高,可根據業務需求設定。

以上,便是 HTTP/1.x 快取設定的首部解釋,可以通過Browser Caching Checker 對瀏覽器快取進行檢查。

Cache Checker

Server Worker 快取

當下時間點,Service Worker 在瀏覽器上的支援度已高達 86.16%, 所以是時候考慮開啟 Service Worker 來加速你的網站了。不僅可以利用 Service Worker 所帶來的快取好處,還能很容易遷移到 PWA,更大程度發掘 Web App 的能力。

不同於 HTTP 快取,Server Worker 不僅能動態快取資源,而且還能提供 offline 模式,對弱網路環境的使用者極為友好。開啟 Service Worker 大概需要註冊、安裝、快取資源、更新和登出等過程。

Server Worker 生命週期

接下來以一個小 Demo 為例,簡單介紹如何開啟一個 Service Worker 服務。原始碼見 sw-cache-example

註冊

註冊流程很簡單,只需要判斷瀏覽器是否支援 Service Worker 特性,並在頁面 Load 之後,註冊 Service Worker 服務,關鍵程式碼:

// sw-reg.js

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('./sw.js').then(
      function(registration) {
        // Registration was successful
        console.log('ServiceWorker registration successful with scope: ', registration.scope)
      },
      function(err) {
        // registration failed :(
        console.log('ServiceWorker registration failed: ', err)
      }
    )
  })
}
複製程式碼

安裝

安裝過程需要做的有:監聽 install 事件,並在其回撥事件內快取資源。

var CACHE_NAME = '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)
    })
  )
})
複製程式碼

響應快取

最重要的一步,就是在資源被快取後利用快取了。需要做的也很簡單:監聽 fetch 事件 -> 對已快取的資源進行響應。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        return response
      }
      return fetch(event.request)
    })
  )
})
複製程式碼

更新

更新也是 Service Worker 很重要的一步,其過程也很易懂:驗證資源是否過期 -> 對過期的資源進行刪除並快取新的資源。

self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1']

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName)
          }
        })
      )
    })
  )
})
複製程式碼

登出

登出只需要拿到 Service Worker 例項,呼叫 unregister 即可。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.ready.then(registration => {
    registration.unregister()
  })
}
複製程式碼

至此,基本完成了 Service Worker 的基本部署,開啟其提供的快取能力。

? 實踐過程中遇到的坑

  1. 遷移 HTTP 請求方法為fetch

由於在響應快取時,需要通過監聽 fetch 事件來響應快取,故需要更改 HTTP 請求方法為 fetch,其 API 參見 MDN。 對於不支援 fetch 的瀏覽器,可以使用這個 fetch 進行打補丁。

  1. 取消 fetch 請求

由於 fetch 沒有提供原生的取消方法,故需要使用 signal 來取消 fetch 請求。

const controller = new AbortController()
const signal = controller.signal

fetch('/some/url', { signal })
  .then(res => res.json())
  .then(data => {
    // do something with "data"
  })
  .catch(err => {
    if (err.name == 'AbortError') {
      return
    }
  })

// controller.abort(); // can be called at any time
複製程式碼

Polyfill 參照 abortcontroller-polyfill

  1. 增加 Service Worker 開關 Service Worker 提供的快取雖然好用,但有時候需要根據業務登出 Service Worker, 這時就需要一個開關來控制。而且應該在第一次部署的時候就增加開關,對於快取進行控制。
fetch(API.switch)
  .then(res => {
    const isOn = res.status
    if (isOn) {
      sw.register()
    } else {
      sw.unregister()
    }
  })
  .catch(err => {
    console.error('fetch sw status error', err)
  })
複製程式碼
  1. 對入口檔案取消快取 對於一般的 SPA,是通過入口檔案進行資源的索引,所以對入口檔案應該不予快取,並要求其強制更新。在使用sw-precache-webpack-plugin應排除入口檔案:
new SWPrecacheWebpackPlugin(
    {
      cacheId: 'my-project-name',
      dontCacheBustUrlsMatching: /\.\w{8}\./,
      filename: 'service-worker.js',
      minify: true,
      navigateFallback: PUBLIC_PATH + 'index.html',
      staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/, /index\.html$/],
    }
  ),
複製程式碼

對入口檔案可以設定 HTTP 響應首部:

Cache-Control: no-cache, no-store, must-revalidate
複製程式碼

其含義是不使用本地及任何中間儲存快取,必須和伺服器取得驗證才能拿到新的內容。

  1. 如果不想自己編寫 Service Worker, 可以參照網上的模板或外掛 ?:

總結

  1. 使用 Cache-Control 對靜態資源進行長期快取,配合 webpack 打包生成的檔案 hash 名,可全部採用這一策略
  2. 使用 ETag/If-None-Match 對內容 hash 進行精確快取
  3. 對於時間要求不精確的資源,使用 Last-Modified/If-Modified-Since 對修改時間對內容進行快取,以替代使用ETag/If-None-Match對 CPU 的高消耗
  4. 使用Service Worker 提供動態快取和離線能力

所以,現在開始開啟除錯工具,為你的網站增加快取吧~ ✌️

Reference

  1. HTTP Caching
  2. Browser Caching Checker
  3. Understanding The Vary Header
  4. ServiceWorker Cache

相關文章