跟着Vue-cli来'学'并'改'Webpack之 打包优化

12,365 阅读3分钟

首先,我们要知道什么要用webpack来打包,这样打包有那些好处。我们可以简单的列出以下几点:

  • 单文件组件 (.vue文件)
  • 优化Vue构建过程 (alias等)
  • 浏览器缓存管理
  • 代码分离 (懒加载等)

这篇文章的重点讲的就是webpack打包之优化浏览器缓存管理,vue-cli生成的脚手架的配置中,已经做了很多对于打包,利用缓存的优化处理,本文将来学校其中知识,并且做出改动。

了解浏览器的缓存原理

在此之前呢,我们需要先了解浏览器缓存是怎么工作的,先抄了一张图。

  • 浏览器: 我需要 test.js
  • 服务器:找到了给你,并且在259200秒(一个月)内别来找我
  • 浏览器:好的,那我缓存到磁盘里

过了一个星期,再次访问这个页面

  • 浏览器:我需要test.js,缓存期限还在,直接从磁盘读取
  • 服务器:没我卵事
  • 用户:哇塞打开页面好快

应产品经理需求更改了一个图标

  • 浏览器:我需要test.js,缓存期限还在,直接从磁盘读取
  • 产品经理:发布了吗?你确定?怎么没效果啊?
  • 服务器:吃瓜

弄清了原理,我们就知道怎么去破坏缓存机制,让浏览器请求到新的文件。

清楚缓存技术

ctrl+F5 强制刷新页面

手动强制刷新页面,但是用户不是程序员啊,他们怎么会知道需要强制刷新呢,所以这个方案给用户肯定是不可行的。

更改文件

  1. 修改文件的名字:test.js -> test.v2.js
  2. 修改文件的路径:/static/test.js -> /static/v2/test.js
  3. 加 query string : test.js -> test.js?v=qwer

我们了解完了如何清楚缓存,再来看看Vue-cli模版中是如何进行配置的

Code Splitting(代码分割)

什么是代码分割

我们直接生成一个Vue-cli的新项目,安装依赖后直接运行 npm run build命令,并打开/dist/js文件目录

发现有3个js文件,这就是webpack将代码进行了分割。

为什么要进行代码分割

  1. 分离业务代码和第三方库( vendor )
  2. 按需加载(利用 import() 语法)

之所以把业务代码和第三方库代码分离出来,是因为产品经理的需求是源源不断的,因此业务代码更新频率大,相反第三方库代码更新迭代相对较慢且可以锁版本,所以可以充分利用浏览器的缓存来加载这些第三方库。

而按需加载的适用场景,比如说「访问某个路由的时候再去加载对应的组件」,用户不一定会访问所有的路由,所以没必要把所有路由对应的组件都先在开始的加载完;更典型的例子是「某些用户他们的权限只能访问某些页面」,所以没必要把他们没权限访问的页面的代码也加载

剖析 vue-cli 的 webpack Code Splitting

分离业务代码和第三方库( vendor )

vue-cli中使用了 CommonsChunkPlugin这个webpack插件来提取框架代码。 打开 webpack.prod.conf.js 文件,找到下面这段代码

new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: function (module, count) {
    // any required modules inside node_modules are extracted to vendor
    return (
      module.resource &&
      /\.js$/.test(module.resource) &&
      module.resource.indexOf(
        path.join(__dirname, '../node_modules')
      ) === 0
    )
  }
}),

这段代码在打包的时候把 node_modules 下面的 ,并且名字是 .js 结尾的,并且不是重复的模块提取到vender里面。

所以打包后应该会生成app.js(业务代码)、vender.js(框架代码)这个两个文件,细心的同学可能会发现还有个 manifest.js,在后面我们讲进行解读。

按需加载(利用 import() 语法)

如果我们修改一下hello组件的加载方式改为路由懒加载(import()语法),在进行打包

// import Hello from '@/components/Hello'

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Hello',
      // component: Hello,
      component: () => import('@/components/Hello')
    }
  ]
})

很明显的看到,打包后有4个js文件,仔细的同学还发现,app.js文件的大小加上新多出文件的大小,正好等于没有分割打包的app的大小。 这样等于异步加载的组件,是单独打包成了一个js,在页面首次加载的时候不需要加载他,等到请求相应的页面的时候在去服务器请求它,减小了页面首屏加载的时间。

vue-cli /webpack.prod.conf.js 中配置 output.chunkFilename 规定了打包异步文件的格式

