Webpack Freestyle 之 Long Term Cache

王朕11 7年前發布 | 22K 次閱讀 前端技術 webpack

題圖: Getting started with Fable and Webpack 今天,我們一起來學習如何用 webpack 實現持久性緩存。:clap:

How Browser Cache Works

首先,我們要搞清楚瀏覽器緩存是怎么工作的。 那么,就讓我畫一張圖來告訴大家吧,嘻嘻。:grinning:

  • 瀏覽器: 我需要 foo.js
  • 服務器: 讓我找找。找到了,給你,緩存有效期為 1 年。
  • 瀏覽器: 好,我把他緩存到磁盤里。

過了 2 天,

  • 瀏覽器: 我需要 foo.js ,在緩存里找到了。緩存還有效,那直接讀緩存。
  • 用戶:哇塞,這網頁秒開啊。

又過了 2 天,foo.js 的代碼更新了。(內容從 Hello world 變成了 Goodbye world )

  • 瀏覽器:我需要 foo.js ,在緩存里找到了。緩存還有效,那直接讀緩存。
  • 產品經理:???這頁面怎么跟以前一樣啊?

很尷尬。foo.js 明明更新了,但是瀏覽器還是讀取在緩存中舊的 foo.js ,原因是我們用了緩存,不用緩存就沒這事兒了。:hear_no_evil:

解決辦法嘛,當然有的。比如每次利用緩存之前,先向服務器確認文件是否有更新,有更新則使用新的否則讀緩存。還有一種方法是把緩存破壞掉,也就是下面要說的 Cache Busting Technique 。

Cache Busting Technique

因為 foo.js 的代碼變化了,但是他的緩存還沒失效,此時瀏覽器還是會讀取以前的緩存了的 foo.js ,并不會去服務器下載最新的。這顯然不是我們想要的,怎么辦呢?我們需要破壞緩存( Cache Busting )。

破壞緩存并不是禁止緩存,而是換一種方式讓緩存失效。比如:

  1. 修改文件的名字:foo.js -> foo.v2.js
  2. 修改文件的路徑:/static/foo.js -> /static/v2/foo.js
  3. 加 query string : foo.js -> foo.js?v=qwer

我們下面將采用第一種方法,也就是修改文件的名字。我們把更新后的 foo.js 的文件名改成 foo.v2.js 。這樣,瀏覽器就不會去讀取緩存里的舊的 foo.js ,而是向服務請請求 foo.v2.js ,如下圖所示:

那么,假設我們現在有很多很多的靜態文件,然后每次需要更新很多很多的文件,那是不是要手動地一個一個地修改文件的名字呢?我們的理想當然是:哪個文件更新了,就 自動地 生成一個新的文件名。

另外,如果我們打包出來的靜態文件只有一個單獨的 JavaScript 文件 app.js ,那么每次改動一點代碼,app.js 的文件名肯定都會變。但實際上,我只改動了某個模塊的代碼(其他模塊并沒有修改),就破壞了其他模塊的緩存,這顯然沒有充分利用到緩存啊。我們的想法是: 哪個模塊更新了破壞他的緩存,沒更新的模塊繼續利用緩存 。:+1:

這個時候,我們就需要用到 webpack 的 code splitting(如果還不會的話,可以閱讀 Webpack 大法之 Code Splitting )。把整個 App 分成一個個 chunk ,然后哪個 chunk 發生改變,我就破壞他的緩存;沒有更新的 chunk ,則繼續利用緩存。這樣一來,我們就把緩存的作用發揮到淋漓盡致~

所以,code splitting 的作用除了”減少文件大小”之外,還能更充分地利用緩存。所以,下面就讓我們用 webpack 來實現持久性緩存吧。

Webpack & Caching

首先,把我們的 demo 項目 (已經實現了 code splitting )下載并安裝好依賴。

