您好,登錄后才能下訂單哦!
前言
最近在看 webpack 如何做持久化緩存的內容,發現其中還是有一些坑點的,正好有時間就將它們整理總結一下,讀完本文你大致能夠明白:
持久化緩存
首先我們需要去解釋一下,什么是持久化緩存,在現在前后端分離的應用大行其道的背景下,前端 html,css,js 往往是以一種靜態資源文件的形式存在于服務器,通過接口來獲取數據來展示動態內容。這就涉及到公司如何去部署前端代碼的問題,所以就涉及到一個更新部署的問題,是先部署頁面,還是先部署資源?
先部署頁面,再部署資源:在二者部署的時間間隔內,如果有用戶訪問頁面,就會在新的頁面結構中加載舊的資源,并且把這個舊版本資源當做新版本緩存起來,其結果就是:用戶訪問到一個樣式錯亂的頁面,除非手動去刷新,否則在資源緩存過期之前,頁面會一直處于錯亂的狀態。
先部署資源,再部署頁面:在部署時間間隔內,有舊版本的資源本地緩存的用戶訪問網站,由于請求的頁面是舊版本,資源引用沒有改變,瀏覽器將直接使用本地緩存,這樣屬于正常情況,但沒有本地緩存或者緩存過期的用戶在訪問網站的時候,就會出現舊版本頁面加載新版本資源的情況,導致頁面執行錯誤。
所以我們需要一種部署策略來保證在更新我們線上的代碼的時候,線上用戶也能平滑地過渡并且正確打開我們的網站。
推薦先看這個回答:大公司里怎樣開發和部署前端代碼?
當你讀完上面的回答,大致就會明白,現在比較成熟的持久化緩存方案就是在靜態資源的名字后面加 hash 值,因為每次修改文件生成的 hash 值不一樣,這樣做的好處在于增量式發布文件,避免覆蓋掉之前文件從而導致線上的用戶訪問失效。
因為只要做到每次發布的靜態資源(css, js, img)的名稱都是獨一無二的,那么我就可以:
上面大致介紹了下主流的前端持久化緩存方案,那么我們為什么需要做持久化緩存呢?
用戶使用瀏覽器第一次訪問我們的站點時,該頁面引入了各式各樣的靜態資源,如果我們能做到持久化緩存的話,可以在 http 響應頭加上 Cache-control 或 Expires 字段來設置緩存,瀏覽器可以將這些資源一一緩存到本地。
用戶在后續訪問的時候,如果需要再次請求同樣的靜態資源,且靜態資源沒有過期,那么瀏覽器可以直接走本地緩存而不用再通過網絡請求資源。
webpack 如何做持久化緩存
上面簡單介紹完持久化緩存,下面這個才是重點,那么我們應該如何在 webpack 中進行持久化緩存的呢,我們需要做到以下兩點:
hash 文件名是實現持久化緩存的第一步,目前 webpack 有兩種計算 hash 的方式([hash] 和 [chunkhash])
所以如果你只是單純地將所有內容打包成同一個文件,那么 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 目錄下,現在我們可以在每次更新項目的時候做到生成新命名的文件了。
如果是應付簡單的場景,這樣做就夠了,但是在大型多頁面應用中,我們往往需要對頁面進行性能優化:
那么如何進行拆包,分模塊進行加載,這就需要 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 文件打包機制 就會知道,在 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 值失效的問題:
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 本身有幾個缺點:
雖然你可以打包成 dll 文件,然后讓瀏覽器去讀取緩存,這樣下次就不用再去請求,比如你用 lodash 其中一個函數,而你用dll會將整個 lodash 文件打進去,這就會導致你加載無用代碼過多,不利于首屏渲染時間。
我認為的正確的姿勢是:
結語
好了,感覺我又扯了很多,最近在看 webpack 確實收獲不少,希望大家能從文章中也能有所收獲。另外推薦再次推薦一下我之前寫的文章,能夠更好地幫你理解文件緩存機制:深入理解 webpack 文件打包機制
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。