前端緩存最佳實踐

前言

緩存,這是一個老生常談的話題,也常被做爲前端面試的一個知識點。css

本文,重點在與探討在實際項目中,如何進行緩存的設置,並給出一個較爲合理的方案。html

強緩存和協商緩存

在介紹緩存的時候,咱們習慣將緩存分爲強緩存和協商緩存兩種。二者的主要區別是使用本地緩存的時候,是否須要向服務器驗證本地緩存是否依舊有效。顧名思義,協商緩存,就是須要和服務器進行協商,最終肯定是否使用本地緩存。前端

兩種緩存方案的問題點

強緩存

咱們知道,強緩存主要是經過http請求頭中的Cache-Control和Expire兩個字段控制。Expire是HTTP1.0標準下的字段,在這裏咱們能夠忽略。咱們重點來討論的Cache-Control這個字段。node

通常,咱們會設置Cache-Control的值爲「public, max-age=xxx」,表示在xxx秒內再次訪問該資源,均使用本地的緩存,再也不向服務器發起請求。react

顯而易見,若是在xxx秒內,服務器上面的資源更新了,客戶端在沒有強制刷新的狀況下,看到的內容仍是舊的。若是說你不着急,能夠接受這樣的,那是否是完美?然而,不少時候不是你想的那麼簡單的,若是發佈新版本的時候,後臺接口也同步更新了,那就gg了。有緩存的用戶還在使用舊接口,而那個接口已經被後臺幹掉了。怎麼辦?webpack

協商緩存

協商緩存最大的問題就是每次都要向服務器驗證一下緩存的有效性,彷佛看起來很省事,無論那麼多,你都要問一下我是否有效。可是,對於一個有追求的碼農,這是不能接受的。每次都去請求服務器,那要緩存還有什麼意義。git

最佳實踐

緩存的意義就在於減小請求,更多地使用本地的資源,給用戶更好的體驗的同時,也減輕服務器壓力。因此,最佳實踐,就應該是儘量命中強緩存,同時,能在更新版本的時候讓客戶端的緩存失效。github

在更新版本以後,如何讓用戶第一時間使用最新的資源文件呢?機智的前端們想出了一個方法,在更新版本的時候,順便把靜態資源的路徑改了,這樣,就至關於第一次訪問這些資源,就不會存在緩存的問題了。web

偉大的webpack可讓咱們在打包的時候,在文件的命名上帶上hash值。面試

entry:{
    main: path.join(__dirname,'./main.js'),
    vendor: ['react', 'antd']
},
output:{
    path:path.join(__dirname,'./dist'),
    publicPath: '/dist/',
    filname: 'bundle.[chunkhash].js'
}
複製代碼

綜上所述,咱們能夠得出一個較爲合理的緩存方案:

  • HTML:使用協商緩存。
  • CSS&JS&圖片:使用強緩存,文件命名帶上hash值。

哈希也有講究

webpack給咱們提供了三種哈希值計算方式,分別是hash、chunkhash和contenthash。那麼這三者有什麼區別呢?

  • hash:跟整個項目的構建相關,構建生成的文件hash值都是同樣的,只要項目裏有文件更改,整個項目構建的hash值都會更改。
  • chunkhash:根據不一樣的入口文件(Entry)進行依賴文件解析、構建對應的chunk,生成對應的hash值。
  • contenthash:由文件內容產生的hash值,內容不一樣產生的contenthash值也不同。

顯然,咱們是不會使用第一種的。改了一個文件,打包以後,其餘文件的hash都變了,緩存天然都失效了。這不是咱們想要的。

那chunkhash和contenthash的主要應用場景是什麼呢?在實際在項目中,咱們通常會把項目中的css都抽離出對應的css文件來加以引用。若是咱們使用chunkhash,當咱們改了css代碼以後,會發現css文件hash值改變的同時,js文件的hash值也會改變。這時候,contenthash就派上用場了。