接著,修改 webpack 配置文件,給我們打包后的靜態文件生成隨機的唯一的名字。( changed files

// webpack.config.js
module.exports = {
  output: {
    //...
    filename: '[name].[chunkhash:8].js',
    chunkFilename: '[name].[chunkhash:8].chunk.js',
    //...
  },
}

我們使用了 [chunkhash] 這個占位符,并且為了更好地分辨和展示 demo ,我們截取了他的前 8 個字符 [chunkhash:8],但是在實際生產中我們不要那么做!

好咯,現在來看看我們的打包后的文件:

Asset       Size  Chunk Names
common-in-lazy.fa79d198.chunk.js    11.6 kB  common-in-lazy
    used-twice.c2c4927c.chunk.js    17.1 kB  used-twice
        Photos.28d663ec.chunk.js    8.57 kB  Photos
         Emoji.d3ea8991.chunk.js    1.15 kB  Emoji
                 app.724a238a.js    2.53 kB  app
              vendor.05be8f94.js     104 kB  vendor

那么,現在我們來修改一下 App.vue ,添加個 <footer> 標簽( changed files ) :

<template>
<div id="app">
  <!-- old codes -->
  <footer> A Footer </footer>
</div>
</template>

此時的打包變成了:

Asset       Size  Chunk Names
common-in-lazy.fa79d198.chunk.js    11.6 kB  common-in-lazy
    used-twice.c2c4927c.chunk.js    17.1 kB  used-twice
        Photos.28d663ec.chunk.js    8.57 kB  Photos
         Emoji.d3ea8991.chunk.js    1.15 kB  Emoji
                 app.fdc2eedb.js    2.57 kB  app
              vendor.b611a5da.js     104 kB  vendor

注意到,我們的 app chunk 的 hash 從 724a238a 變成了 fdc2eedb ,這是我們所希望看到的東西。但是,與此同時 vendor chunk 的 hash 也變了(05be8f94 -> b611a5da)。然而,我們并沒有修改 vendor chunk 的代碼,為什么他的 hash 也變了呢???

原因是 vendor chunk 里面包含了 webpack 的 runtime 代碼(用來解析和加載模塊之類的運行時代碼):

解決辦法就是把 webpack 的 runtime 代碼提取出來( changed files ):

new webpack.optimize.CommonsChunkPlugin({ 
  name: ['manifast'] 
}),

把之前 App.vue 更新了的代碼暫時去掉,也就是上面添加的 <footer> 標簽去掉:

<template>
<div id="app">
  <!-- old codes -->
</div>
</template>

然后看看這個時候的打包:

Asset       Size  Chunk Names
common-in-lazy.fa79d198.chunk.js    11.6 kB  common-in-lazy
    used-twice.c2c4927c.chunk.js    17.1 kB  used-twice
        Photos.28d663ec.chunk.js    8.57 kB  Photos
         Emoji.d3ea8991.chunk.js    1.15 kB  Emoji
                 app.724a238a.js    2.53 kB  app
              vendor.3b70f9d8.js     103 kB  vendor
            manifast.f0563a6f.js    1.54 kB  manifast

不難發現多了一個 manifast chunk ,里面包含著 webpack runtime 代碼:

接著按照之前的,修改 App.vue ,添加 `` 標簽( changed files ):

<template>
<div id="app">
  <!-- old codes -->
  <footer> A Footer </footer>
</div>
</template>

而此時的打包:

Asset       Size  Chunk Names
common-in-lazy.fa79d198.chunk.js    11.6 kB  common-in-lazy
    used-twice.c2c4927c.chunk.js    17.1 kB  used-twice
        Photos.28d663ec.chunk.js    8.57 kB  Photos
         Emoji.d3ea8991.chunk.js    1.15 kB  Emoji
                 app.fdc2eedb.js    2.57 kB  app
              vendor.3b70f9d8.js     103 kB  vendor
            manifast.1442e3f3.js    1.54 kB  manifast

很開心,此時只有 app.js 和 manifast.js 這 2 個 chunk 的文件名的 hash 發生了改變,vendor.js chunk 和其他 chunk 都沒變,舒服。:relieved:

但是 ,假如我們給 App.vue 隨便引入一個模塊的話,比如( changed files ):

<script>
//...
import noop from './shared/utils.js'
</script>

而此時的打包:

Asset       Size  Chunk Names
common-in-lazy.30b1e9b6.chunk.js    11.6 kB  common-in-lazy
    used-twice.9eccbe5a.chunk.js    17.1 kB  used-twice
        Photos.6096611c.chunk.js    8.57 kB  Photos
         Emoji.6208da60.chunk.js    1.15 kB  Emoji
                 app.4675a374.js    2.61 kB  app
              vendor.8b538297.js     103 kB  vendor
            manifast.25580296.js    1.54 kB  manifast

臥槽居然所有 chunk 的 hash 都發生了改變,這是為什么?

原因是在 webpack 里每個模塊都有一個 module id ,module id 是該模塊在 模塊依賴關系圖 里按順序分配的序號,如果這個 module id 發生了變化,那么他的 chunkhash 也會發生變化。(不確定這里是否正確,希望大佬指出錯誤)

假設下圖為我們的 App 的依賴圖在引入一個新模塊 D 的前后比較。可得模塊 B 和 C 的 id 就發生了變化:

所以呢,我們需要用一種新的方式來計算 module id 。 HashedModuleIdsPlugin 這個插件,他是根據模塊所在路徑來映射其 module id ,這樣就算引入了新的模塊,也不會影響 module id 的值,只要模塊的路徑不改變的話。

修改我們的 webpack 配置。并且,去掉上面 App.vue 引入的 noop 模塊。( changed files

// webpack.config.js

plugins: [
  new webpack.HashedModuleIdsPlugin(),
  // ...
],

那么,此時的打包:

Asset       Size  Chunk Names
common-in-lazy.fbe5ebcb.chunk.js    11.9 kB  common-in-lazy
    used-twice.166ea824.chunk.js    17.2 kB  used-twice
        Photos.c2430756.chunk.js    8.66 kB  Photos
         Emoji.96ddcf33.chunk.js     1.2 kB  Emoji
                 app.f0c87e28.js    2.77 kB  app
              vendor.794774d5.js     103 kB  vendor
            manifast.bd440c5c.js    1.54 kB  manifast

來,再次修改我們的的 App.vue ,引入 noop 模塊( changed files ):

<script>
//...
import noop from './shared/utils.js'
</script>

與此同時我們的打包:

Asset       Size  Chunk Names
common-in-lazy.fbe5ebcb.chunk.js    11.9 kB  common-in-lazy
    used-twice.166ea824.chunk.js    17.2 kB  used-twice
        Photos.c2430756.chunk.js    8.66 kB  Photos
         Emoji.96ddcf33.chunk.js     1.2 kB  Emoji
                 app.6dd02fc7.js    2.81 kB  app
              vendor.794774d5.js     103 kB  vendor
            manifast.31b01d25.js    1.54 kB  manifast

可以看到,只有 app chunk 和 manifast chunk 的 hash 發生了改變,其他 chunk 不變所以他們的緩存就沒被破壞。

也就是說, 我修改了某個模塊的代碼,是不會破壞其他模塊的緩存,這就是我們想要實現的持久性緩存,我們做到了 。:tada:

總結一下

用 webpack 實現 long term cache :

  • 生成穩定的 hash 文件名
  • 提取 webpack 的 runtime 代碼
  • code splitting

還有一些東西我們是沒講到的,比如 CSS 的 cache ,內聯 manifast chunk 等等,就留給大家去探索咯。:grimacing:

最后需要注意的是 ,webpack 是允許其他 plugin 來修改 chunkhash 的,如果他們不能正確地處理的話,那么,假設你更新了代碼,但是對應的 chunkhash 沒變,并且此時緩存還沒失效,就會導致線上的代碼還是舊的,用戶看到的還是以前的頁面。因此,一定要特別注意 chunkhash 到底正不正確!!

希望本文可以幫助到大家,這樣我會很開心的。(* *)

當然一定要看的文章咯

* Predictable Long Term Caching with Webpack

* Survivejs - Addding Hashes to Filenames

* Survivejs - Seperating manifest

* Webpack - Caching

 

來自:https://zhuanlan.zhihu.com/p/27710902

 

 本文由用戶 王朕11 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!