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

更新时间:2017-02-03 11:26:08 点击次数:1808次

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

众所周知,对于一个站点而言,网站的加载时间一直都是一个很重要的指标。网页加载时间的长短直接影响到了站点的访问量。试想,正在看这篇文章的你,会有多少耐心等待一个网页慢悠悠的打开呢?

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

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

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

社区有人提供了这个插件用来替换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,从而引用了错误的模块导致发生了错误,中断了代码的运行。

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

二、从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%。

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


本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!