基於webpack3的持久化緩存方案問題梳理

前言

如何基於webpack作持久化緩存目前感受是一直沒有一個很是好的方案來實踐。網上的文章很是多,可是真的有用的很是少,並無一些真正深刻研究和總結的文章。如今依託于于早教寶線上項目和本身的實踐,有了一個完整的方案。css

正文

一、webpack的hash的兩種計算方式node

想要作持久化緩存那麼就要依賴 webpack 自身提供的兩個 hashhashchunkhashwebpack

接着就來看看這兩個值之間的具體含義和差異吧:git

hash: webpack在每一次構建的時候都會產生一個compilation對象,這個hash值就是根據compilation內全部的內容計算而來的值。github

chunkhash:這個值是根據每一個chunk的內容而計算出來的值。web

因此單純根據上面的描述來講,chunkhash是用來作持久化緩存最有效的。api

二、hash和chunkhash的測試緩存

entry 入口文件 入口依賴
pageA a.js a.less->a.css, common.js->common.css
pageB b.js b.less->b.css, common.js->common.css
  • 使用hash計算
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
  entry: {
    pageA: './src/a.js',
    pageB: './src/b.js'
  },
  output: {
    filename: '[name]-[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: ['css-loader?minimize']
        })
      }
    ]
  },
  plugins: [new ExtractTextPlugin('[name]-[hash].css')]
}

構建結果架構

Hash: 80c922b349f516e79fb5
Version: webpack 3.8.1
Time: 1014ms
                         Asset      Size  Chunks             Chunk Names
pageB-80c922b349f516e79fb5.js   2.86 kB       0  [emitted]  pageB
pageA-80c922b349f516e79fb5.js   2.84 kB       1  [emitted]  pageA
pageA-80c922b349f516e79fb5.css  21 bytes       1  [emitted]  pageA
pageB-80c922b349f516e79fb5.css  21 bytes       0  [emitted]  pageB

結論app

能夠發現全部文件的hash所有都是同樣的,可是你多構建幾回產生的hash都是不同的。緣由在於咱們使用了 ExtractTextPluginExtractTextPlugin 自己涉及到異步的抽取流程,因此在生成 assets 資源時存在了不肯定性(前後順序),而 updateHash 則對其敏感,因此就出現瞭如上所說的 hash 異動的狀況。另外全部 assets 資源的 hash 值保持一致,這對於全部資源的持久化緩存來講並無深遠的意義。

  • 使用chunkhash計算
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
  entry: {
    pageA: './src/a.js',
    pageB: './src/b.js'
  },
  output: {
    filename: '[name]-[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: ['css-loader?minimize']
        })
      }
    ]
  },
  plugins: [new ExtractTextPlugin('[name]-[chunkhash].css')]
}

構建結果

Hash: 810904f973cc0cf41992
Version: webpack 3.8.1
Time: 1038ms
                         Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-3a2e5ef3d4506fce8d93.css  21 bytes       1  [emitted]  pageA
pageB-e9ed5150262ba39827d4.css  21 bytes       0  [emitted]  pageB

結論

此時能夠發現,運行多少次,hash 的變更沒有了,每一個 entry 擁有了本身獨一的 hash 值,細心的你或許會發現此時樣式資源的 hash 值和 入口腳本保持了一致,這彷佛並不符合咱們的想法,冥冥之中告訴咱們發生了某些壞事情。

三、探索css文件的hash和入口文件hash之間的關係

在上面的構建結果中,咱們發現css的hash值和入口文件的hash值是同樣的,這裏咱們容易產生疑問,是否是這兩個文件之間必定會有聯繫呢?呆着疑問去修改下b.css文件中的內容,產生構建結果:

Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1028ms
                         Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-3a2e5ef3d4506fce8d93.css  21 bytes       1  [emitted]  pageA
pageB-e9ed5150262ba39827d4.css  41 bytes       0  [emitted]  pageB

納尼???改動css文件內容,爲何css文件的hash沒有改變呢?不科學啊,入口文件的hash也沒有改變。仔細想了一下 webpack 是將全部的內容都認爲是js文件的一部分。在構建的過程當中使用 ExtractTextPlugin 將樣式抽離出entry chunk 了,而此時的 entry chunk 自己並無發生改變,改變的是已經被抽離出去的css部分。而chunkunhash 倒是根據 chunk 計算出來的,因此不變動應該是正常的。可是這個又不符合咱們想要作的持久化緩存的要求,由於又變更就應該改變hash纔是。

