浅入webpack4 高效简单的配置

752 阅读9分钟

前言

在vue-cli3中已经将webpack等详细配置(config)去除,我们配置webpack只能在vue.config.js里进行配置,这里我个人总结了一套webpack的优化方案模板并且附有我个人的讲解(尚在研究中)。 总体优化这几个方面:

  1. 提升生产打包的构建速度
  2. 拆分每个 npm 包
  3. 将稳定的第三方库(体积比较大的)改用cdn引入,不进行打包
  4. 安装可视化打包分析器(可选)

1.提升生产打包的构建速度

首先,你要知道运行在 Node.js 之上的 Webpack 是单线程模型的,所以Webpack 需要处理的事情需要一件一件的做,不能多件事一起做。我们需要Webpack 能同一时间处理多个任务,发挥多核 CPU 电脑的威力,HappyPack 就能让 Webpack 做到这点,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。 想了解happypack原理机制可以参考下面这篇文章,我主要讲解如何使用,不作过多剖析。 happypack原理详解

运行机制

在这里插入图片描述 首页利用npm安装happypack并在package文件的devDependencies节点写入依赖。

npm install happypack --save-dev
或
npm install happypack -D

在vue.config.js中:

const HappyPack = require('happypack')
/*
os 模块提供了一些基本的系统操作函数
os.cpus()
返回一个对象数组,包含所安装的每个 CPU/内核的信息:型号、速度(单位 MHz)、时间
(一个包含 user、nice、sys、idle 和 irq 所使用 CPU/内核毫秒数的对象)。
*/
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
  configureWebpack: {
      plugins: [
      new HappyPack({
        //用id来标识 happypack处理那里类文件 要和 rules 中指定的 id 对应起来
        id: 'babel',
        //如何处理  用法和loader 的配置一样
        loaders: ['babel-loader?cacheDirectory=true'],
        //共享进程池
        threadPool: happyThreadPool
      })
    ],
  }
   chainWebpack: config => {
    //在这里使用loader时候后面跟的id就是happypack处理那一类文件
    const jsRule = config.module.rule('js');
    jsRule.uses.clear();
    jsRule.use('happypack/loader?id=babel')
      .loader('happypack/loader?id=babel')
      .end();
   }
 }

这里要注意的是plugins中的id标识符要和loader中的 ?id=babel要一致,去告诉 happypack/loader 去选择哪个 HappyPack 实例去处理文件。 HappyPack 参数 id: String 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件. loaders: Array 用法和 webpack Loader 配置中一样. threads: Number 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。 verbose: Boolean 是否允许 HappyPack 输出日志,默认是 true。 threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。 verboseWhenProfiling: Boolean 开启webpack --profile ,仍然希望HappyPack产生输出。 debug: Boolean 启用debug 用于故障排查。默认 false。

注:这上面还引用了Node的OS模块,他提供了一些基本的系统操作函数。

2.拆分每个 npm 包

当我们运行项目并且打包的时候,会发现chunk-vendors.js这个文件非常大,那是因为webpack将所有的依赖全都压缩到了这个文件里面,这时我们可以将其拆分,将所有的依赖都打包成单独的js。

这里可以利用splitChunks将每个依赖包单独打包,拆分每个npm包。 cacheGroups其实是splitChunks里面最核心的配置,splitChunks就是根据cacheGroups去拆分模块的, splitChunks默认有两个缓存组:vender和default 我拿了这里的默认配置→_→ splitChunks的系统默认配置

