導語:在現代前端工程中,模塊化已經成了前端項目組織文件的標配,網站上線前都會把需要的相關模塊預先打包、處理一番。然而打包的方式多種多樣,如何才能優雅的分離業務代碼和依賴庫?如何才能高效的利用緩存?本文將會和大家分享餓了么前端團隊總結的各方案優劣、踩過的坑,以及終的解決方案。
眾所周知,對于一個站點而言,網站的加載時間一直都是一個很重要的指標。網頁加載時間的長短直接影響到了站點的訪問量。試想,正在看這篇文章的你,會有多少耐心等待一個網頁慢悠悠的打開呢?
對于前端而言,縮短網頁加載時間的常見方式有:
為了讓更改過的文件能夠生效,我們還會給每個文件的文件名里加上一段根據文件內容計算出的hash。每當文件內容改變時,這段hash也會隨之改變,所以瀏覽器會通過網絡下載更新過的文件,但沒有更新過的文件仍然會從緩存里讀取,從而縮短加載時間。
同理,在開發一個單頁面應用的時候,我們通常會將應用的JavaScript代碼打包成兩個文件:一個用于存放內容很少更改的第三方依賴庫,這部分代碼的體積一般會比較大;另一個存放更改比較頻繁的業務邏輯代碼,但它的體積一般比第三方依賴庫小。為了方便描述,我們可以分別稱這兩個文件為vendor.js與app.js。
有了優化方案,接下來就該選擇打包工具了。毫無疑問,時下流行的就是Webpack了。Webpack在文檔里提供了一段簡單易懂的配置,用于將項目中的JavaScript代碼打包成vendor.js與app.js這兩個文件,并分別在它們的文件名里加上一段根據文件內容生成的hash,就像前面說的那樣:
const webpack = require('webpack')
module.exports = {
entry: {
vendor: ['jquery', 'other-lib'],
app: './entry' },
output: {
filename: '[name].[chunkhash].js' },
plugins: [ new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' })
]
}
但是,幾乎所有使用類似配置的人都遇到了一個問題:每當更改了業務邏輯代碼時,都會導致vendor.js的hash發生變化。這意味著用戶仍然要重新下載vendor.js,即使這部分代碼并沒有變過。
為此,開源社區里有人給Webpack指出了這個問題,并吸引了很多人一同討論,一時之間涌出了很多解決的辦法,但這些辦法既有人說有用,也有人說沒用,而官方卻遲遲沒有給出一個定論。
為了得到一個準確的答案,我們嘗試了社區里幾乎所有的方案。接下來,本文會依次給大家介紹我們嘗試過的種種辦法,并在文章的后給出行之有效的解決方案。
社區有人提供了這個插件用來替換Webpack生成的chunkhash:
const webpack = require('webpack') const WebpackMd5Hash = require('webpack-md5-hash')
module.exports = {
entry: {
vendor: ['jquery', 'other-lib'],
app: './entry' },
output: {
filename: '[name].[chunkhash].js' },
plugins: [ new WebpackMd5Hash(), new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' })
]
}
它的原理是:根據模塊打包前的代碼內容生成hash,而不是像Webpack那樣根據打包后的內容生成hash。經簡單測試,在修改業務代碼后,它確實能保證vendor.js的hash不被改變,于是我們滿心歡喜的將它用到了正式環境,但網站卻在上線之后變成了一片空白。
隨后,我們對比了兩次編譯生成的vendor.js,發現代碼里的模塊id已經變了,但由于 hash沒有更新,所以項目上線后,瀏覽器直接從緩存里讀取了上次上線時的舊版 vendor.js文件,但此時新版的app.js里引用的id為41的模塊,在舊版里其實是40,從而引用了錯誤的模塊導致發生了錯誤,中斷了代碼的運行。
不久之后,社區里也有人提出了這個問題。
有人指出,Webpack的CommonsChunkPlugin會在個entry里注入一些運行時代碼。按照模塊的依賴關系,個entry當然就是vendor.js了。這段運行時代碼里包含了終編譯出來的app.js的文件名,而app.js的文件名里包含的hash在每次更改業務代碼后都會變,所以包含了這段代碼的vendor.js的內容也會改變,這才導致它的hash總是不固定。所以,我們需要從vendor.js里抽離出這段運行時代碼,才能避免 vendor.js的hash受到影響。
除此之外,我們還需要用到OccurenceOrderPlugin,將模塊按照一定的順序排序,這才能保證每次編譯時模塊的id都是相同的,否則模塊id一旦改變,就會引起文件內容的變化并影響到hash。
終的Webpack配置就像下面這樣:
const webpack = require('webpack')
module.exports = {
entry: {
vendor: ['jquery', 'other-lib'],
app: './entry' },
output: {
filename: '[name].[chunkhash].js' },
plugins: [ new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' }), // 抽離出 Webpack 的運行時代碼 new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
]
}
這個方法確實有效,但我們發現,在刪除或新增業務代碼中的模塊時,vendor.js的hash偶爾還是會受到影響。Webpack的作者也提到了這一點,原文大意如下:
默認情況下,模塊的id是這個模塊在模塊數組中的索引。OccurenceOrderPlugin 會將引用次數多的模塊放在前面,在每次編譯時模塊的順序都是一致的……如果你修改代碼時新增或刪除了一些模塊,這將會影響到所有模塊的id。
所以,這個方案也不能完全保證vendor.js的hash不受到業務代碼的影響。
在嘗試過第二個解決方案后,我們意識到問題的根源在于Webpack使用模塊的引用順序作為模塊的id,這樣就不能避免新增或刪除模塊對其他模塊的id產生影響。
不過,Webpack提供了NamedModulesPlugin插件,它使用模塊的相對路徑作為模塊的 id,所以只要我們不重命名一個模塊文件,那么它的id就不會變,更不會影響到其它模塊了:
const webpack = require('webpack')
module.exports = {
entry: {
vendor: ['jquery', 'other-lib'],
app: './entry' },
output: {
filename: '[name].[chunkhash].js' },
plugins: [ new webpack.NamedModulesPlugin(), new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' })
]
}
但是,相對路徑比數字id要長了很多。
社區對比了使用這個插件后文件的大小,結論是在gzip壓縮后,文件并沒有大多少。然而我們在項目里實際使用之后,雖然 vendor.js 只比以前大了 1KB,但 app.js 卻大了近 15%。
所以,我們對于這個解決方案仍然不是很滿意。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。