這篇文章主要介紹了淺談Webpack 持久化快取實踐,現在分享給大家,也給大家做個參考。
前言
最近在看webpack 如何做持久化快取的內容,發現其中還是有一些坑點的,正好有時間就將它們整理總結一下,讀完本文你大致能夠明白:
什麼是持久化緩存,為什麼要做持久化緩存?
持久化緩存
首先我們需要去解釋一下,什麼是持久化緩存,在現在前後端分離的應用大行其道的背景下,前端html,css,js 往往是以一種靜態資源檔案的形式存在於伺服器,透過介面來取得資料來展示動態內容。這就牽涉到公司如何部署前端程式碼的問題,所以就牽涉到一個更新部署的問題,是先部署頁面,還是先部署資源? 先部署頁面,再部署資源:在二者部署的時間間隔內,如果有使用者存取頁面,就會在新的頁面結構中載入舊的資源,並且把這個舊版本資源當作新版本快取起來,結果就是:使用者造訪到一個樣式錯亂的頁面,除非手動去刷新,否則在資源快取過期之前,頁面會一直處於錯亂的狀態。 先部署資源,再部署頁面:在部署時間間隔內,有舊版的資源本地快取的使用者造訪網站,由於請求的頁面是舊版本,資源引用沒有改變,瀏覽器將直接使用本機緩存,這樣屬於正常情況,但沒有本地快取或快取過期的使用者在造訪網站的時候,就會出現舊版頁面載入新版本資源的情況,導致頁面執行錯誤。 所以我們需要一種部署策略來保證在更新我們線上的程式碼的時候,線上使用者也能平滑地過渡並且正確地開啟我們的網站。 推薦先看這個回答:大公司怎麼開發和部署前端程式碼? 當你讀完上面的回答,大致就會明白,現在比較成熟的持久化快取方案就是在靜態資源的名字後面加hash 值,因為每次修改檔案產生的hash 值不一樣,這樣做的好處在於增量式發布文件,避免覆蓋掉先前文件從而導致線上的使用者存取失效。 因為只要做到每次發布的靜態資源(css, js, img)的名稱都是獨一無二的,那麼我就可以:webpack 如何做持久化緩存
上面簡單介紹完持久化緩存,下面這個才是重點,那麼我們應該如何在webpack 中進行持久化緩存的呢,我們需要做到以下兩點:所以如果你只是單純地將所有內容打包成同一個文件,那麼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 文件打包机制 就会知道,在 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 文件打包機制
上面是我整理給大家的,希望今後會對大家有幫助。
相關文章:
#以上是淺談Webpack 持久化快取實踐的詳細內容。更多資訊請關注PHP中文網其他相關文章!