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

Posted CSDN云计算

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用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插件

社区有人提供了这个插件用来替换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%。

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

本文全部内容,请见《程序员》2017年2月期。

以上是关于使用Webpack打包单页应用的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章

手摸手,带你用合理的姿势使用webpack4(下

Vue2 + webpack + express4构建单页应用

vue项目静态资源(图片,字体)引用路径正确姿势

hbuilder+vue单页应用打包成APP后退按钮返回上一页的问题

Webpack入门

前端自动化构建工具Webpack开发模式入门指南