为什么要优化打包?

  • 项目越做越大,依赖包越来越多,打包文件太大
  • 单页面应用首页白屏时间长,用户体验差

我们的目的

  • 减小打包后的文件大小
  • 首页按需引入文件
  • 优化 webpack 打包时间

    按需加载

    路由组件按需加载

    1. const router = [
    2. {
    3. path: '/index',
    4. component: resolve => require.ensure([], () => resolve(require('@/components/index')))
    5. },
    6. {
    7. path: '/about',
    8. component: resolve => require.ensure([], () => resolve(require('@/components/about')))
    9. }
    10. ]

    第三方组件和插件。按需加载需引入第三方组件

    1. // 引入全部组件
    2. import ElementUI from 'element-ui'
    3. import 'element-ui/lib/theme-chalk/index.css'
    4. Vue.use(ElementUI)
    5. // 按需引入组件
    6. import { Button } from 'element-ui'
    7. Vue.component(Button.name, Button)

    对于一些插件,如果只是在个别组件中用的到,也可以不要在 main.js 里面引入,而是在组件中按需引入

    1. // 在main.js引入
    2. import Vue from vue
    3. import Vuelidate from 'vuelidate'
    4. Vue.use(Vuelidate)
    5. // 按组件按需引入
    6. import { Vuelidate } from 'vuelidate'

    优化 loader 配置

  • 优化正则匹配

  • 通过 cacheDirectory 选项开启缓存
  • 通过 include、exclude 来减少被处理的文件。

    1. module: {
    2. rules: [
    3. {
    4. test: /\.js$/,
    5. loader: 'babel-loader?cacheDirectory',
    6. include: [resolve('src')]
    7. }
    8. ]
    9. }

    优化文件路径

  • extension 配置之后可以不用在 require 或是 import 的时候加文件扩展名,会依次尝试添加扩展名进行匹配。

  • mainFiles 配置后不用加入文件名,会依次尝试添加的文件名进行匹配
  • alias 通过配置别名可以加快 webpack 查找模块的速度。

    1. resolve: {
    2. extensions: ['.js', '.vue', '.json'],
    3. alias: {
    4. 'vue$': 'vue/dist/vue.esm.js',
    5. '@': resolve('src'),
    6. }
    7. },

    生产环境关闭 sourceMap

  • sourceMap 本质上是一种映射关系,打包出来的 js 文件中的代码可以映射到代码文件的具体位置,这种映射关系会帮助我们直接找到在源代码中的错误。

  • 打包速度减慢,生产文件变大,所以开发环境使用 sourceMap,生产环境则关闭。

    代码压缩

  • UglifyJS: vue-cli 默认使用的压缩代码方式,它使用的是单线程压缩代码,打包时间较慢

  • ParallelUglifyPlugin: 开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成

    两种方法使用如下:

    1. plugins: [
    2. new UglifyJsPlugin({
    3. uglifyOptions: {
    4. compress: {
    5. warnings: false
    6. }
    7. },
    8. sourceMap: true,
    9. parallel: true
    10. }),
    11. new ParallelUglifyPlugin({
    12. //缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回,
    13. //cacheDir 用于配置缓存存放的目录路径。
    14. cacheDir: '.cache/',
    15. sourceMap: true,
    16. uglifyJS: {
    17. output: {
    18. comments: false
    19. },
    20. compress: {
    21. warnings: false
    22. }
    23. }
    24. })
    25. ]

    打包速度和打包后的文件大小啊对比

方法 文件大小 打包速度
不用插件 14.6M 32s
UglifyJsPlugin 12.9M 33s
ParallelUglifyPlugi 7.98M 17s

提取公共代码

  • 相同资源重复被加载,浪费用户流量,增加服务器成本。
  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

webpack3 使用 CommonsChunkPlugin 的实现:

  1. plugins: [
  2. new webpack.optimize.CommonsChunkPlugin({
  3. name: 'vendor',
  4. minChunks: function(module, count) {
  5. console.log(module.resource, `引用次数${count}`)
  6. //"有正在处理文件" + "这个文件是 .js 后缀" + "这个文件是在 node_modules 中"
  7. return module.resource && /\.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, './node_modules')) === 0
  8. }
  9. }),
  10. new webpack.optimize.CommonsChunkPlugin({
  11. name: 'common',
  12. chunks: 'initial',
  13. minChunks: 2
  14. })
  15. ]

