[前端必学]精准控制webpack处理文件名hash的问题

背景知识

1、静态资源首次被加载后浏览器会进行缓存,同一个资源在缓存未过期情况下一般不会再去请求,那么当资源有更新时如何通知浏览器资源有变化呢?资源文件命名 hash 化就是解决该问题而生;

2、对于浏览器来说,一方面期望每次请求页面资源时,获得的都是最新的资源;一方面期望在资源没有发生变化时,能够复用缓存对象。

这个时候,使用【文件名+文件哈希值】的方式,就可以实现只要通过文件名,就可以区分资源是否有更新。

写在前面

1、webpack 就内置了 hash 计算方法,对生成的文件可以在输出的文件中添加 hash 字段。

2、webpack 内置的 hash 有三种:

  • hash:每次构建会生成一个 hash。和整个项目有关,只要项目有文件更改,就会改变 hash
  • contenthash:和单个文件的内容有关。指定文件的内容发生改变,就会改变 hash
  • chunkhash:和 webpack 打包生成的 chunk 相关。每一个 entry ,都会有不用的 hash

正文

一、一个最基本的配置

const path = require('path')

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

src/foo.js 内容如下:

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

注意这里的 output.filename 你也可以用 [hash] 而不是 [chunkhash],但是这两种生成的 hash 码是不一样的。

使用 hash 如下:

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

使用 chunkhash 如下:

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 生成。

二、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.CommonsChunkPluginname 指定一个在entry 中没有声明的名字,那么他会把 runtime 代码打包到这个文件中,所以你这里可以任意指定你喜欢的 name

那么现在打包出来会是神马样的呢?

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

我们可以看到,appruntimehash 是不一样的。那么如果我们使用 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

从这里就可以看出 hashchunkhash 的区别了,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

vendorapp 分开了,而且 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,那么 vendorhash 应该是不会变的,那么结果如我们所愿吗?

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 变化时因为它本身就是维护模块依赖关系的

那么怎么解决呢?

NamedModulesPlugin 和 HashedModuleIdsPlugin

这两个 pluginwebpack 不再使用数字给我们的模块进行命名,这样每个模块都会有一个独有的名字,也就不会出现增删模块导致模块 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

可以看到 vendorhash 没有变化,HashedModuleIdsPlugin 也是一样的效果。

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 名字的,似乎在最新的版本中不需要这个也能正常打包普通模块的名字。但是这里我们可以用来处理异步模块的名字,在 webpackplugins 中加入如下代码:

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 如下:

// foo-two.js
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

恩,熟悉的味道。

到这里我们跟 webpackhash 变化之战算是告一段落,大部分 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'
    })
  ]
}

补充知识点

webpack 的 hash 原理

webpackhash 是通过 crypto 加密和哈希算法实现的,webpack 提供了 hashDigest(在生成 hash 时使用的编码方式,默认为 'hex')、hashDigestLength(散列摘要的前缀长度,默认为 20)、hashFunction(散列算法,默认为 'md5')、hashSalt(一个可选的加盐值)等参数来实现自定义hash;

CommonsChunkPlugin

相信大家如果听说过 webpack4 的更新,最大的感触应该就是去除了 CommonsChunkPlugin,毕竟官方 change log 写的篇幅最多的就是这个。

CommonsChunkPlugin 删除之后,改成使用 optimization.splitChunks 进行模块划分,有兴趣的可以去看以下官方的详细文档。

官方的说法是默认设置已经对大部分用户来说非常棒了,但是需要注意一个问题,默认配置只会对异步请求的模块进行提取拆分,如果要对 entry 进行拆分,需要设置optimization.splitChunks.chunks = 'all'。其他的内容大家就自己研究吧。

对应之前我们拆分 runtime 的情况,现在也有一个配置 optimization.runtimeChunk,设置为 true 就会自动拆分 runtime 文件。

UglifyJsPlugin

现在也不需要使用这个 plugin 了,只需要使用 optimization.minimizetrue 就行,production mode 下面自动为 true

optimization.minimizer 可以配置你自己的压缩程序。

—————————— 【正文完】——————————

前端学习交流群,想进来面基的,可以加群: 832485817685486827
前端顶级学习交流群(一) 前端顶级学习交流群(二)

写在最后: 约定优于配置 —— 软件开发的简约原则

——————————【完】——————————

我的:
个人网站: https://neveryu.github.io/neveryu/
Github: https://github.com/Neveryu
新浪微博: https://weibo.com/Neveryu
微信: miracle421354532

更多学习资源请关注我的新浪微博…好吗

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值