但願作瀏覽器長緩存?關於Webpack生成的Hash,你應該知道這些

童鞋,你看到這篇文章的時候極可能你只是在找一篇webpack的配置文章教學,可是聽老哥說一句,別去搜什麼startkit或者best practice文章,特別是中文的,若是你找到了,也記得看一下文章啥時候寫的,超過半年的文章就別看了,百分之92.6裏面的內容已通過期了。你想學webpack相關的姿式,最好的辦法就是:看文檔javascript

言歸正傳,這篇文章並不教你怎麼配置webpack,內容所有都是關於webpack生成文件的hash的。在打包出來的文件名上加上文件內容的hash是目前最多見的有效使用瀏覽器長緩存的方法,js文件若是有內容更新,hash就會更新,瀏覽器請求路徑變化因此更新緩存,若是js內容不變,hash不變,直接用緩存,PERFECT!因此全部的問題就留給如何更好得控制文件的hash了。vue

基本

首先咱們弄一個最簡單的webpack配置:java

const path = require('path')

module.exports = {
  entry: {
    app: path.join(__dirname, 'src/foo.js')
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.join(__dirname, 'dist')
  }
}

而咱們foo.js以下:react

import React from 'react'
console.log(React.toString())

注意這裏的output.filename你也能夠用[hash]而不是[chunkhash],可是這兩種生成的hash碼是不同的
使用hash以下:jquery

app.03700a98484e0f02c914.js  70.4 kB       0  [emitted]  app
   [6] ./src/foo.js 55 bytes {0} [built]
    + 11 hidden modules

使用chunkhash以下:webpack

app.f2f78b37e74027320ebf.js  70.4 kB       0  [emitted]  app
   [6] ./src/foo.js 55 bytes {0} [built]
    + 11 hidden modules

對於單個entry來講用哪一個都沒有問題,作例子期間使用的是webpack@3.8.1版本,這個版本webpack對於源碼沒有改動的狀況,已經修復了hash串會變的問題。可是在以前的版本有可能會出現對於同一份沒有修改的代碼進行修改,hash不一致的問題,因此無論你使用的版本會不會有問題,都建議使用接下去的配置。以後的配置都是用chunkhash做爲hash生成web

hash vs chunkhash

由於webpack要處理不一樣模塊的依賴關係,因此他內置了一個js模板用來處理依賴關係(後面稱爲runtime),這段js所以也會被打包的咱們最後bundle裏面。在實際項目中咱們經常須要將這部分代碼分離出來,好比咱們要把類庫分開打包的狀況,若是不單獨給runtime單獨生成一個js,那麼他會和類庫一塊兒打包,而這部分代碼會隨着業務代碼改變而改變,致使類庫的hash也每次都改變,那麼咱們分離出類庫就沒有意義了。因此這裏咱們須要給runtime單獨提供一個js。瀏覽器

修改配置以下:緩存

module.exports = {
  entry: {
    app: path.join(__dirname, 'src/foo.js')
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.join(__dirname, 'dist')
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
  ]
}

webpack的文檔中說明,若是給webpack.optimize.CommonsChunkPlugin的name指定一個在entry中沒有聲明的名字,那麼他會把runtime代碼打包到這個文件中,因此你這裏能夠任意指定你喜歡的name (ゝ∀・)bapp

那麼如今打包出來會是神馬樣的呢?

app.aed80e077eb0a6c42e65.js    68 kB       0  [emitted]  app
runtime.ead626e4060b3a0ecb1f.js  5.82 kB       1  [emitted]  runtime
   [6] ./src/foo.js 55 bytes {0} [built]
    + 11 hidden modules

咱們能夠看到,app和runtime的hash是不同的。那麼若是咱們使用hash而不是chunkhash呢?

app.357eff03ae011d688ac3.js    68 kB       0  [emitted]  app
runtime.357eff03ae011d688ac3.js  5.81 kB       1  [emitted]  runtime
   [6] ./src/foo.js 55 bytes {0} [built]
    + 11 hidden modules

從這裏就能夠看出hash和chunkhash的區別了,chunkhash會包含每一個chunk的區別(chunk能夠理解爲每一個entry),而hash則是全部打包出來的文件都是同樣的,因此一旦你的打包輸出有多個文件,你勢必須要使用chunkhash。

類庫文件單獨打包

在通常的項目中,咱們的類庫文件都不會常常更新,好比react,更多的時候咱們更新的是業務代碼。那麼咱們確定但願類庫代碼可以儘量長的在瀏覽器進行緩存,這就須要咱們單獨給類庫文件打包了,怎麼作呢?