output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 
  },

如何利用利用浏览器缓存

目标

假设我们现在有很多很多的静态文件,然后每次需要更新很多很多的文件,那是不是要手动地一个一个地修改文件的名字呢?我们的理想当然是:哪个文件更新了,就自动地生成一个新的文件名。

另外,如果我们打包出来的静态文件只有一个单独的 JavaScript 文件 app.js ,那么每次改动一点代码,app.js 的文件名肯定都会变。但实际上,我只改动了某个模块的代码(其他模块并没有修改),就破坏了其他模块的缓存,这显然没有充分利用到缓存啊。我们的目标是:

哪个模块更新了破坏他的缓存,没更新的模块继续利用缓存。

步骤1:增加hash值

上文中我们提到清楚缓存的三种方式:修改文件名,修改路径,给url加参数,webpack的做法是修改文件名。 vue-cli /webpack.prod.conf.js 中 output.chunkFilename 规定了打包异步文件的格式

output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),   
    // 规定文件名为 js文件夹下 Chunk.name . hash值 .js 的文件
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
    // 规定文件名为 js文件夹下 module id. hash值 .js 的文件
  },

这样的话给每个文件加上了hash值,那个文件发生了变化hash值就会改变

步骤2:提取manifast文件

为什么要提取manifast文件呢?

原因是 vendor chunk 里面包含了 webpack 的 runtime 代码(用来解析和加载模块之类的运行时代码)

这样会导致:即使你没有更改引入模块(vendor的模块没有发生变动的情况下,你仅仅修改了其他代码) 也会导致 vendor 的chunkhash值发生变化,从而破坏了缓存,达不到预期效果

vue-cli /webpack.prod.conf.js 提取 manifast

    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),

步骤3:根据模块的相对路径生成一个四位数的hash作为模块id

webpack 里每个模块都有一个 module id ,module id 是该模块在模块依赖关系图里按顺序分配的序号,如果这个 module id 发生了变化,那么他的 chunkhash 也会发生变化。

这样会导致:如果你引入一个新的模块,会导致 module id 整体发生改变,可能会导致所有文件的chunkhash发生变化,这显然不是我们想要的

这里需要用 HashedModuleIdsPlugin ,根据模块的相对路径生成一个四位数的hash作为模块id,这样就算引入了新的模块,也不会影响 module id 的值,只要模块的路径不改变的话。

vue-cli /webpack.prod.conf.js

    new webpack.HashedModuleIdsPlugin()

完成目标

至此如果我们改了某个模块的代码,是不会破坏其他模块的缓存,这就是我们想要实现的持久性缓存。

改造vue-cli中的webpack提升首页加载速度

分析

我们首先来看一个实际项目

运行 npm run build --report 可以查看打包分布图 我们发现最大的文件还是vendor,大部分框架代码都打包在这里面,而这些框架代码是不常变化的,也不需要每次进行打包。所以我们可以想办法把他们提取出来,挂到cdn上面去。

具体步骤

以 vue, vue-router,element-ui为例

步骤1 index.html cdn引入框架

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>demo-vue-project</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/element-ui/2.0.8/theme-chalk/index.css">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <script src="https://cdn.bootcss.com/vue/2.5.13/vue.min.js"></script>
    <script src="https://cdn.bootcss.com/vue-router/2.7.0/vue-router.min.js"></script>
    <script src="https://cdn.bootcss.com/element-ui/2.0.7/index.js"></script>
  </body>
</html>

步骤2 修改 build/webpack.base.conf.js

module.exports = {
  ...
  externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'element-ui': 'ELEMENT'
  },
  ...
}

步骤3 修改框架注册方式

修改 src/router/index.js

// import Vue from 'vue'
import VueRouter from 'vue-router'
// 注释掉
// Vue.use(VueRouter)
...

修改 src/main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import ELEMENT from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'

Vue.config.productionTip = false

Vue.use(ELEMENT)
Vue.prototype.$http = axios

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  template: '<App/>',
  components: { App }
})

打包

打包后我们发现vendor体积大大减小,因为库代码都用cdn加载了。但是这样会导致请求资源增多也有响应的代价,这仅可算是一个思路。 首屏问题的最终解决方案还是SSR

总结

至此 vue-cli中的打包配置,也有一些了解了。个人吐槽下webpack是真的复杂。观望和期待 parcel能来带不一样的体验。