//splitChunks的默认配置:
module.exports = {
  //...
    optimization{
        splitChunks: {
            /**
                1. 三个值 
                    async 仅提取按需载入的module
                    initial 仅提取同步载入的module
                    all 按需、同步都有提取
            */
            chunks: "async",
            // 只有导入的模块 大于 该值 才会 做代码分割 (单位字节)
            minSize: 30000,
            // 提取出的新chunk在两次压缩之前要小于多少kb,默认为0,即不做限制
            maxSize: 0,
            // 被提取的chunk最少需要被多少chunks共同引入
            minChunks: 1,
            // 按需加载的代码块(vendor-chunk)并行请求的数量小于或等于5个
            maxAsyncRequests: 5, 
            // 初始加载的代码块,并行请求的数量小于或者等于3个
            maxInitialRequests: 3,
            // 默认命名 连接符
            automaticNameDelimiter: '~',
            /**
                name 为true时,分割文件名为 [缓存组名][连接符][入口文件名].js
                name 为false时,分割文件名为 [模块id][连接符][入口文件名].js
                如果 缓存组存在 name 属性时 以缓存组的name属性为准
            */
            name: true,
            // 缓存组 当符合 代码分割的 条件时 就会进入 缓存组 把各个模块进行分组,最后一块打包
            cacheGroups: {
                // 如果 引入文件 在node_modules 会被打包 这个缓存组(vendors)
                vendors: {
                    // 只分割 node_modules文件夹下的模块
                    test: /[\\/]node_modules[\\/]/,
                    // 优先级 因为如果 同时满足 vendors、和default 哪个优先级高 就会打包到哪个缓存组
                    priority: -10
                },
                default: {
                    // 表示这个库 至少被多少个chunks引入,满足要求的包会被拆分出来
                    minChunks: 2,
                    priority: -20,
                    // 如果 这个模块已经 被分到 vendors组,这里无需在分割 直接引用 分割后的
                    reuseExistingChunk: true
                }
            }
        }
    } 
};
这里我们做个简单的配置,各取所需就好了。
module.exports = {
 optimization: {
/* runtimeChunk 将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是
基于内容 hash 出来的,所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。
*/
      runtimeChunk: 'single',
      splitChunks: {
        chunks: 'all',
        maxInitialRequests: Infinity, //最大初始化请求 
        minSize: 20000, // 依赖包超过20000bit将被单独打包
        cacheGroups: {  //设置缓存组用来抽取满足不同规则的chunk,这里以生成vendor为例
        //第三方库
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name(module) {
              //  拆分每个 npm 包
              const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
              return `npm.${packageName.replace('@', '')}`
            }
          }
        }
      }
 }
 }

上面的代码会将超过20000bit的依赖包单独打包,并且加上前缀npm.,如果你觉得第三方引入的库或包实在太大,莫方,接下来我要讲的就是解决这个问题的方法。

3.改用CDN引入第三方库

什么是CDN? 内容分发网络,加速网络传输,就是通过将资源部署到世界各地,用户访问时按照就近原则从最近的服务器获取资源,来提高获取资源的速度,cdn就是对http的提速。

这个方法推荐使用于那些版本稳定,体积较大的第三方库。 webpack中提供了externals配置用于第三方库脱离webpack打包,不被打入bundle中,从而减少打包时间,但又不影响运用第三方库的方式,例如import方式等。

module.exports = {
	chainWebpack: config => {
	    config.externals({
	      'vue': 'Vue',
	      'vuex': 'Vuex',
	      'vue-router': 'VueRouter',
	      'axios': 'axios',
	      'element-ui': 'ELEMENT',
	    })
	}
}

然后你需要在你项目中的根html使用cdn引入第三方库:

    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.runtime.min.js"></script>
    <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
    <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
    <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
    <script src="https://cdn.bootcss.com/element-ui/2.11.1/index.js"></script>

这种方式大大减少了打包之后的项目体积并且不影响项目中这些库的使用。 但是这种方式也有缺点,在项目加载的时候cdn依赖网络。不论是cdn还是打包在项目中,在首屏加载时候都一样会加载,只是第三方库在不在包里的区别。

关于打包之后的Map文件,可以通过productionSourceMap:boolen来舍取,Map文件占的体积非常大。

4.安装可视化打包分析器(可选)

安装:

npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
  configureWebpack:{
    plugins:[
      new BundleAnalyzerPlugin()
    ]
  }
}

