Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/1.x 及 Service Worker 缓存实践小结 #3

Open
chen86860 opened this issue Dec 17, 2018 · 0 comments
Open

HTTP/1.x 及 Service Worker 缓存实践小结 #3

chen86860 opened this issue Dec 17, 2018 · 0 comments
Labels

Comments

@chen86860
Copy link
Owner

在错纵复杂的网络环境下,如何将页面快速得传递给用户是前端们的职责,而在此之后,如何减少网络传输的花费同样值得我们关注。本文以 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
@chen86860 chen86860 added the blog label Dec 17, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant