Webpack Freestyle 之 Long Term Cache

王朕11 7年前
   <p style="text-align:center"><img src="https://simg.open-open.com/show/ffa9af33640871bba2b87a24923fcc91.png"></p>    <p><em>题图: <a href="/misc/goto?guid=4959750366772509225" rel="nofollow,noindex"> Getting started with Fable and Webpack </a> </em> 今天,我们一起来学习如何用 webpack 实现持久性缓存。:clap:</p>    <h2>How Browser Cache Works</h2>    <p>首先,我们要搞清楚浏览器缓存是怎么工作的。 那么,就让我画一张图来告诉大家吧,嘻嘻。:grinning:</p>    <p><img src="https://simg.open-open.com/show/62e7950d4688a15047759317d6df976d.png"></p>    <ul>     <li>浏览器: 我需要 foo.js</li>     <li>服务器: 让我找找。找到了,给你,缓存有效期为 1 年。</li>     <li>浏览器: 好,我把他缓存到磁盘里。</li>    </ul>    <p>过了 2 天,</p>    <p><img src="https://simg.open-open.com/show/6e74d9fcd568e21f8abe07be12e3039b.png"></p>    <ul>     <li>浏览器: 我需要 foo.js ,在缓存里找到了。缓存还有效,那直接读缓存。</li>     <li>用户:哇塞,这网页秒开啊。</li>    </ul>    <p>又过了 2 天,foo.js 的代码更新了。(内容从 Hello world 变成了 Goodbye world )</p>    <p><img src="https://simg.open-open.com/show/0d9614f620ccce8039fb47f836d8f57f.png"></p>    <ul>     <li>浏览器:我需要 foo.js ,在缓存里找到了。缓存还有效,那直接读缓存。</li>     <li>产品经理:???这页面怎么跟以前一样啊?</li>    </ul>    <p>很尴尬。foo.js 明明更新了,但是浏览器还是读取在缓存中旧的 foo.js ,原因是我们用了缓存,不用缓存就没这事儿了。:hear_no_evil:</p>    <p>解决办法嘛,当然有的。比如每次利用缓存之前,先向服务器确认文件是否有更新,有更新则使用新的否则读缓存。还有一种方法是把缓存破坏掉,也就是下面要说的 Cache Busting Technique 。</p>    <h2>Cache Busting Technique</h2>    <p>因为 foo.js 的代码变化了,但是他的缓存还没失效,此时浏览器还是会读取以前的缓存了的 foo.js ,并不会去服务器下载最新的。这显然不是我们想要的,怎么办呢?我们需要破坏缓存( Cache Busting )。</p>    <p>破坏缓存并不是禁止缓存,而是换一种方式让缓存失效。比如:</p>    <ol>     <li>修改文件的名字:foo.js -> foo.v2.js</li>     <li>修改文件的路径:/static/foo.js -> /static/v2/foo.js</li>     <li>加 query string : foo.js -> foo.js?v=qwer</li>    </ol>    <p>我们下面将采用第一种方法,也就是修改文件的名字。我们把更新后的 foo.js 的文件名改成 foo.v2.js 。这样,浏览器就不会去读取缓存里的旧的 foo.js ,而是向服务请请求 foo.v2.js ,如下图所示:</p>    <p><img src="https://simg.open-open.com/show/f17f0163d7629bbc4cefa5cbe90efe81.png"></p>    <p>那么,假设我们现在有很多很多的静态文件,然后每次需要更新很多很多的文件,那是不是要手动地一个一个地修改文件的名字呢?我们的理想当然是:哪个文件更新了,就 <strong>自动地</strong> 生成一个新的文件名。</p>    <p>另外,如果我们打包出来的静态文件只有一个单独的 JavaScript 文件 app.js ,那么每次改动一点代码,app.js 的文件名肯定都会变。但实际上,我只改动了某个模块的代码(其他模块并没有修改),就破坏了其他模块的缓存,这显然没有充分利用到缓存啊。我们的想法是: <strong>哪个模块更新了破坏他的缓存,没更新的模块继续利用缓存</strong> 。:+1:</p>    <p>这个时候,我们就需要用到 webpack 的 code splitting(如果还不会的话,可以阅读 <a href="/misc/goto?guid=4959750366866498608" rel="nofollow,noindex">Webpack 大法之 Code Splitting</a> )。把整个 App 分成一个个 chunk ,然后哪个 chunk 发生改变,我就破坏他的缓存;没有更新的 chunk ,则继续利用缓存。这样一来,我们就把缓存的作用发挥到淋漓尽致~</p>    <p>所以,code splitting 的作用除了”减少文件大小”之外,还能更充分地利用缓存。所以,下面就让我们用 webpack 来实现持久性缓存吧。</p>    <h2>Webpack & Caching</h2>    <p>首先,把我们的 <a href="/misc/goto?guid=4959750366950513781" rel="nofollow,noindex"> demo 项目 </a> (已经实现了 code splitting )下载并安装好依赖。</p>    <p>接着,修改 webpack 配置文件,给我们打包后的静态文件生成随机的唯一的名字。( <a href="/misc/goto?guid=4959750367036624781" rel="nofollow,noindex"> changed files </a> )</p>    <pre>  <code class="language-javascript">// webpack.config.js  module.exports = {    output: {      //...      filename: '[name].[chunkhash:8].js',      chunkFilename: '[name].[chunkhash:8].chunk.js',      //...    },  }</code></pre>    <p>我们使用了 [chunkhash] 这个占位符,并且为了更好地分辨和展示 demo ,我们截取了他的前 8 个字符 [chunkhash:8],但是在实际生产中我们不要那么做!</p>    <p>好咯,现在来看看我们的打包后的文件:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>那么,现在我们来修改一下 App.vue ,添加个 <footer> 标签( <a href="/misc/goto?guid=4959750367130767369" rel="nofollow,noindex"> changed files </a> ) :</p>    <pre>  <code class="language-javascript"><template>  <div id="app">    <!-- old codes -->    <footer> A Footer </footer>  </div>  </template></code></pre>    <p>此时的打包变成了:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>注意到,我们的 app chunk 的 hash 从 724a238a 变成了 fdc2eedb ,这是我们所希望看到的东西。但是,与此同时 vendor chunk 的 hash 也变了(05be8f94 -> b611a5da)。然而,我们并没有修改 vendor chunk 的代码,为什么他的 hash 也变了呢?��</p>    <p>原因是 vendor chunk 里面包含了 webpack 的 runtime 代码(用来解析和加载模块之类的运行时代码):</p>    <p><img src="https://simg.open-open.com/show/ecdd46e6c38c49c466827258a4ae0431.png"></p>    <p>解决办法就是把 webpack 的 runtime 代码提取出来( <a href="/misc/goto?guid=4959750367210268597" rel="nofollow,noindex"> changed files </a> ):</p>    <pre>  <code class="language-javascript">new webpack.optimize.CommonsChunkPlugin({     name: ['manifast']   }),</code></pre>    <p>把之前 App.vue 更新了的代码暂时去掉,也就是上面添加的 <footer> 标签去掉:</p>    <pre>  <code class="language-javascript"><template>  <div id="app">    <!-- old codes -->  </div>  </template></code></pre>    <p>然后看看这个时候的打包:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>不难发现多了一个 manifast chunk ,里面包含着 webpack runtime 代码:</p>    <p>接着按照之前的,修改 App.vue ,添加 `` 标签( <a href="/misc/goto?guid=4959750367302442034" rel="nofollow,noindex"> changed files </a> ):</p>    <pre>  <code class="language-javascript"><template>  <div id="app">    <!-- old codes -->    <footer> A Footer </footer>  </div>  </template></code></pre>    <p>而此时的打包:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>很开心,此时只有 app.js 和 manifast.js 这 2 个 chunk 的文件名的 hash 发生了改变,vendor.js chunk 和其他 chunk 都没变,舒服。:relieved:</p>    <p><strong>但是</strong> ,假如我们给 App.vue 随便引入一个模块的话,比如( <a href="/misc/goto?guid=4959750367384763532" rel="nofollow,noindex"> changed files </a> ):</p>    <pre>  <code class="language-javascript"><script>  //...  import noop from './shared/utils.js'  </script></code></pre>    <p>而此时的打包:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>卧槽居然所有 chunk 的 hash 都发生了改变,这是为什么?</p>    <p>原因是在 webpack 里每个模块都有一个 module id ,module id 是该模块在 <a href="/misc/goto?guid=4959750367473616626" rel="nofollow,noindex"> 模块依赖关系图 </a> 里按顺序分配的序号,如果这个 module id 发生了变化,那么他的 chunkhash 也会发生变化。(不确定这里是否正确,希望大佬指出错误)</p>    <p>假设下图为我们的 App 的依赖图在引入一个新模块 D 的前后比较。可得模块 B 和 C 的 id 就发生了变化:</p>    <p><img src="https://simg.open-open.com/show/58bdba63e23baf7f5b22bc30cca17dd3.png"></p>    <p>所以呢,我们需要用一种新的方式来计算 module id 。 HashedModuleIdsPlugin 这个插件,他是根据模块所在路径来映射其 module id ,这样就算引入了新的模块,也不会影响 module id 的值,只要模块的路径不改变的话。</p>    <p>修改我们的 webpack 配置。并且,去掉上面 App.vue 引入的 noop 模块。( <a href="/misc/goto?guid=4959750367559610873" rel="nofollow,noindex"> changed files </a> )</p>    <pre>  <code class="language-javascript">// webpack.config.js    plugins: [    new webpack.HashedModuleIdsPlugin(),    // ...  ],</code></pre>    <p>那么,此时的打包:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>来,再次修改我们的的 App.vue ,引入 noop 模块( <a href="/misc/goto?guid=4959750367650848302" rel="nofollow,noindex"> changed files </a> ):</p>    <pre>  <code class="language-javascript"><script>  //...  import noop from './shared/utils.js'  </script></code></pre>    <p>与此同时我们的打包:</p>    <pre>  <code class="language-javascript">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</code></pre>    <p>可以看到,只有 app chunk 和 manifast chunk 的 hash 发生了改变,其他 chunk 不变所以他们的缓存就没被破坏。</p>    <p>也就是说, <strong>我修改了某个模块的代码,是不会破坏其他模块的缓存,这就是我们想要实现的持久性缓存,我们做到了</strong> 。:tada:</p>    <h2>总结一下</h2>    <p>用 webpack 实现 long term cache :</p>    <ul>     <li>生成稳定的 hash 文件名</li>     <li>提取 webpack 的 runtime 代码</li>     <li>code splitting</li>    </ul>    <p>还有一些东西我们是没讲到的,比如 CSS 的 cache ,内联 manifast chunk 等等,就留给大家去探索咯。:grimacing:</p>    <p><strong>最后需要注意的是</strong> ,webpack 是允许其他 plugin 来修改 chunkhash 的,如果他们不能正确地处理的话,那么,假设你更新了代码,但是对应的 chunkhash 没变,并且此时缓存还没失效,就会导致线上的代码还是旧的,用户看到的还是以前的页面。因此,一定要特别注意 chunkhash 到底正不正确!!</p>    <p>希望本文可以帮助到大家,这样我会很开心的。(* *)</p>    <h2>当然一定要看的文章咯</h2>    <p>* <a href="/misc/goto?guid=4959750367731421347" rel="nofollow,noindex"> Predictable Long Term Caching with Webpack </a></p>    <p>* <a href="/misc/goto?guid=4959750367813269169" rel="nofollow,noindex"> Survivejs - Addding Hashes to Filenames </a></p>    <p>* <a href="/misc/goto?guid=4959750367897216458" rel="nofollow,noindex"> Survivejs - Seperating manifest </a></p>    <p>* <a href="/misc/goto?guid=4959750367988339845" rel="nofollow,noindex"> Webpack - Caching </a></p>    <p> </p>    <p>来自:https://zhuanlan.zhihu.com/p/27710902</p>    <p> </p>