BundleAnalyzerPlugin中的默认可选参数: // 可以是serverstaticdisabled。 // 在server模式下,分析器将启动HTTP服务器来显示软件包报告。 // 在“静态”模式下,会生成带有报告的单个HTML文件。 // 在disabled模式下,你可以使用这个插件来将generateStatsFile设置为true来生成Webpack Stats JSON文件。 analyzerMode: 'server', // 将在“服务器”模式下使用的主机启动HTTP服务器。 analyzerHost: '127.0.0.1', // 将在“服务器”模式下使用的端口启动HTTP服务器。 analyzerPort: 8888, // 路径捆绑,将在static模式下生成的报告文件。 // 相对于捆绑输出目录。 reportFilename: 'report.html', // 模块大小默认显示在报告中。 // 应该是statparsed或者gzip中的一个。 defaultSizes: 'parsed', // 在默认浏览器中自动打开报告 openAnalyzer: true, // 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成 generateStatsFile: false, // 如果generateStatsFiletrue,将会生成Webpack Stats JSON文件的名字。 // 相对于捆绑输出目录。 statsFilename: 'stats.json', // stats.toJson()方法的选项。 // 例如,您可以使用source:false选项排除统计文件中模块的来源。 // 在这里查看更多选项: // https//github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21 statsOptions: null, logLevel: 'info' // 日志级别。可以是'信息','警告','错误'或'沉默'。

效果如图 在这里插入图片描述

5.总结

附上上面所有的配置代码,一些没提到的,代码中都有注释。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HappyPack = require('happypack')
/*
os 模块提供了一些基本的系统操作函数
os.cpus()
返回一个对象数组,包含所安装的每个 CPU/内核的信息:型号、速度(单位 MHz)、时间
(一个包含 user、nice、sys、idle 和 irq 所使用 CPU/内核毫秒数的对象)。
*/
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const UglifyPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  publicPath: '',
  outputDir: "dist",
  assetsDir: 'static',
  lintOnSave: false,
  productionSourceMap: false,
  configureWebpack: {
    plugins: [
      new BundleAnalyzerPlugin({        
      	analyzerHost: '127.0.0.1',
      	//区分开发和生产环境的端口,若你的项目在开发环境运行的情况下进行打包他就会使用另一个端口号,不会产生冲突。
      	//这里需要在根目录下创建.env.development进行一个简单的配置 NODE_ENV=development 区分环境
        analyzerPort: process.env.NODE_ENV == 'development' ? 8888 : 8889,
      }),
      new HappyPack({
        //用id来标识 happypack处理那里类文件 要和 rules 中指定的 id 对应起来
        id: 'babel',
        //如何处理  用法和loader 的配置一样
        loaders: ['babel-loader?cacheDirectory=true'],
        //共享进程池
        threadPool: happyThreadPool
      })
    ],
    optimization: {
      runtimeChunk: 'single',
      splitChunks: {
        chunks: 'all',
        maxInitialRequests: Infinity, //最大初始化请求 
        minSize: 20000, // 依赖包超过20000bit将被单独打包
        cacheGroups: {  
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name(module) {
              //  拆分每个 npm 包
              const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
              return `npm.${packageName.replace('@', '')}`
            }
          }
        }
      }

    }
    //关闭webpack性能提示
    // performance: {
    //   // hints: 'warning',            
    //   //入口起点的最大体积 整数类型(以字节为单位)
    //   maxEntrypointSize: 50000000,
    //   //生成文件的最大体积 整数类型(以字节为单位 300k)
    //   maxAssetSize: 30000000,
    //   //只给出 js 文件的性能提示
    //   assetFilter: function(assetFilename) {
    //       return assetFilename.endsWith('.js');
    //   }
    // }
  },
  chainWebpack: config => {
    //改用CDN引入模块
    config.externals({
      'vue': 'Vue',
      'vuex': 'Vuex',
      'vue-router': 'VueRouter',
      'axios': 'axios',
      'element-ui': 'ELEMENT',
    })
    //在这里使用loader时候后面跟的id就是happypack处理那一类文件
    const jsRule = config.module.rule('js');
    jsRule.uses.clear();
    jsRule.use('happypack/loader?id=babel')
      .loader('happypack/loader?id=babel')
      .end();

  },

}

参考文章: vue-cli3 webpack配置 happypack原理解析 打包文件分析工具 核心功能optimization介绍 webpack官网