開心的是 ExtractTextPlugin 插件爲咱們提供了一個contenthash來變化:

plugins: [new ExtractTextPlugin('[name]-[contenthash].css')]

修改b.css先後兩次構建結果:

Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1091ms
                     Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-9783744431577cdcfea658734b7db20f.css  21 bytes       1  [emitted]  pageA
pageB-2d03aa12ae45c64dedd7f66bb88dd3db.css  41 bytes       0  [emitted]  pageB
Hash: 7a96bcf1ef668a49c9d8
Version: webpack 3.8.1
Time: 1193ms
                     Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-9783744431577cdcfea658734b7db20f.css  21 bytes       1  [emitted]  pageA
pageB-7e05e00e24f795b674df5701f6a38bd9.css  42 bytes       0  [emitted]  pageB

對比發現修改了樣式文件後只有樣式文件的hash發生了改變,符合咱們想要的預期。

四、module id的不可控和修正

通過上面的測試,咱們理所固然的認爲我完成了持久化緩存的hash穩定。而後咱們不當心刪除了a.js中的a.less文件,而後先後兩次構建:

Hash: 88ab71080c53db9d9f70
Version: webpack 3.8.1
Time: 1279ms
                                     Asset       Size  Chunks             Chunk Names
             pageB-a2d1e1d73336f17e2dc4.js    3.82 kB       0  [emitted]  pageB
             pageA-96c9f5afea30e7e09628.js     3.8 kB       1  [emitted]  pageA
pageA-d7ac82de795ddf50c9df43291d77b4c8.css   92 bytes       1  [emitted]  pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       0  [emitted]  pageB
Hash: 172153ea2b39c2046a92
Version: webpack 3.8.1
Time: 1260ms
                                     Asset       Size  Chunks             Chunk Names
             pageB-884da67fe2322246ab28.js    3.81 kB       0  [emitted]  pageB
             pageA-4c0dfb634722c556ffa0.js    3.68 kB       1  [emitted]  pageA
pageA-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       1  [emitted]  pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       0  [emitted]  pageB

奇怪的事產生了,我移除了a.less文件後發現pageB入口文件的hash都改變了。若是隻有pageA相關的文件hash變了我還能夠理解。可是????爲何都變了???不行我得看看爲何都變了。
image
經過上面的diff發現咱們移除了a.less後總體的id發生了改變了。那麼這個地方的id咱們能夠推測是表明的是具體的引用的模塊。

接着咱們在看看先後兩次構建模塊的信息:

[3] ./src/a.js 284 bytes {1} [built]
[4] ./src/a.less 41 bytes {1} [built]
[5] ./src/b.js 284 bytes {0} [built]
[6] ./src/b.less 41 bytes {0} [built]
[3] ./src/a.js 264 bytes {1} [built]
[4] ./src/b.js 284 bytes {0} [built]
[5] ./src/b.less 41 bytes {0} [built]

經過對比發現前面的序號在構建出來的pageB中有隱藏pageA相關的信息,這對於咱們來作持久化緩存來講是很是不便的。咱們期待的是pageB中只包含和自身相關的信息,不包含其餘與自身無關的信息。

五、module id的變化

排除與己不相關的module id或者內容

會用webpack的人大概都之都一個特性:Code Splitting,本質上是對 chunk 進行拆分再組合的過程。具體要怎麼作呢?

The answer is CommonsChunkPlugin,在plugin中添加:

plugins: [
    new ExtractTextPlugin('[name]-[contenthash].css'),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
]

接下來在看看移除pageA中的a.less的先後變化:

Hash: 697b36118920d991364a
Version: webpack 3.8.1
Time: 1488ms
                                       Asset       Size  Chunks             Chunk Names
               pageB-9b2eb6768499c911a728.js  491 bytes       0  [emitted]  pageB
               pageA-c342383ca09604e8e7b8.js  495 bytes       1  [emitted]  pageA
             runtime-b6ec3c0d350aef6cbf3e.js     6.8 kB       2  [emitted]  runtime
  pageA-b812cf5b72744af29181f642fe4dbf38.css   43 bytes       1  [emitted]  pageA
  pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css   59 bytes       0  [emitted]  pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]  runtime
Hash: 7ddaf109d5aa67c43ce2
Version: webpack 3.8.1
Time: 1793ms
                                       Asset       Size  Chunks             Chunk Names
               pageB-613cc5a6a90adfb635f4.js  491 bytes       0  [emitted]  pageB
               pageA-0b72f85fda69a9442076.js  375 bytes       1  [emitted]  pageA
             runtime-a41b8b8bfe7ec70fd058.js    6.79 kB       2  [emitted]  runtime
  pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css   59 bytes       0  [emitted]  pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]  runtime

