使用Webpack打包单页应用的正确姿势

导语:在现代前端工程中,模块化已经成了前端项目组织文件的标配,网站上线前都会把需要的相关模块预先打包、处理一番。然而打包的方式多种多样,如何才能最优雅的分离业务代码和依赖库?如何才能最高效的利用缓存?本文将会和大家分享饿了么前端团队总结的各方案优劣、踩过的坑,以及最终的解决方案。

对于前端而言,缩短网页加载时间的常见方式有:

  • 合并文件以减少网络请求数量。
  • 对静态文件设置长达一年的缓存,让浏览器直接从缓存里读取文件。

为了让更改过的文件能够生效,我们还会给每个文件的文件名里加上一段根据文件内容计算出的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-md5-hash插件

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,从而引用了错误的模块导致发生了错误,中断了代码的运行。

不久之后,社区里也有人提出了这个问题

二、从vendor.js中抽离出Webpack的运行时代码

有人指出,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不受到业务代码的影响。

三、使用NamedModulesPlugin

在尝试过第二个解决方案后,我们意识到问题的根源在于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%。

所以,我们对于这个解决方案仍然不是很满意。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值