ETag計算

Nginx

Nginx官方默認的ETag計算方式是爲"文件最後修改時間16進制-文件長度16進制"。例:ETag: 「59e72c84-2404」

Express

Express框架使用了serve-static中間件來配置緩存方案,其中,使用了一個叫etag的npm包來實現etag計算。從其源碼能夠看出,有兩種計算方式:

  • 方式一:使用文件大小和修改時間
function stattag (stat) {
  var mtime = stat.mtime.getTime().toString(16)
  var size = stat.size.toString(16)

  return '"' + size + '-' + mtime + '"'
}
複製代碼
  • 方式二:使用文件內容的hash值和內容長度
function entitytag (entity) {
  if (entity.length === 0) {
    // fast-path empty
    return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'
  }

  // compute hash of entity
  var hash = crypto
    .createHash('sha1')
    .update(entity, 'utf8')
    .digest('base64')
    .substring(0, 27)

  // compute length of entity
  var len = typeof entity === 'string'
    ? Buffer.byteLength(entity, 'utf8')
    : entity.length

  return '"' + len.toString(16) + '-' + hash + '"'
}
複製代碼

ETag與Last-Modified誰優先

協商緩存,有ETag和Last-Modified兩個字段。那當這兩個字段同時存在的時候,會優先以哪一個爲準呢?

在Express中,使用了fresh這個包來判斷是不是最新的資源。主要源碼以下:

function fresh (reqHeaders, resHeaders) {
  // fields
  var modifiedSince = reqHeaders['if-modified-since']
  var noneMatch = reqHeaders['if-none-match']

  // unconditional request
  if (!modifiedSince && !noneMatch) {
    return false
  }

  // Always return stale when Cache-Control: no-cache
  // to support end-to-end reload requests
  // https://tools.ietf.org/html/rfc2616#section-14.9.4
  var cacheControl = reqHeaders['cache-control']
  if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
    return false
  }

  // if-none-match
  if (noneMatch && noneMatch !== '*') {
    var etag = resHeaders['etag']

    if (!etag) {
      return false
    }

    var etagStale = true
    var matches = parseTokenList(noneMatch)
    for (var i = 0; i < matches.length; i++) {
      var match = matches[i]
      if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
        etagStale = false
        break
      }
    }

    if (etagStale) {
      return false
    }
  }

  // if-modified-since
  if (modifiedSince) {
    var lastModified = resHeaders['last-modified']
    var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))

    if (modifiedStale) {
      return false
    }
  }

  return true
}
複製代碼

咱們能夠看到,若是不是強制刷新,並且請求頭帶上了if-modified-since和if-none-match兩個字段,則先判斷etag,再判斷last-modified。固然,若是你不喜歡這種策略,也能夠本身實現一個。

補充:後端須要怎麼設置

上文主要說的是前端如何進行打包,那後端怎麼作呢? 咱們知道,瀏覽器是根據響應頭的相關字段來決定緩存的方案的。因此,後端的關鍵就在於,根據不一樣的請求返回對應的緩存字段。 以nodejs爲例,若是須要瀏覽器強緩存,咱們能夠這樣設置:

res.setHeader('Cache-Control', 'public, max-age=xxx');
複製代碼

若是須要協商緩存,則能夠這樣設置:

res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('Last-Modified', xxx);
res.setHeader('ETag', xxx);
複製代碼

固然,如今已經有不少現成的庫可讓咱們很方便地去配置這些東西。 寫了一個簡單的demo,方便有須要的朋友去了解其中的原理,有興趣的能夠閱讀源碼

總結

在作前端緩存時,咱們儘量設置長時間的強緩存,經過文件名加hash的方式來作版本更新。在代碼分包的時候,應該將一些不常變的公共庫獨立打包出來,使其可以更持久的緩存。

以上,若有錯漏,歡迎指正!

@Author: TDGarden

相關文章
相關標籤/搜索