接着在看看兩次構建中pageB的對比:
image

通過對比咱們發如今pageB中只包含的是自身相關的內容。因此使用CommonsChunkPlugin達到了咱們的指望。而抽離出去的代碼就是webpack的運行時代碼。運行時代碼也存儲着webpack對module和chunk相關的信息。另外咱們發現pageA和pageB的文件大小也發生了變化。致使這個變化的緣由是CommonsChunkPlugin會默認的把entry chunk都包含的module抽取到咱們取名爲runtime的normal chunk中去。

假如咱們在開發中每一個頁面都會用到一些工具庫,例如lodash這類的。因爲CommonsChunkPlugin的默認行爲會抽取公共部分,可能lodash並無發生改變,可是被抽離在運行時代碼中的時候,每次都是會去請求新的。這不能達到咱們要求的最小更新原則。因此咱們要人工去幹預一些代碼。

plugins: [
    new ExtractTextPlugin('[name]-[contenthash].css'),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
})

在次對邊先後兩次構建的日誌:

Hash: a703a57c828ec32b24e1
Version: webpack 3.8.1
Time: 1493ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-f11f58b8150930590a10.js     541 kB       0  [emitted]  [big]  vendor
             pageB-7d065cd319176f44c605.js  938 bytes       1  [emitted]         pageB
             pageA-2b7e3707314e7ec4d770.js  910 bytes       2  [emitted]         pageA
           runtime-e68dec8bcad8a5870f0c.js    5.88 kB       3  [emitted]         runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css   92 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB
Hash: 26fc9ad18554b28cd8e1
Version: webpack 3.8.1
Time: 1806ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-d9bad56677b04b803651.js     541 kB       0  [emitted]  [big]  vendor
             pageB-a55dadfbf25a45856d6a.js  929 bytes       1  [emitted]         pageB
             pageA-7cbd77a502262ddcdd19.js  790 bytes       2  [emitted]         pageA
           runtime-fa8eba6e81ed41f50d6f.js    5.88 kB       3  [emitted]         runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB

到此爲止咱們解決了:排除與己不相關的module id或者內容問題。

穩定module id,儘量的保持module id保持不變

一個module id是一個模塊的惟一標示,而且該標示會出如今對應的entry chunk構建後的代碼中。看個pageB的構建後代碼的例子:

__webpack_require__(7)
const sum = __webpack_require__(0)
const _ = __webpack_require__(3)

根據前面的實驗,模塊的增長或者減小都會引發module id的改變,因此爲了避免引發module id的改變,那麼咱們只能找一個東西來代替module id做爲標示。咱們在構建的過程當中就將尋找出來替代標示來替換module id。

因此上面的敘述能夠轉換成兩個步驟來行動。

  • 找到替代module id的方式
  • 找到時機替換module id

六、穩定 module id 的相關操做

找到替代module id的方式
咱們在平常的開發中,常常引用模塊,都是經過地址來引用的。從這裏咱們能夠獲得啓發,咱們能不可以把module id所有替換成路徑呢?再一個咱們瞭解到在webpack resolve module階段咱們確定是能夠拿到資源路徑的。在開始咱們擔憂平臺的路徑差別性。幸運的是webpack 的源碼其中在 ContextModule#74ContextModule#35 中 webpack 對 module 的路徑作了差別性修復。也就是說咱們能夠放心的經過module的libIdent來獲取模塊的路徑了。

在整個webpack的執行過程當中涉及到module id有三個鉤子:

before-module-ids -> optimize-module-ids -> after-optimize-module-ids

因此咱們只要在before-module-ids中作出修改就行了。

編寫插件:

'use strict'

class moduleIDsByFilePath {
    constructor(options) {}

    apply(compiler) {
        compiler.plugin('compilation', compilation => {
            compilation.plugin("before-module-ids", (modules) => {
                modules.forEach((module) => {
                    if(module.id === null && module.libIdent) {
                        module.id = module.libIdent({
                            context: this.options.context || compiler.options.context
                        })
                    }
                })
            })
        })
    }
}

module.exports = moduleIDsByFilePath

上面的其實已經被webpack抽成一個插件了:

NamedModulesPlugin

因此只須要在插件那一部分裏面添加上

new webpack.NamedModulesPlugin()

