這次帶給大家Webpack如何延遲儲存緩存,Webpack延遲儲存緩存的注意事項有哪些,以下就是實戰案例,一起來看一下。
前言
最近在看webpack 如何做持久化快取的內容,發現其中還是有一些坑點的,正好有時間就將它們整理總結一下,讀完本文你大致能夠明白:
什麼是持久化緩存,為什麼要做持久化緩存?
持久化緩存
首先我們需要去解釋一下,什麼是持久化緩存,在現在前後端分離的應用大行其道的背景下,前端html,css,js 往往是以一種靜態資源檔案的形式存在於伺服器,透過介面來取得資料來展示動態內容。這就牽涉到公司如何部署前端程式碼的問題,所以就牽涉到一個更新部署的問題,是先部署頁面,還是先部署資源? 先部署頁面,再部署資源:在二者部署的時間間隔內,如果有使用者存取頁面,就會在新的頁面結構中載入舊的資源,並且把這個舊版本資源當作新版本快取起來,結果就是:使用者造訪到一個樣式錯亂的頁面,除非手動去刷新,否則在資源快取過期之前,頁面會一直處於錯亂的狀態。 先部署資源,再部署頁面:在部署時間間隔內,有舊版的資源本地快取的使用者造訪網站,由於請求的頁面是舊版本,資源引用沒有改變,瀏覽器將直接使用本機緩存,這樣屬於正常情況,但沒有本地快取或快取過期的使用者在造訪網站的時候,就會出現舊版頁面載入新版本資源的情況,導致頁面執行錯誤。 所以我們需要一種部署策略來保證在更新我們線上的程式碼的時候,線上使用者也能平滑地過渡並且正確地開啟我們的網站。 推薦先看這個回答:大公司怎麼開發和部署前端程式碼? 當你讀完上面的回答,大致就會明白,現在比較成熟的持久化快取方案就是在靜態資源的名字後面加hash 值,因為每次修改檔案產生的hash 值不一樣,這樣做的好處在於增量式發布文件,避免覆蓋掉先前文件從而導致線上的使用者存取失效。 因為只要做到每次發布的靜態資源(css, js, img)的名稱都是獨一無二的,那麼我就可以:webpack 如何做持久化緩存
上面簡單介紹完持久化緩存,下面這個才是重點,那麼我們應該如何在webpack 中進行持久化緩存的呢,我們需要做到以下兩點:保證 hash 值的穩定性,我們需要做到修改某個模組的時候,只有受影響的打包後檔案 hash 值改變,與該模組無關的打包檔案 hash 值不變。
hash 檔名是實現持久化快取的第一步,目前webpack 有兩種計算hash 的方式([hash] 和[chunkhash])
#hash 代表每次webpack 在編譯的過程中會產生唯一的hash 值,在專案中任何一個檔案改變後就會重新創建,然後webpack 計算新的hash 值。
chunkhash 是根據模組計算出來的 hash 值,所以某個檔案的改變只會影響它本身的 hash 值,不會影響其他檔案。
所以如果你只是單純地將所有內容打包成同一個文件,那麼hash 就能夠滿足你了,如果你的專案涉及到拆包,分模組進行加載等等,那你需要用chunkhash,來確保每次更新之後只有相關的檔案hash 值改變。
所以我們在一個具有持久化快取的webpack 配置應該長這樣:
module.exports = { entry: dirname + '/src/index.js', output: { path: dirname + '/dist', filename: '[name].[chunkhash:8].js', } }
上面程式碼的意思是:以index.js 為入口,將所有的程式碼全部打包成一個檔案取名為index.xxxx.js 放到dist 目錄下,現在我們可以在每次更新專案的時候做到產生新命名的檔案了。
如果是應付簡單的場景,這樣做就夠了,但是在大型多頁面應用程式中,我們往往需要對頁面進行效能最佳化:
#分離業務程式碼和第三方的程式碼:之所以將業務程式碼和第三方程式碼分離出來,是因為業務程式碼更新頻率高,而第三方程式碼更新迭代速度慢,所以我們將第三方程式碼(函式庫,框架)進行抽離,這樣可以充分利用瀏覽器的快取來載入第三方函式庫。
按需載入:例如在使用React-Router 的時候,當使用者需要存取到某個路由的時候再去載入對應的元件,那麼使用者就沒有必要在一開始的時候就將所有的路由組件下載到本地。
在多頁面應用程式中,我們往往可以將公共模組進行抽離,例如header, footer 等等,這樣頁面在進行跳轉的時候這些公共模組因為存在於緩存裡,就可以直接進行載入了,而不是再進行網路請求了。
那麼如何進行拆包,分模組進行加載,這需要 webpack 內建外掛:CommonsChunkPlugin,下面我將透過一個例子,來詮釋 webpack 該如何進行配置。
本文的程式碼放在我的Github 上,有興趣的可以下載來看看:
git clone https://github.com/happylindz/blog.git cd blog/code/multiple-page-webpack-demo npm install
閱讀下面的內容之前我強烈建議你看下我之前的文章:深入理解webpack 檔案打包機制,理解webpack 檔案的打包的機制有助於你更好地實現持久化快取。
範例大概是這樣描述的:它由兩個頁面組成pageA 和pageB
// src/pageA.js import componentA from './common/componentA'; // 使用到 jquery 第三方库,需要抽离,避免业务打包文件过大 import $ from 'jquery'; // 加载 css 文件,一部分为公共样式,一部分为独有样式,需要抽离 import './css/common.css' import './css/pageA.css'; console.log(componentA); console.log($.trim(' do something ')); // src/pageB.js // 页面 A 和 B 都用到了公共模块 componentA,需要抽离,避免重复加载 import componentA from './common/componentA'; import componentB from './common/componentB'; import './css/common.css' import './css/pageB.css'; console.log(componentA); console.log(componentB); // 用到异步加载模块 asyncComponent,需要抽离,加载首屏速度 document.getElementById('xxxxx').addEventListener('click', () => { import( /* webpackChunkName: "async" */ './common/asyncComponent.js').then((async) => { async(); }) }) // 公共模块基本长这样 export default "component X";
上面的頁面內容基本上簡單涉及到了我們拆分模組的三種模式:拆分公共庫,按需加載和拆分公共模組。那麼接下來要來設定webpack:
const path = require('path'); const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { entry: { pageA: [path.resolve(dirname, './src/pageA.js')], pageB: path.resolve(dirname, './src/pageB.js'), }, output: { path: path.resolve(dirname, './dist'), filename: 'js/[name].[chunkhash:8].js', chunkFilename: 'js/[name].[chunkhash:8].js' }, module: { rules: [ { // 用正则去匹配要用该 loader 转换的 CSS 文件 test: /.css$/, use: ExtractTextPlugin.extract({ fallback: "style-loader", use: ["css-loader"] }) } ] }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'common', minChunks: 2, }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: ({ resource }) => ( resource && resource.indexOf('node_modules') >= 0 && resource.match(/.js$/) ) }), new ExtractTextPlugin({ filename: `css/[name].[chunkhash:8].css`, }), ] }
第一個CommonsChunkPlugin 用來抽離公共模組,相當於是說webpack 大佬,如果你看到某個模組被載入兩次即以上,那麼請你幫我移到common chunk 裡面,這裡minChunks 為2,粒度拆解最細,你可以根據自己的實際情況,看選擇是用多少次模組才將它們抽離。
第二個 CommonsChunkPlugin 用來提取第三方程式碼,將它們抽離,判斷資源是否來自 node_modules,如果是,則說明是第三方模組,那就將它們抽離。相當於是告訴webpack 大佬,如果你看見某些模組是來自node_modules 目錄的,並且名字是.js 結尾的話,麻煩把他們都移到vendor chunk 裡去,如果vendor chunk 不存在的話,就創建一個新的。
這樣配置有什麼好處,隨著業務的成長,我們依賴的第三方函式庫程式碼很可能會越來越多,如果我們專門配置一個入口來存放第三方程式碼,這時候我們的webpack .config.js 就會變成:
// 不利于拓展 module.exports = { entry: { app: './src/main.js', vendor: [ 'vue', 'axio', 'vue-router', 'vuex', // more ], }, }
第三个 ExtractTextPlugin 插件用于将 css 从打包好的 js 文件中抽离,生成独立的 css 文件,想象一下,当你只是修改了下样式,并没有修改页面的功能逻辑,你肯定不希望你的 js 文件 hash 值变化,你肯定是希望 css 和 js 能够相互分开,且互不影响。
运行 webpack 后可以看到打包之后的效果:
├── css │ ├── common.2beb7387.css │ ├── pageA.d178426d.css │ └── pageB.33931188.css └── js ├── async.03f28faf.js ├── common.2beb7387.js ├── pageA.d178426d.js ├── pageB.33931188.js └── vendor.22a1d956.js
可以看出 css 和 js 已经分离,并且我们对模块进行了拆分,保证了模块 chunk 的唯一性,当你每次更新代码的时候,会生成不一样的 hash 值。
唯一性有了,那么我们需要保证 hash 值的稳定性,试想下这样的场景,你肯定不希望你修改某部分的代码(模块,css)导致了文件的 hash 值全变了,那么显然是不明智的,那么我们去做到 hash 值变化最小化呢?
换句话说,我们就要找出 webpack 编译中会导致缓存失效的因素,想办法去解决或优化它?
影响 chunkhash 值变化主要由以下四个部分引起的:
包含模块的源代码
webpack 用于启动运行的 runtime 代码
webpack 生成的模块 moduleid(包括包含模块 id 和被引用的依赖模块 id)
chunkID
这四部分只要有任意部分发生变化,生成的分块文件就不一样了,缓存也就会失效,下面就从四个部分一一介绍:
一、源代码变化:
显然不用多说,缓存必须要刷新,不然就有问题了
二、webpack 启动运行的 runtime 代码:
看过我之前的文章:深入理解 webpack 文件打包机制 就会知道,在 webpack 启动的时候需要执行一些启动代码。
(function(modules) { window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) { // ... }; function webpack_require(moduleId) { // ... } webpack_require.e = function requireEnsure(chunkId, callback) { // ... script.src = webpack_require.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js"; }; })([]);
大致内容像上面这样,它们是 webpack 的一些启动代码,它们是一些函数,告诉浏览器如何加载 webpack 定义的模块。
其中有一行代码每次更新都会改变的,因为启动代码需要清楚地知道 chunkid 和 chunkhash 值得对应关系,这样在异步加载的时候才能正确地拼接出异步 js 文件的路径。
那么这部分代码最终放在哪个文件呢?因为我们刚才配置的时候最后生成的 common chunk 模块,那么这部分运行时代码会被直接内置在里面,这就导致了,我们每次更新我们业务代码(pageA, pageB, 模块)的时候, common chunkhash 会一直变化,但是这显然不符合我们的设想,因为我们只是要用 common chunk 用来存放公共模块(这里指的是 componentA),那么我 componentA 都没去修改,凭啥 chunkhash 需要变了。
所以我们需要将这部分 runtime 代码抽离成单独文件。
module.exports = { // ... plugins: [ // ... // 放到其他的 CommonsChunkPlugin 后面 new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, }), ] }
这相当于是告诉 webpack 帮我把运行时代码抽离,放到单独的文件中。
├── css │ ├── common.4cc08e4d.css │ ├── pageA.d178426d.css │ └── pageB.33931188.css └── js ├── async.03f28faf.js ├── common.4cc08e4d.js ├── pageA.d178426d.js ├── pageB.33931188.js ├── runtime.8c79fdcd.js └── vendor.cef44292.js
多生成了一个 runtime.xxxx.js,以后你在改动业务代码的时候,common chunk 的 hash 值就不会变了,取而代之的是 runtime chunk hash 值会变,既然这部分代码是动态的,可以通过 chunk-manifest-webpack-plugin 将他们 inline 到 html 中,减少一次网络请求。
三、webpack 生成的模块 moduleid
在 webpack2 中默认加载 OccurrenceOrderPlugin 这个插件,OccurrenceOrderPlugin 插件会按引入次数最多的模块进行排序,引入次数的模块的 moduleId 越小,但是这仍然是不稳定的,随着你代码量的增加,虽然代码引用次数的模块 moduleId 越小,越不容易变化,但是难免还是不确定的。
默认情况下,模块的 id 是这个模块在模块数组中的索引。OccurenceOrderPlugin 会将引用次数多的模块放在前面,在每次编译时模块的顺序都是一致的,如果你修改代码时新增或删除了一些模块,这将可能会影响到所有模块的 id。
最佳实践方案是通过 HashedModuleIdsPlugin 这个插件,这个插件会根据模块的相对路径生成一个长度只有四位的字符串作为模块的 id,既隐藏了模块的路径信息,又减少了模块 id 的长度。
这样一来,改变 moduleId 的方式就只有文件路径的改变了,只要你的文件路径值不变,生成四位的字符串就不变,hash 值也不变。增加或删除业务代码模块不会对 moduleid 产生任何影响。
module.exports = { plugins: [ new webpack.HashedModuleIdsPlugin(), // 放在最前面 // ... ] }
四、chunkID
实际情况中分块的个数的顺序在多次编译之间大多都是固定的, 不太容易发生变化。
这里涉及的只是比较基础的模块拆分,还有一些其它情况没有考虑到,比如异步加载组件中包含公共模块,可以再次将公共模块进行抽离。形成异步公共 chunk 模块。有想深入学习的可以看这篇文章:Webpack 大法之 Code Splitting
webpack 做缓存的一些注意点
CSS 文件 hash 值失效的问题
不建议线上发布使用 DllPlugin 插件
CSS 文件 hash 值失效的问题:
ExtractTextPlugin 有个比较严重的问题,那就是它生成文件名所用的[chunkhash]是直接取自于引用该 css 代码段的 js chunk ;换句话说,如果我只是修改 css 代码段,而不动 js 代码,那么最后生成出来的 css 文件名依然没有变化。
所以我们需要将 ExtractTextPlugin 中的 chunkhash 改为 contenthash,顾名思义,contenthash 代表的是文本文件内容的 hash 值,也就是只有 style 文件的 hash 值。这样编译出来的 js 和 css 文件就有独立的 hash 值了。
module.exports = { plugins: [ // ... new ExtractTextPlugin({ filename: `css/[name].[contenthash:8].css`, }), ] }
如果你使用的是 webpack2,webpack3,那么恭喜你,这样就足够了,js 文件和 css 文件修改都不会影响到相互的 hash 值。那如果你使用的是 webpack1,那么就会出现问题。
具体来讲就是 webpack1 和 webpack 在计算 chunkhash 值得不同:
webpack1 在涉及的时候并没有考虑像 ExtractTextPlugin 会将模块内容抽离的问题,所以它在计算 chunkhash 的时候是通过打包之前模块内容去计算的,也就是说在计算的时候 css 内容也包含在内,之后才将 css 内容抽离成单独的文件,
那么就会出现:如果只修改了 css 文件,未修改引用的 js 文件,那么编译输出的 js 文件的 hash 值也会改变。
对此,webpack2 做了改进,它是基于打包后文件内容来计算 hash 值的,所以是在 ExtractTextPlugin 抽离 css 代码之后,所以就不存在上述这样的问题。如果不幸的你还在使用 webpack1,那么推荐你使用 md5-hash-webpack-plugin 插件来改变 webpack 计算 hash 的策略。
不建议线上发布使用 DllPlugin 插件
为什么这么说呢?因为最近有朋友来问我,他们 leader 不让在线上用 DllPlugin 插件,来问我为什么?
DllPlugin 本身有几个缺点:
首先你需要额外多配置一份 webpack 配置,增加工作量。
其中一个页面用到了一个体积很大的第三方依赖库而其它页面根本不需要用到,但若直接将它打包在 dll.js 里很不值得,每次页面打开都要去加载这段无用的代码,无法使用到 webpack2 的 Code Splitting 功能。
第一次打开的时候需要下载 dll 文件,因为你把很多库全部打在一起了,导致 dll 文件很大,首次进入页面加载速度很慢。
虽然你可以打包成 dll 文件,然后让浏览器去读取缓存,这样下次就不用再去请求,比如你用 lodash 其中一个函数,而你用dll会将整个 lodash 文件打进去,这就会导致你加载无用代码过多,不利于首屏渲染时间。
我认为的正确的姿势是:
像 React、Vue 这样整体性偏强的库,可以生成 vendor 第三方库来去做缓存,因为你一般技术体系是固定的,一个站点里面基本上都会用到统一技术体系,所以生成 vendor 库用于缓存。
像 antd、lodash 这种功能性组件库,可以通过 tree shaking 来进行消除,只保留有用的代码,千万不要直接打到 vendor 第三方库里,不然你将大量执行无用的代码。
結語
好了,感覺我又扯了很多,最近在看webpack 確實收穫不少,希望大家能從文章中也能有所收穫。另外推薦再次推薦我之前寫的文章,能夠更好地幫你理解文件緩存機制:深入理解webpack 文件打包機制
相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!
推薦閱讀:
以上是Webpack如何延遲儲存快取的詳細內容。更多資訊請關注PHP中文網其他相關文章!