修改配置文件:

module.exports = {
  entry: {
    app: path.join(__dirname, 'src/foo.js'),
    vendor: ['react']  // 全部類庫均可以在這裏聲明
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.join(__dirname, 'dist')
  },
  plugins: [
    // 單獨打包,app中就不會出現類庫代碼
    // 必須放在runtime以前
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
  ]
}

而後咱們來執行如下打包:

vendor.72d208b8e74b753cf09c.js    67.7 kB       0  [emitted]  vendor
    app.fdc2c0fe8694c1690cb3.js  494 bytes       1  [emitted]  app
runtime.035d95805255d39272ba.js    5.85 kB       2  [emitted]  runtime
   [7] ./src/foo.js 55 bytes {1} [built]
  [12] multi react 28 bytes {0} [built]
    + 11 hidden modules

vendor和app分開了,並且hash都不同,看上去很美好是否是?高興太早了年輕人。咱們再新建一個文件,叫bar.js,代碼以下:

import React from 'react'

export default function() {
  console.log(React.toString())
}

而後修改foo.js以下:

import bar from './bar.js'
console.log(bar())

從這個修改中能夠看出,咱們並無修改類庫相關的內容,咱們的vendor中應該依然只有react,那麼vendor的hash應該是不會變的,那麼結果如咱們所願嗎?

vendor.424ef301d6c78a447180.js    67.7 kB       0  [emitted]  vendor
    app.0dfe0411d4a47ce89c61.js  845 bytes       1  [emitted]  app
runtime.e90ad557ba577934a75f.js    5.85 kB       2  [emitted]  runtime
   [7] ./src/foo.js 45 bytes {1} [built]
   [8] ./src/bar.js 88 bytes {1} [built]
  [13] multi react 28 bytes {0} [built]
    + 11 hidden modules

很遺憾,webpack狠狠打了咱們的臉╮(╯_╰)╭

這是什麼緣由呢?這是由於咱們多加入了一個文件,對於webpack來講就是多了一個模塊,默認狀況下webpack的模塊都是以一個有序數列命名的,也就是[0,1,2....],咱們中途加了一個模塊致使每一個模塊的順序變了,vendor裏面的模塊的模塊id變了,因此hash也就變了。總結一下:

  1. app變化是由於內容發生了變化
  2. vendor變化時由於他的module.id發生了變化
  3. runtime變化時由於它自己就是維護模塊依賴關係的

那麼怎麼解決呢?

NamedModulePlugin和HashedModuleIdsPlugin

這兩個plugin讓webpack再也不使用數字給咱們的模塊進行命名,這樣每一個模塊都會有一個獨有的名字,也就不會出現增刪模塊致使模塊id變化引發最終的hash變化了。如何使用?

{
  plugins: [
    new webpack.NamedModulesPlugin(),
    // new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
  ]
}

NamedModulePlugin通常用在開發時,能讓咱們看到模塊的名字,可讀性更高,可是性能相對較差。HashedModuleIdsPlugin更建議在正式環境中使用。

咱們來看一下使用這個插件後,兩次打包的結果,修改前:

vendor.91148d0e2f4041ef2280.js      69 kB       0  [emitted]  vendor
    app.0228a43edf0a32a59426.js  551 bytes       1  [emitted]  app
runtime.8ed369e8c4ff541ad301.js    5.85 kB       2  [emitted]  runtime
[./src/foo.js] ./src/foo.js 56 bytes {1} [built]
   [0] multi react 28 bytes {0} [built]
    + 11 hidden modules

修改後:

vendor.91148d0e2f4041ef2280.js      69 kB       0  [emitted]  vendor
    app.f64e232e4b6d6a59e617.js  917 bytes       1  [emitted]  app
runtime.c12d50e9a1902f12a9f4.js    5.85 kB       2  [emitted]  runtime
[./src/bar.js] ./src/bar.js 88 bytes {1} [built]
   [0] multi react 28 bytes {0} [built]
[./src/foo.js] ./src/foo.js 43 bytes {1} [built]
    + 11 hidden modules