接下來對比下兩次構建先後文件的變化:

Hash: e5bc78237ca9a3ad31f8
Version: webpack 3.8.1
Time: 1508ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-ebd9bfc583f45a344630.js     541 kB       0  [emitted]  [big]  vendor
             pageB-432105effc229524c683.js    1.09 kB       1  [emitted]         pageB
             pageA-158bf2a923c98ab49be2.js    1.09 kB       2  [emitted]         pageA
           runtime-9ca4cebe90e444e723b9.js    5.88 kB       3  [emitted]         runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css   92 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB
Hash: 7dce5d9dc88f619522fe
Version: webpack 3.8.1
Time: 1422ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-ebd9bfc583f45a344630.js     541 kB       0  [emitted]  [big]  vendor
             pageB-432105effc229524c683.js    1.09 kB       1  [emitted]         pageB
             pageA-dae883ddaeff861761da.js  940 bytes       2  [emitted]         pageA
           runtime-c874a0c304fa03493296.js    5.88 kB       3  [emitted]         runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB

哇,咱們對比發現只有相關改動的文件和運行時代碼發生了改變,vendor和pageB相關都沒有發生改變。美滋滋~~

這下咱們達到了咱們的目的,咱們能夠去看看咱們構建後的代碼了:

__webpack_require__("./src/b.less")
const sum = __webpack_require__("./src/common.js")
const _ = __webpack_require__("./node_modules/lodash/lodash.js")

真的是變成了路徑,成功~~。可是新的問題貌似又來了,和以前的文件對比發現咱們的文件廣泛比以前的變大了。好吧,是咱們換成文件路徑的時候形成的。這個時候咱們能不能用hash來代替文件路徑呢?答案是能夠,官方也有插件能夠供咱們使用:

new webpack.HashedModuleIdsPlugin()

官方說 NamedModulesPlugin 適合在開發環境,而在生產環境下請使用 HashedModuleIdsPlugin。
這樣咱們就達成了使用hash來代替原來的module id使之穩定。並且構建後的代碼也不會變化太大。

本覺得能夠到此爲止了。可是細心的人會發現runtime文件每次編譯都發生了變化。是什麼致使呢的?來看看吧:
image
咱們觀察發現,在咱們的entry chunk數量沒有發生變化的時候,改變一個entry chunk的內容致使runtime內容發生變化的只有chunk id這個時候問題就又來了。根據上面穩定module id的操做同樣,數值型的chunk id不穩定性太大,咱們要換,方式和上面同樣。

  • 找到穩定chunk id的方式
  • 找到改變chunk id的時機

七、穩定chunk id的相關操做

找到穩定chunk id的方式

由於咱們知道webpack在打包的時候入口是具備惟一性的,那麼很簡單咱們能不可以用入口對應的name呢?因此這裏就比較簡單了咱們就用咱們的entry name來替換chunk id。

找到改變chunk id的時機

根據經驗module 有上面的過程那麼 chunk我以爲也是有的。

before-chunk-ids -> optimize-chunk-ids -> after-optimize-chunk-ids

因此編寫插件:

'use strict'

class chunkIDsByFilePath {
    constructor(options) {}

    apply(compiler) {
        compiler.plugin('compilation', compilation => {
            compilation.plugin('before-chunk-ids', chunks => {
                chunks.forEach(chunk => {
                    chunk.id = chunk.name
                })
            })
        })
    }
}

module.exports = chunkIDsByFilePath

不巧的是官方也有這個插件因此不用咱們寫。

NamedChunksPlugin

構建後的代碼裏面咱們能夠看到了:

/******/         script.src = __webpack_require__.p + "" + chunkId + "-" + {"vendor":"ed00d7222262ac99e510","pageA":"b5b4e2893bce99fd5c57","pageB":"34be879b3374ac9b2072"}[chunkId] + ".js";

原來的chunk id如今所有變成了entry name了,變動的風險又小了一點了。美滋滋~~

咱們換成名字後那麼問題又和上面module id換成name 又同樣的問題,文件會變大。這個時候仍是想到和上面的方式同樣用hash來處理。這個時候就真的要編寫插件了。安利一波咱們本身寫的
webpack-hashed-chunk-id-plugin

到此持久化緩存中遇到的核心難題都已經處理完了。

最後

若是你想要快速搭建一個項目,歡迎使用這邊的項目架構哦。
webpack-project-seed已經有線上項目用的用這個在跑了哦。順便star一個吧。

感謝:@pigcan

相關文章
相關標籤/搜索