Webpack4 使用 splitChunks 的实现

  1. module.exports = {
  2. optimization: {
  3. splitChunks: {
  4. cacheGroups: {
  5. vendor: {
  6. priority: 1, //添加权重
  7. test: /node_modules/, //把这个目录下符合下面几个条件的库抽离出来
  8. chunks: 'initial', //刚开始就要抽离
  9. minChunks: 2 //重复2次使用的时候需要抽离出来
  10. },
  11. common: {
  12. //公共的模块
  13. chunks: 'initial',
  14. minChunks: 2
  15. }
  16. }
  17. }
  18. }
  19. }

CDN 优化

  • 随着项目越做越大,依赖的第三方 npm 包越来越多,构建之后的文件也会越来越大。
  • 再加上又是单页应用,这就会导致在网速较慢或者服务器带宽有限的情况出现长时间的白屏。

将 vue、vue-router、vuex、element-ui 和 axios 这五个库,全部改为通过 CDN 链接获取,在 index.html 里插入 相应链接。

  1. <head>
  2. <link rel="stylesheet" href="https://cdn.bootcss.com/element-ui/2.0.7/theme-chalk/index.css" />
  3. </head>
  4. <body>
  5. <div id="app"></div>
  6. <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  7. <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
  8. <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
  9. <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
  10. <script src="https://cdn.bootcss.com/element-ui/2.6.1/index.js"></script>
  11. <!-- built files will be auto injected -->
  12. </body>

在 webpack.config.js 配置文件

  1. module.exports = {
  2. ···
  3. externals: {
  4. 'vue': 'Vue',
  5. 'vuex': 'Vuex',
  6. 'vue-router': 'VueRouter',
  7. 'element-ui': 'ELEMENT',
  8. 'Axios':'axios'
  9. }
  10. },

卸载依赖的 npm 包,npm uninstall axios element-ui vue vue-router vuex
修改 main.js 文件里之前的引包方式

  1. // import Vue from 'vue'
  2. // import ElementUI from 'element-ui'
  3. // import 'element-ui/lib/theme-chalk/index.css'
  4. // import VueRouter from 'vue-router'
  5. import App from './App.vue'
  6. import routes from './router'
  7. import utils from './utils/Utils'
  8. Vue.use(ELEMENT)
  9. Vue.use(VueRouter)
  10. const router = new VueRouter({
  11. mode: 'hash', //路由的模式
  12. routes
  13. })
  14. new Vue({
  15. router,
  16. el: '#app',
  17. render: h => h(App)
  18. })

使用 HappyPack 多进程解析和处理文件

  • 由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情需要一件一件的做,不能多件事一起做。
  • HappyPack 就能让 Webpack 把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
  • HappyPack 对 file-loader、url-loader 支持的不友好,所以不建议对该 loader 使用。

使用方法如下:
1、HappyPack 插件安装: npm i -D happypack
2、webpack.base.conf.js 文件对 module.rules 进行配置

  1. module: {
  2. rules: [
  3. {
  4. test: /\.js$/,
  5. use: ['happypack/loader?id=babel'],
  6. include: [resolve('src'), resolve('test')],
  7. exclude: path.resolve(__dirname, 'node_modules')
  8. },
  9. {
  10. test: /\.vue$/,
  11. use: ['happypack/loader?id=vue']
  12. }
  13. ]
  14. }

3、在生产环境 webpack.prod.conf.js 文件进行配置

  1. const HappyPack = require('happypack')
  2. // 构造出共享进程池,在进程池中包含5个子进程
  3. const HappyPackThreadPool = HappyPack.ThreadPool({ size: 5 })
  4. plugins: [
  5. new HappyPack({
  6. // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
  7. id: 'babel',
  8. // 如何处理.js文件,用法和Loader配置中一样
  9. loaders: ['babel-loader?cacheDirectory'],
  10. threadPool: HappyPackThreadPool
  11. }),
  12. new HappyPack({
  13. id: 'vue', // 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件
  14. loaders: [
  15. {
  16. loader: 'vue-loader',
  17. options: vueLoaderConfig
  18. }
  19. ],
  20. threadPool: HappyPackThreadPool
  21. })
  22. ]