作者:華爾街見聞技術(shù)團隊 - 花褲衩
segmentfault.com/a/1190000015919928
推薦先閱讀 webpack 入門教程之后再來閱讀本文盅藻。
Webpack 4 和單頁應(yīng)用入門
手摸手,帶你用合理的姿勢使用 webpack4 (上)
本文為手摸手使用 webpack4(下)群嗤,主要分為兩部分:
怎么合理的運用瀏覽器緩存
怎么構(gòu)建可靠的持久化緩存
默認分包策略
webpack 4 最大的改動就是廢除了 CommonsChunkPlugin 引入了 optimization.splitChunks菠隆。
webpack 4 的Code Splitting 它最大的特點就是配置簡單,如果你的 mode 是 production狂秘,那么 webpack 4 就會自動開啟 Code Splitting浸赫。
以下內(nèi)容都會以 vue-element-admin 為例子。 在線
bundle-report
如上圖所示赃绊,在沒配置任何東西的情況下,webpack 4 就智能的幫你做了代碼分包羡榴。入口文件依賴的文件都被打包進了app.js碧查,那些大于 30kb 的第三方包,如:echarts校仑、xlsx忠售、dropzone等都被單獨打包成了一個個獨立 bundle。
它內(nèi)置的代碼分割策略是這樣的:
新的 chunk 是否被共享或者是來自 node_modules 的模塊
新的 chunk 體積在壓縮之前是否大于 30kb
按需加載 chunk 的并發(fā)請求數(shù)量小于等于 5 個
頁面初始加載時的并發(fā)請求數(shù)量小于等于 3 個
但有一些小的組件迄沫,如上圖:vue-count-to 在未壓縮的情況下只有 5kb稻扬,雖然它被兩個頁面共用了,但 webpack 4 默認的情況下還是會將它和那些懶加載的頁面代碼打包到一起羊瘩,并不會單獨將它拆成一個獨立的 bundle泰佳。(雖然被共用了盼砍,但因為體積沒有大于 30kb)
你可能會覺得 webpack 默認策略是不是有問題,我一個組件被多個頁面逝她,你每個頁面都將這個組件打包進去了浇坐,豈不是會重復(fù)打包很多次這個組件?就拿vue-count-to來舉例黔宛,你可以把共用兩次以上的組件或者代碼單獨抽出來打包成一個 bundle近刘,但你不要忘了vue-count-to未壓縮的情況下就只有 5kb,gizp 壓縮完可能只有 1.5kb 左右臀晃,你為了共用這 1.5kb 的代碼觉渴,卻要額外花費一次 http 請求的時間損耗,得不償失徽惋。我個人認為 webpack 目前默認的打包規(guī)則是一個比較合理的策略了案淋。
但有些場景下這些規(guī)則可能就顯得不怎么合理了。比如我有一個管理后臺寂曹,它大部分的頁面都是表單和 Table哎迄,我使用了一個第三方 table 組件,幾乎后臺每個頁面都需要它隆圆,但它的體積也就 15kb漱挚,不具備單獨拆包的標(biāo)準(zhǔn),它就這樣被打包到每個頁面的 bundle 中了渺氧,這就很浪費資源了旨涝。這種情況下建議把大部分頁面能共用的組件單獨抽出來,合并成一個component-vendor.js的包(后面會介紹)侣背。
優(yōu)化沒有銀彈白华,不同的業(yè)務(wù),優(yōu)化的側(cè)重點是不同的贩耐。個人認為 webpack 4 默認拆包已經(jīng)做得不錯了弧腥,對于大部分簡單的應(yīng)用來說已經(jīng)夠用了。但作為一個通用打包工具潮太,它是不可能滿足所有的業(yè)務(wù)形態(tài)和場景的管搪,所以接下來就需要我們自己稍微做一些優(yōu)化了。
優(yōu)化分包策略
就拿 vue-element-admin 來說铡买,它是一個基于 Element-UI 的管理后臺更鲁,所以它會用到如 echarts、xlsx奇钞、dropzone等各種第三方插件澡为,同時又由于是管理后臺,所以本身自己也會寫很多共用組件景埃,比如各種封裝好的搜索查詢組件媒至,共用的業(yè)務(wù)模塊等等顶别,如果按照默認的拆包規(guī)則,結(jié)果就不怎么完美了塘慕。
如第一張圖所示筋夏,由于element-ui在entry入口文件中被引入并且被大量頁面共用,所以它默認會被打包到 app.js 之中图呢。這樣做是不合理的夫凸,因為app.js里還含有你的router 路由聲明误算、store 全局狀態(tài)轰豆、utils 公共函數(shù)不翩,icons 圖標(biāo)等等這些全局共用的東西。
但除了element-ui指蚜,其它這些又是平時開發(fā)中經(jīng)常會修改的東西乞巧,比如我新增了一個全局功能函數(shù),utils文件就會發(fā)生改變摊鸡,或者我修改一個路由的 path绽媒,router文件就變了,這些都會導(dǎo)致app.js的 hash 發(fā)生改變:app.1.js => app.2.js免猾。但由于 element-ui和 vue/react等也被打包在其中是辕,雖然你沒改變它們,但它們的緩存也會隨著app.xxx.js變化而失效了猎提,這就非常不合理的获三。所以我們需要自己來優(yōu)化一下緩存策略。
我們現(xiàn)在的策略是按照體積大小锨苏、共用率疙教、更新頻率重新劃分我們的包,使其盡可能的利用瀏覽器緩存伞租。
我們根據(jù)上表來重新劃分我們的代碼就變成了這樣贞谓。
基礎(chǔ)類庫 chunk-libs
它是構(gòu)成我們項目必不可少的一些基礎(chǔ)類庫,比如 vue+vue-router+vuex+axios 這種標(biāo)準(zhǔn)的全家桶葵诈,它們的升級頻率都不高经宏,但每個頁面都需要它們。(一些全局被共用的驯击,體積不大的第三方庫也可以放在其中:比如 nprogress、js-cookie耐亏、clipboard 等)
UI 組件庫
理論上 UI 組件庫也可以放入 libs 中徊都,但這里單獨拿出來的原因是: 它實在是比較大,不管是 Element-UI還是Ant Design gizp 壓縮完都可能要 200kb 左右广辰,它可能比 libs 里面所有的庫加起來還要大不少暇矫,而且 UI 組件庫的更新頻率也相對的比 libs 要更高一點主之。我們不時的會升級 UI 組件庫來解決一些現(xiàn)有的 bugs 或使用它的一些新功能。所以建議將 UI 組件庫也單獨拆成一個包李根。
自定義組件/函數(shù) chunk-commons
這里的 commons 主要分為 必要和非必要槽奕。
必要組件是指那些項目里必須加載它們才能正常運行的組件或者函數(shù)。比如你的路由表房轿、全局 state粤攒、全局側(cè)邊欄/Header/Footer 等組件、自定義 Svg 圖標(biāo)等等囱持。這些其實就是你在入口文件中依賴的東西夯接,它們都會默認打包到app.js中。
非必要組件是指被大部分頁面使用纷妆,但在入口文件 entry 中未被引入的模塊盔几。比如:一個管理后臺,你封裝了很多 select 或者 table 組件掩幢,由于它們的體積不會很大逊拍,它們都會被默認打包到到每一個懶加載頁面的 chunk 中,這樣會造成不少的浪費际邻。你有十個頁面引用了它芯丧,就會包重復(fù)打包十次。所以應(yīng)該將那些被大量共用的組件單獨打包成chunk-commons枯怖。
不過還是要結(jié)合具體情況來看注整。一般情況下,你也可以將那些非必要組件函數(shù)也在入口文件 entry 中引入度硝,和必要組件函數(shù)一同打包到app.js之中也是沒什么問題的肿轨。
低頻組件
低頻組件和上面的共用組件 chunk-commons 最大的區(qū)別是,它們只會在一些特定業(yè)務(wù)場景下使用蕊程,比如富文本編輯器椒袍、js-xlsx前端 excel 處理庫等。一般這些庫都是第三方的且大于 30kb藻茂,所以 webpack 4 會默認打包成一個獨立的 bundle驹暑。也無需特別處理。小于 30kb 的情況下會被打包到具體使用它的頁面 bundle 中辨赐。
業(yè)務(wù)代碼
這部分就是我們平時經(jīng)常寫的業(yè)務(wù)代碼优俘。一般都是按照頁面的劃分來打包,比如在 vue 中掀序,使用路由懶加載的方式加載頁面 component: () => import('./Foo.vue') webpack 默認會將它打包成一個獨立的 bundle帆焕。
完整配置代碼:
splitChunks: {
??chunks: "all",
??cacheGroups: {
????libs: {
??????name: "chunk-libs",
??????test: /[/]node_modules[/]/,
??????priority: 10,
??????chunks: "initial" // 只打包初始時依賴的第三方
????},
????elementUI: {
??????name: "chunk-elementUI", // 單獨將 elementUI 拆包
??????priority: 20, // 權(quán)重要大于 libs 和 app 不然會被打包進 libs 或者 app
??????test: /[/]node_modules[/]element-ui[/]/
????},
????commons: {
??????name: "chunk-comomns",
??????test: resolve("src/components"), // 可自定義拓展你的規(guī)則
??????minChunks: 2, // 最小共用次數(shù)
??????priority: 5,
??????reuseExistingChunk: true
????}
??}
};
上圖就是最終拆包結(jié)果概要,你可以 點我點我點我不恭,在線查看拆包結(jié)果叶雹。
這樣就能盡可能的利用了瀏覽器緩存财饥。當(dāng)然這種優(yōu)化還是需要因項目而異的。比如上圖中的共用組件 chunk-commons折晦,可能打包出來發(fā)現(xiàn)特別大钥星,包含了很多組件,但又不是每一個頁面或者大部分頁面需要它满着。很可能出現(xiàn)這種狀況:A 頁面只需要 chunk-commons里面的 A 組件谦炒,
但卻要下載整個chunk-commons.js,這時候就需要考慮一下漓滔,目前的拆包策略是否合理编饺,是否還需要chunk-commons?還是將這些組件打包到各自的 bundle 中响驴?還是調(diào)整一下 minChunks: 2( 最小共用次數(shù))透且?或者修改一下它的拆包規(guī)則?
// 或者你可以把策略改為只提取那些你注冊在全局的組件豁鲤。
- test: resolve("src/components")
+ test: resolve("src/components/global_components") //你注冊全局組件的目錄
博弈
其實優(yōu)化就是一個博弈的過程秽誊,是讓 a bundle 大一點還是 b? 是讓首次加載快一點還是讓 cache 的利用率高一點? 但有一點要切記琳骡,拆包的時候不要過分的追求顆凉郏化,什么都單獨的打成一個 bundle楣号,不然你一個頁面可能需要加載十幾個.js文件最易,如果你還不是HTTP/2的情況下,請求的阻塞還是很明顯的(受限于瀏覽器并發(fā)請求數(shù))炫狱。所以還是那句話資源的加載策略并沒什么完全的方案藻懒,都需要結(jié)合自己的項目找到最合適的拆包策略。
比如支持HTTP/2的情況下视译,你可以使用 webpack4.15.0 新增的 maxSize嬉荆,它能將你的chunk在minSize的范圍內(nèi)更加合理的拆分,這樣可以更好地利用HTTP/2來進行長緩存(在HTTP/2的情況下酷含,緩存策略就和之前又不太一樣了)鄙早。
Long term caching
持久化緩存其實是一個老生常談的問題,前端發(fā)展到現(xiàn)在椅亚,緩存方案已經(jīng)很成熟了限番。簡單原理:
針對 html 文件:不開啟緩存,把 html 放到自己的服務(wù)器上呀舔,關(guān)閉服務(wù)器的緩存
針對靜態(tài)的 js扳缕,css,圖片等文件:開啟 cdn 和緩存,將靜態(tài)資源上傳到 cdn 服務(wù)商躯舔,我們可以對資源開啟長期緩存,因為每個資源的路徑都是獨一無二的省古,所以不會導(dǎo)致資源被覆蓋粥庄,保證線上用戶訪問的穩(wěn)定性。
每次發(fā)布更新的時候豺妓,先將靜態(tài)資源(js, css, img) 傳到 cdn 服務(wù)上惜互,然后再上傳 html 文件,這樣既保證了老用戶能否正常訪問琳拭,又能讓新用戶看到新的頁面训堆。
相關(guān)文章 大公司里怎樣開發(fā)和部署前端代碼?(https://www.zhihu.com/question/20790576/answer/32602154)
所以我們現(xiàn)在要做的就是要讓 webpack 給靜態(tài)資源生產(chǎn)一個可靠的 hash白嘁,讓它能自動在合適的時候更新資源的 hash坑鱼,
并且保證 hash 值的唯一性,即為每個打包后的資源生成一個獨一無二的 hash 值絮缅,只要打包內(nèi)容不一樣鲁沥,那么 hash 值就不一樣。
其實 webpack 4 在持久化緩存這一塊已經(jīng)做得非常的不錯了耕魄,但還是有一些欠缺画恰,下面我們將要從這幾個方面討論這個問題。
RuntimeChunk(manifest)
Module vs Chunk
HashedModuleIdsPlugin
NamedChunksPlugin
RuntimeChunk(manifest)
webpack 4 提供了 runtimeChunk 能讓我們方便的提取 manifest吸奴,以前我們需要這樣配置
new webpack.optimize.CommonsChunkPlugin({
??name: "manifest",
??minChunks: Infinity
});
現(xiàn)在只要一行配置就可以了
{
??runtimeChunk: true;
}
它的作用是將包含chunks 映射關(guān)系的 list單獨從 app.js里提取出來允扇,因為每一個 chunk 的 id 基本都是基于內(nèi)容 hash 出來的,所以你每次改動都會影響它则奥,如果不將它提取出來的話考润,等于app.js每次都會改變。緩存就失效了逞度。
單獨抽離 runtimeChunk 之后额划,每次打包都會生成一個runtimeChunk.xxx.js。(默認叫這名字档泽,可自行修改)
優(yōu)化
其實我們發(fā)現(xiàn)打包生成的 runtime.js非常的小俊戳,gzip 之后一般只有幾 kb,但這個文件又經(jīng)常會改變馆匿,我們每次都需要重新請求它抑胎,它的 http 耗時遠大于它的執(zhí)行時間了,所以建議不要將它單獨拆包渐北,而是將它內(nèi)聯(lián)到我們的 index.html 之中(index.html 本來每次打包都會變)阿逃。
這里我選用了 script-ext-html-webpack-plugin,主要是因為它還支持preload和 prefetch,正好需要就不想再多引用一個插件了恃锉,你完全可以使用 inline-manifest-webpack-plugin或者 assets-webpack-plugin等來實現(xiàn)相同的效果搀菩。
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
// 注意一定要在HtmlWebpackPlugin之后引用
// inline 的name 和你 runtimeChunk 的 name保持一致
new ScriptExtHtmlWebpackPlugin({
??//`runtime` must same as runtimeChunk name. default is `runtime`
??inline: /runtime..*.js$/
});
Module vs Chunk
我們經(jīng)常看到xxxModuleIdsPlugin破托、xxxChunksPlugin肪跋,所以在 webpack 中 module和 chunk到底是一個怎么樣的關(guān)系呢?
chunk: 是指代碼中引用的文件(如:js土砂、css州既、圖片等)會根據(jù)配置合并為一個或多個包,我們稱一個包為 chunk萝映。
module: 是指將代碼按照功能拆分吴叶,分解成離散功能塊。拆分后的代碼塊就叫做 module序臂“雎保可以簡單的理解為一個 export/import 就是一個 module。
每個 chunk 包可含多個 module贸宏。 比如:
//9.xxxxxxxxx.js
//chunk id為 9 造寝,包含了Vc2m和JFUb兩個module
(window.webpackJsonp = window.webpackJsonp || []).push([
??[9],
??{
????Vc2m: function(e, t, l) {},
????JFUb: function(e, t, l) {}
??}
]);
一個module還能跨chunk引用另一個module,比如我想在app.js里面需要引用 chunkId為13的模塊2700可以這樣引用:
return n.e(13).then(n.bind(null, "27OO"));
HashedModuleIdsPlugin
了解了 module和chunk之后吭练,我們來研究一下 moduleId诫龙。
首先要確定你的 filename 配置的是chunkhash(它與 hash 的區(qū)別可以看上篇文章)。
output: {
??path: path.join(__dirname, 'dist'),
??filename: '[name].[chunkhash].js',
}
我們在入口文件中隨便引入一個新文件test.js
//main.js
import "./test";
//test.js
console.log("apple");
我們運行npm run build鲫咽,發(fā)現(xiàn)了一件奇怪的事情签赃,我只是多引入了一個文件,但發(fā)現(xiàn)有十幾個文件發(fā)生了變化分尸。這是為什么呢锦聊?
我們隨便挑一個文件 diff 一下,發(fā)現(xiàn)兩個文件只有 module id 的不同箩绍。
這是因為:
webpack 內(nèi)部維護了一個自增的 id孔庭,每個 module 都有一個 id。所以當(dāng)增加或者刪除 module 的時候材蛛,id 就會變化圆到,導(dǎo)致其它文件雖然沒有變化,但由于 id 被強占卑吭,只能自增或者自減芽淡,導(dǎo)致整個 id 的順序都錯亂了。
雖然我們使用 [chunkhash] 作為輸出名豆赏,但仍然是不夠的挣菲。
因為 chunk 內(nèi)部的每個 module 都有一個 id富稻,webpack 默認使用遞增的數(shù)字作為 moduleId。
如果引入了一個新文件或刪掉一個文件白胀,都可能會導(dǎo)致其它文件的 moduleId 發(fā)生改變椭赋,
那這樣緩存失效了。如:
本來是一個按序的 moduleId list或杠,這時候我插入一個orange模塊纹份,插在第三個位置,這樣就會導(dǎo)致它之后的所以 module id 都依次加了 1廷痘。
這到了原因,解決方案就很簡單了件已。我們就不要使用一個自增的 id 就好了笋额,這里我們使用HashedModuleIdsPlugin。
或者使用optimization.moduleIds v4.16.0 新發(fā)布篷扩,文檔還沒有兄猩。查看 源碼發(fā)現(xiàn)它有natural、named鉴未、hashed枢冤、size、total-size铜秆。這里我們設(shè)置為optimization.moduleIds='hash'等于HashedModuleIdsPlugin淹真。源碼了也寫了webpack5會優(yōu)化這部分代碼。
它的原理是使用文件路徑的作為 id连茧,并將它 hash 之后作為 moduleId核蘸。
使用了 HashedModuleIdsPlugin`,我們再對比一下發(fā)現(xiàn) module id 不再是簡單的 id 了啸驯,而是一個四位 hash 過得字符串(不一定都是四位的客扎,如果重復(fù)的情況下會增加位數(shù),保證唯一性 源碼)罚斗。
這樣就固定住了 module id 了徙鱼。
NamedModulesPlugin 和 HashedModuleIdsPlugin 原理是相同的,將文件路徑作為 id针姿,只不過沒有把路徑 hash 而已袱吆,適用于開發(fā)環(huán)境方便調(diào)試。不建議在生產(chǎn)環(huán)境配置搓幌,因為這樣不僅會增加文件的大懈斯省(路徑一般偶讀比較長),更重要的是為暴露你的文件路徑溉愁。
NamedChunkPlugin
我們在固定了 module id 之后同理也需要固定一下 chunk id处铛,不然我們增加 chunk 或者減少 chunk 的時候會和 module id 一樣饲趋,都可能會導(dǎo)致 chunk 的順序發(fā)生錯亂,從而讓 chunk 的緩存都失效撤蟆。
作者也意識到了這個問題奕塑,提供了一個叫NamedChunkPlugin的插件,但在使用路由懶加載的情況下家肯,你會發(fā)現(xiàn)NamedChunkPlugin并沒什么用龄砰。
供了一個線上demo,可以自行測一下讨衣。這里提就直接貼一下結(jié)果:
產(chǎn)生的原因前面也講了换棚,使用自增 id 的情況下是不能保證你新添加或刪除 chunk 的位置的,一旦它改變了反镇,這個順序就錯亂了固蚤,就需要重排,就會導(dǎo)致它之后的所有 id 都發(fā)生改變了歹茶。
接著我們 查看源碼 還發(fā)現(xiàn)它只對有 name 的 chunk 才奏效夕玩!所以我們那些異步懶加載的頁面都是無效的。這啟不是坑爹惊豺!我們迭代業(yè)務(wù)肯定會不斷的添加刪除頁面燎孟,這豈不是每新增一個頁面都會讓之前的緩存都失效?那我們之前還費這么大力優(yōu)化什么拆包呢尸昧?
其實這是一個古老的問題了 相關(guān) issue: Vendor chunkhash changes when app code changes 早在 2015 年就有人提了這個問題揩页,這個問題也一直討論至今,’網(wǎng)友們’也提供了各種奇淫巧技彻磁,不過大部分隨著 webpack 的迭代已經(jīng)不適用或者是修復(fù)了碍沐。
這里我就結(jié)合一下 timse(webpack 第二多貢獻)寫的持久緩存的文章(在 medium 上需要翻墻)
總結(jié)一下目前能解決這個問題的三種方案。
目前解決方案有三種
records
webpackChunkName
自定義 nameResolver
webpack records
很多人可能連這個配置項都沒有注意過衷蜓,不過早在 2015 年就已經(jīng)被設(shè)計出來讓你更好的利用 cache累提。官方文檔
要使用它配置也很簡單:
recordsPath: path.join(__dirname, "records.json");
對,只要這一行代碼就能開啟這個選項磁浇,并打包的時候會自動生成一個 JSON 文件斋陪。它含有 webpack 的 records 記錄 – 即「用于存儲跨多次構(gòu)建(across multiple builds)的模塊標(biāo)識符」的數(shù)據(jù)片段≈孟牛可以使用此文件來跟蹤在每次構(gòu)建之間的模塊變化无虚。
大白話就是:等于每次構(gòu)建都是基于上次構(gòu)建的基礎(chǔ)上進行的。它會先讀取你上次的 chunk 和 module id 的信息之后再進行打包衍锚。所以這時候你再添加或者刪除 chunk友题,并不會導(dǎo)致之前所說的亂序了。
簡單看一下構(gòu)建出來的 JSON 長啥樣戴质。
{
??"modules": {
????"byIdentifier": {
??????"demo/vendor.js": 0,
??????"demo/vendor-two.js": 1,
??????"demo/index.js": 2,
??????....
????},
????"usedIds": {
??????"0": 0,
??????"1": 1,
??????"2": 2,
??????...
????}
??},
??"chunks": {
????"byName": {
??????"vendor-two": 0,
??????"vendor": 1,
??????"entry": 2,
??????"runtime": 3
????},
????"byBlocks": {},
????"usedIds": [
??????0,
??????1,
??????2
??}
}
我們和之前一樣度宦,在路由里面添加一個懶加載的頁面踢匣,打包對比后發(fā)現(xiàn) id 并不會像之前那樣按照遍歷到的順序插入了,而是基于之前的 id 依次累加了戈抄。一般新增頁面都會在末尾填寫一個新 id离唬,刪除 chunk 的話,會將原來代表 chunk 的 id划鸽,保留输莺,但不會再使用。
但這個方案不被大家知曉主要原因就是維護這個records.json比較麻煩裸诽。如果你是在本地打包運行webpack的話嫂用,你只要將records.json當(dāng)做普通文件上傳到github、gitlab或其它版本控制倉庫丈冬。
但現(xiàn)在一般公司都會將打包放在 CI里面尸折,用docker打包,這時候這份records.json存在哪里就是一個問題了殷蛇。它不僅需要每次打包之前先讀取你這份 json,打包完之后它還需要再更新這份 json橄浓,并且還要找地方存貯粒梦,為了下次構(gòu)建再使用。你可以存在 git 中或者找一個服務(wù)器存荸实,但存在什么地其它方都感覺怪怪的匀们。
如果你使用 Circle CI可以使用它的store_artifacts,相關(guān)教程准给。
本人在使用了之后還是放棄了這個方案泄朴,使用成本略高。前端打包應(yīng)該更加的純粹露氮,不需要依賴太多其它的東西祖灰。
webpackChunkName
在 webpack2.4.0 版本之后可以自定義異步 chunk 的名字了,例如:
import(/* webpackChunkName: "my-chunk-name" */ "module");
我們在結(jié)合 vue 的懶加載可以這樣寫畔规。
{
????path: '/test',
????component: () => import(/* webpackChunkName: "test" */ '@/views/test')
??},
打包之后就生成了名為 test的 chunk 文件局扶。
chunk 有了 name 之后就可以解決NamedChunksPlugin沒有 name 的情況下的 bug 了。查看打包后的代碼我們發(fā)現(xiàn) chunkId 就不再是一個簡單的自增 id 了叁扫。
不過這種寫法還是有弊端的三妈,首先你需要手動編寫每一個 chunk 的 name,同時還需要保證它的唯一性莫绣,當(dāng)頁面一多畴蒲,維護起來還是很麻煩的。這就違背了程序員的原則:能偷懶就偷懶对室。
所以有什么辦法可以自動生成一個 name 給 chunk 么 模燥?查看 webpack 源碼我們發(fā)現(xiàn)了NamedChunksPlugin其實可以自定義 nameResolver 的咖祭。
自定義 nameResolver
NamedChunksPlugin支持自己寫 nameResolver 的規(guī)則的。但目前大部分相關(guān)的文章里的自定義函數(shù)是不適合 webpack4 涧窒,而且在結(jié)合 vue 的情況下還會報錯心肪。
社區(qū)舊方案:
new webpack.NamedChunksPlugin(chunk => {
??if (chunk.name) {
????return chunk.name;
??}
??return chunk.modules.map(m => path.relative(m.context, m.request)).join("_");
});
適配 webpack4 和 vue 的新實現(xiàn)方案:
new webpack.NamedChunksPlugin(chunk => {
??if (chunk.name) {
????return chunk.name;
??}
??return Array.from(chunk.modulesIterable, m => m.id).join("_");
});
當(dāng)然這個方案還是有一些弊端的因為 id 會可能很長,如果一個 chunk 依賴了很多個 module 的話纠吴,id 可能有幾十位硬鞍,所以我們還需要縮短一下它的長度。我們首先將拼接起來的 id hash 以下戴已,而且要保證 hash 的結(jié)果位數(shù)也能太長固该,浪費字節(jié),但太短又容易發(fā)生碰撞糖儡,所以最后我們我們選擇 4 位長度伐坏,并且手動用 Set 做一下碰撞校驗,發(fā)生碰撞的情況下位數(shù)加 1握联,直到碰撞為止桦沉。詳細代碼如下:
const seen = new Set();
const nameLength = 4;
new webpack.NamedChunksPlugin(chunk => {
??if (chunk.name) {
????return chunk.name;
??}
??const modules = Array.from(chunk.modulesIterable);
??if (modules.length > 1) {
????const hash = require("hash-sum");
????const joinedHash = hash(modules.map(m => m.id).join("_"));
????let len = nameLength;
????while (seen.has(joinedHash.substr(0, len))) len++;
????seen.add(joinedHash.substr(0, len));
????return `chunk-${joinedHash.substr(0, len)}`;
??} else {
????return modules[0].id;
??}
});
我給 vue-cli 官方也提了一個相關(guān)issue尤雨溪最后也采納了這個方案。
所以如果你現(xiàn)在下載最新 vue-cli@3上面啰嗦了半天的東西金闽,其實都已經(jīng)默認配置好了(但作者本人為了找到這個 hack 方法整整花了兩天時間 o(╥﹏╥)o)纯露。
目前測試了一段時間沒發(fā)現(xiàn)有什么問題。不過有一點不是很理解代芜,不知道 webpack 出于什么樣的原因埠褪,官方一直沒有修復(fù)這個問題?可能是在等 webpack5 的時候放大招吧挤庇。
總結(jié)
拆包策略:
基礎(chǔ)類庫 chunk-libs
UI 組件庫 chunk-elementUI
自定義共用組件/函數(shù) chunk-commons
低頻組件 chunk-eachrts/chunk-xlsx等
業(yè)務(wù)代碼 lazy-loading xxxx.js
持久化緩存:
使用 runtimeChunk 提取 manifest钞速,使用 script-ext-html-webpack-plugin等插件內(nèi)聯(lián)到index.html減少請求
使用 HashedModuleIdsPlugin 固定 moduleId
使用 NamedChunkPlugin結(jié)合自定義 nameResolver 來固定 chunkId
上述說的問題大部分在 webpack 官方文檔都沒明確指出,唯一可以參考的就是這份 cache 文檔嫡秕,在剛更新 webpack4 的時候渴语,我以為官方已經(jīng)將 id 不能固定的問題解決了,但現(xiàn)實是殘酷的昆咽,結(jié)果并不理想遵班。不過作者也在很多的 issue 中說他正在著手優(yōu)化 long term caching。
We plan to add another way to assign module/chunk ids for long term caching, but this is not ready to be told yet.
在 webpack 的 issue 和源碼中也經(jīng)常見到 Long term caching will be improved in webpack@5和TODO webpack 5 xxxx這樣的代碼注釋潮改。這讓我對webpack 5很期待狭郑。真心希望webpack 5能真正的解決前面幾個問題,并且讓它更加的out-of-the-box汇在,更加的簡單和智能翰萨,就像webpack 4的optimization.splitChunks,你基本不用做什么糕殉,它就能很好的幫你拆分好bundle包亩鬼,同時又給你非常的自由發(fā)揮空間殖告。
展望
Whats next? 官方在這篇文章中展望了一下 webpack5 和講述了一下未來的計劃–持續(xù)改進用戶體驗、提升構(gòu)建速度和性能雳锋,降低使用門檻黄绩,完善Persistent Caching等等。同時 webpack 也已經(jīng)支持 Prefetching/Preloading modules玷过,我相信之后也會有更多的網(wǎng)站會使用這一屬性爽丹。
同時 webpack 的團隊已經(jīng)承諾會通過投票的方式來決定一些功能。比如不久前發(fā)起的投票辛蚊。
大家可以關(guān)注 Tobias Koppers 的 twitter 進行投票粤蝎。
最后還是期待一下 webpack5 和它之后的發(fā)展吧。如果沒有 webpack袋马,也就不會有今天的前端初澎。
其實如一開始就講的,vue 有vue-cli虑凛、react 有creat-react-app碑宴,現(xiàn)在新建項目基本都是基于腳手架的,很少有人從零開始寫 webpack 配置文件的桑谍,而且一般開發(fā)中墓懂,一般程序員也不需要經(jīng)常去修改 webpack 的配置。webpack 官方本身也在不斷完善默認配置項霉囚,相信 webpack 的配置門檻也會越來低多。
愿世間再無 webpack 配置工程師匕积。
感興趣的小伙伴盈罐,可以關(guān)注公眾號【grain先森】,回復(fù)關(guān)鍵詞 “小程序”闪唆,獲取更多資料盅粪,更多關(guān)鍵詞玩法期待你的探索~