能夠看到vendor的hash沒有變化,HashedModuleIdsPlugin也是同樣的效果。貌似世界變得更和諧了d(`・∀・)b,是嗎?哈哈,並非!

async module

隨着咱們的系統變得愈來愈大,模塊變得不少,若是全部模塊一次性打包到一塊兒,那麼首次加載就會變得很慢。這時候咱們會考慮作異步加載,webpack原生支持異步加載,用起來很方便。

咱們再建立一個js叫作async-bar.js,在foo.js中:

import('./async-bar').then(a => console.log(a))

打包:

0.1415eebc42d74a3dc01d.js  131 bytes       0  [emitted]
 vendor.19a637337ab59d16fb34.js      69 kB       1  [emitted]  vendor
    app.f7e5ecde27458097680e.js    1.04 kB       2  [emitted]  app
runtime.c4caa7f9859faa94b02e.js    5.88 kB       3  [emitted]  runtime
[./src/async-bar.js] ./src/async-bar.js 32 bytes {0} [built]
[./src/bar.js] ./src/bar.js 88 bytes {2} [built]
   [0] multi react 28 bytes {1} [built]
[./src/foo.js] ./src/foo.js 92 bytes {2} [built]
    + 11 hidden modules

恩,這時候咱們已經看到,咱們的vendor變了,可是更可怕的還在後頭,咱們再建了一個模塊叫async-baz.js,同樣的在foo.js引用:

import('./async-baz').then(a => console.log(a))

而後再打包:

0.eb2218a5fc67e9cc73e4.js  131 bytes       0  [emitted]
      1.61c2f5620a41b50b31eb.js  131 bytes       1  [emitted]
 vendor.1eada47dd979599cc3e5.js      69 kB       2  [emitted]  vendor
    app.1f82033832b8a5dd6e3b.js    1.17 kB       3  [emitted]  app
runtime.615d429d080c11c1979f.js     5.9 kB       4  [emitted]  runtime
[./src/async-bar.js] ./src/async-bar.js 32 bytes {1} [built]
[./src/async-baz.js] ./src/async-baz.js 32 bytes {0} [built]
[./src/bar.js] ./src/bar.js 88 bytes {3} [built]
   [0] multi react 28 bytes {2} [built]
[./src/foo.js] ./src/foo.js 140 bytes {3} [built]
    + 11 hidden modules

恩,我能說髒話嗎?不能?(╯‵□′)╯︵┻━┻

爲啥每一個模塊的hash都變了啊?!!爲啥模塊又變成數字ID了啊?!!

好吧,言歸正傳,決絕辦法仍是有的,那就是NamedChunksPlugin,以前是用來處理每一個chunk名字的,彷佛在最新的版本中不須要這個也能正常打包普通模塊的名字。可是這裏咱們能夠用來處理異步模塊的名字,在webpack的plugins中加入以下代碼:

new webpack.NamedChunksPlugin((chunk) => { 
  if (chunk.name) { 
    return chunk.name; 
  } 
  return chunk.mapModules(m => path.relative(m.context, m.request)).join("_"); 
}),

再執行打包,兩次結果以下:

app.5faeebb6da84bedaac0a.js    1.11 kB           app  [emitted]  app
async-bar.js.457b1711c7e8c6b6914c.js  144 bytes  async-bar.js  [emitted]
     runtime.f263e4cd58ad7b17a4bf.js     5.9 kB       runtime  [emitted]  runtime
      vendor.05493d3691191b049e65.js      69 kB        vendor  [emitted]  vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {app} [built]
   [0] multi react 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 143 bytes {app} [built]
    + 11 hidden modules


         app.55e3f40adacf95864a96.js     1.2 kB           app  [emitted]  app
async-bar.js.457b1711c7e8c6b6914c.js  144 bytes  async-bar.js  [emitted]
async-baz.js.a85440cf862a8ad3a984.js  144 bytes  async-baz.js  [emitted]
     runtime.deeb657e46f5f7c0da42.js    5.94 kB       runtime  [emitted]  runtime
      vendor.05493d3691191b049e65.js      69 kB        vendor  [emitted]  vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 32 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {app} [built]
   [0] multi react 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 140 bytes {app} [built]
    + 11 hidden modules

能夠看到結果都是用名字而不是id了,並且不改改變的地方也都沒有改變

注意生成chunk名字的邏輯代碼你能夠根據本身的需求去改

使用上面的方式會有一些問題,好比使用.vue文件開發模式,m.request是一大串vue-loader生成的代碼,因此打包會報錯。固然你們能夠本身找對應的命名方式,在這裏我推薦一個webpack原生支持的方式,在使用import的時候,寫以下注釋:

import(/* webpackChunkName: "views-home" */ '../views/Home')

而後配置文件只要使用new NamedChunksPlugin()就能夠了,不須要本身再拼寫名字,由於這個時候咱們的異步chunk已經有名字了。

因此到這就結束了是嗎?真的,求你快結束吧,我想去吃我兩小時前買的烤鴨了。

~~~的吧,咱們還得搞點事情

增長更多的entry

修改webpack.config.js

{
  ...
  entry: {
    app: path.join(__dirname, 'src/foo.js'),
    vendor: ['react'],
    two: path.join(__dirname, 'src/foo-two.js')
  },
  ...
}

增長的enrty以下:

import bar from './bar.js'
console.log(bar)

import('./async-bar').then(a => console.log(a))
// import('./async-baz').then(a => console.log(a))

是的跟foo.js如出一轍,固然你能夠改邏輯,只須要記得引用bar.js就能夠。

而後咱們打包,結果會讓你想再次(╯‵□′)╯︵┻━┻

app.77b13a56bbc0579ca35c.js  612 bytes           app  [emitted]  app
async-bar.js.457b1711c7e8c6b6914c.js  144 bytes  async-bar.js  [emitted]
     runtime.bbe8e813f5e886e7134a.js    5.93 kB       runtime  [emitted]  runtime
         two.9e4ce5a54b4f73b2ed60.js  620 bytes           two  [emitted]  two
      vendor.8ad1e07bfa18dd78ad0f.js    69.5 kB        vendor  [emitted]  vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {vendor} [built]
   [0] multi react 28 bytes {vendor} [built]
[./src/foo-two.js] ./src/foo-two.js 143 bytes {two} [built]
[./src/foo.js] ./src/foo.js 143 bytes {app} [built]
    + 11 hidden modules

爲毛全部文件的hash都變化了啊?!!!逗我玩呢?

好吧,緣由是vendor做爲common chunk並不僅是包含咱們在entry中聲明的部分,他還會包含每一個entry中引用的公共代碼,有些時候你可能但願這樣的結果,但在咱們這裏,這就是我要解決的一個問題啊ლ(゚д゚ლ)

因此這裏怎麼作呢,在CommonsChunkPlugin裏面有一個參數,能夠用來告訴webpack咱們的vendor真的只想包含咱們聲明的內容:

{
  plugins: [
    ...
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity
    }),
  ]
}

這個參數的意思是儘量少的把公用代碼包含到vendor裏面。因而咱們又打包:

app.5faeebb6da84bedaac0a.js    1.13 kB           app  [emitted]  app
async-bar.js.457b1711c7e8c6b6914c.js  144 bytes  async-bar.js  [emitted]
     runtime.b0406822caa4d1898cb8.js    5.93 kB       runtime  [emitted]  runtime
         two.9be2d4a28265bfc9d947.js    1.13 kB           two  [emitted]  two
      vendor.05493d3691191b049e65.js      69 kB        vendor  [emitted]  vendor
[./src/async-bar.js] ./src/async-bar.js 32 bytes {async-bar.js} [built]
[./src/bar.js] ./src/bar.js 88 bytes {app} {two} [built]
   [0] multi react 28 bytes {vendor} [built]
[./src/foo-two.js] ./src/foo-two.js 143 bytes {two} [built]
[./src/foo.js] ./src/foo.js 143 bytes {app} [built]
    + 11 hidden modules

恩,熟悉的味道。

到這裏咱們跟webpack的hash變化之戰算是告一段落,大部分webpack打包出現問題的緣由是模塊命名的問題,因此解決辦法其實也就是給每一個模塊一個固定的名字。

最後咱們的配置以下:

const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: {
    app: path.join(__dirname, 'src/foo.js'),
    vendor: ['react'],
    two: path.join(__dirname, 'src/foo-two.js')
  },
  externals: {
    jquery: 'jQuery'
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.join(__dirname, 'dist')
  },
  plugins: [
    new webpack.NamedChunksPlugin((chunk) => { 
      if (chunk.name) { 
        return chunk.name; 
      } 
      return chunk.mapModules(m => path.relative(m.context, m.request)).join("_"); 
    }),
    new webpack.NamedModulesPlugin(),
    // new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
  ]
}

若是你遇到了其餘問題,你能夠給我留言,我會去嘗試解決,但願你們看完能有一些收穫( σ՞ਊ ՞)σ

參考文章:

  1. https://webpack.js.org/guides/caching/
  2. 一篇牛逼的blog
相關文章
相關標籤/搜索