vue-cli webpack 配置信息分析(optimization 及 plugins)

Posted by XuBaoshi on June 30, 2020

vue-cli webpack 配置信息分析(optimization 及 plugins)

使用 vue-cli3 生成 webpack 配置(default)

/img/vue-cli/1.png

项目生成和完毕后,切换到项目目录,执行命令 npx vue-cli-service inspect, 生成的 webpack 信息如下:

webpack 配置信息

webpack 中 module、chunk、bundle 的概念

  1. module webpack 支持 commonJS、ES6 等模块化规范。
  2. chunk chunk 是 webpack 根据功能拆分出来的,包含三种情况: 1. 项目入口文件 2. 动态引入的代码 import (*.js) 3. webpack splitChunks 拆分出来的代码,chunk 中包含 module 可能是一对一也可能是一对多。
  3. bundle 是 webpack 打包后的各个文件, 一般与 chunk 是一对一的关系。

optimization

splitChunks

splitChunks 的作用

  • 抽离相同的代码到一个共享块
  • 脚本懒加载,使得初始下载的代码更小

在 webpack4 之前通常使用的是 CommonsChunkPlugin 作为拆分工具。

CommonsChunkPlugin 弊端

example1:

entryA:  vue  vuex  someComponents
entryB:  vue axios someComponents
entryC: vue vuex axios someComponents
minchunks: 2

产出后的 chunk:

vendor-chunk:vue vuex axios
chunkA~chunkC: only the components

example2:

entryA:  vue  vuex  someComponents
asyncB:vue axios someComponents
entryC: vue vux axios someComponents
minchunks: 2
vendor-chunk:vue vuex
chunkA: only the components
chunkB: vue axios someComponents
chunkC: axios someComponents

CommmonsChunkPlugin 的思路是即将满足 minChunks 配置想所设置的条件的模块移到一个新的 chunk 文件中去,这个思路是基于父子关系的,也就是这个新产出的 new chunk 是所有 chunk 的父亲,在加载孩子 chunk 的时候,父亲 chunk 是必须要提前加载的。

/img/vue-cli/split.jpg

由于 webpack4 是开箱即用的,在没有配置情况下(没有 webpack.config.js)optimization.splitChunks 配置如下:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
}

splitChunks.chunks

chunks 选项有 async、initial、all

  • async 表示只从异步加载的模块进行拆分
  • initial 表示只从入口模块进行拆分
  • all 两者以上

splitChunks.minSize

被分割文件(没有被压缩前的体积)是否大于 splitChunks.minSize, 如果体积不够容保留在原来的引入文件中。

splitChunks.minChunks

被分割文件为被引用的数量大于或等于 splitChunks.minChunks, cacheGroups 内的约束条件可以单独设置。

splitChunks.maxAsyncRequests

按需加载的最大并行请求数。

splitChunks.maxInitialRequests

一个入口最大并行请求数。

splitChunks.automaticNameDelimiter

自动命名连接符, 默认 ~

splitChunks.cacheGroups

cacheGroups 中的每一可以单独配置,覆盖 cacheGroups 之外默认的配置。 cacheGroups 之外设置的约束条件比如说默认配置里面的 chunks、minSize、minChunks 等等都会作用于 cacheGroups,除了 test, priority、 reuseExistingChunk。

cacheGroups 为一个数组,每一个对象定义的是一个拆分规则。

splitChunks.cacheGroups.test

匹配指定的文件或目录如: node_modules。

splitChunks.cacheGroups.priority

权重,如:上面的 webpack 的默认配置中 vendors 与 default,vendors 默认匹配引用的 node_modules 的模块,default 匹配的是 minChunks(引用次数大于等于 2)的模块,会有一种情况两种匹配规则都满足,但 webpack 的规则是 priority 值越大,表示优先级越高,故匹配的包优先打到 vendors chunks 中。

splitChunks.cacheGroups.reuseExistingChunk

复用其他 chunk 内已拥有的模块 当 chunks 引用了已经存在的被抽离的 chunks 时不会新创建一个 chunk 而是复用 chunk。

vue-cli 配置:

splitChunks: {
  cacheGroups: {
    vendors: {
      name: 'chunk-vendors',
      test: /[\\\/]node_modules[\\\/]/,
      priority: -10,
      chunks: 'initial'
    },
    common: {
      name: 'chunk-common',
      minChunks: 2,
      priority: -20,
      chunks: 'initial',
      reuseExistingChunk: true
    }
  }
}

minimize、minimizer

minimize

minimize 默认为 true,默认使用 terser-webpack-plugin 压缩 bundle 代码, 如果对 terser-webpack-plugin 使用自定义配置,可以在 minimizer 中配置。同时 minimizer 中也可以使用其他的压缩插件进行处理, 如: uglifyjs。

terser-webpack-plugin 与 glifyjs-webpack-plugin

Terser 是一款兼容 ES2015 + 的 JavaScript 压缩器。与 UglifyJS(许多项目的早期标准)相比,它是面向未来的选择。有一个 UglifyJS 的分支—— uglify-es,但由于它不再维护,于是就从这个分支诞生出了一个独立分支,它就是 terser。主要保留了与 ugly -es 和 ugly -js@3 的 API 和 CLI 兼容性。

minimizer

minimizer 主要用来覆盖压缩处理器,可以配置一个到多个。

常用配置方式:

常用配置:

const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        cache: true,
        parallel: true,
        sourceMap: true, // Must be set to true if using source-maps in production
        terserOptions: {
          // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
        },
      }),
    ],
  },
}

函数形式配置:

module.exports = {
  optimization: {
    minimizer: [
      (compiler) => {
        const TerserPlugin = require('terser-webpack-plugin')
        new TerserPlugin({
          /* your config */
        }).apply(compiler)
      },
    ],
  },
}

使用 ... 作为默认配置

module.exports = {
  optimization: {
    minimizer: [new CssMinimizer(), '...'],
  },
}

vue-cli 配置:

const TerserPlugin = require('terser-webpack-plugin')

{
  minimizer: [
    /* config.optimization.minimizer('terser') */
    new TerserPlugin({
      terserOptions: {
        compress: {
          // 是否将代码转换成箭头函数(注意浏览器兼容)
          arrows: false,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: false,
          comparisons: false,
          // 将对象计算属性转换为对象常规属性
          computed_props: false,
          // 变量提升
          hoist_funs: false,
          // 属性提升
          hoist_props: false,
          // 变量提升
          hoist_vars: false,
          //
          inline: false,
          // 优化循环
          loops: false,
          // 对立即执行函数执行否定
          negate_iife: false,
          // 对象获取使用点操作符
          properties: false,
          reduce_funcs: false,
          reduce_vars: false,
          // 重复和删除不可到达的开关分支
          switches: false,
          // 在顶层范围内删除未引用的函数(“funcs”)和/或变量(“vars”)
          toplevel: false,
          // 转换 typeof foo == "undefined" into foo === void
          typeofs: false,
          // 优化布尔值变量
          booleans: true,
          // 优化  if/return and if/continue
          if_return: true,
          // 使用逗号操作连接多个语句
          sequences: true,
          // 去除未引用的代码
          unused: true,
          // 对 if-else 条件表达式进行优化
          conditionals: true,
          // 去除不会被执行的代码
          dead_code: true,
          // 尝试求取常量表达式的值
          evaluate: true,
        },
        // 代码混淆
        mangle: {
          safari10: true,
        },
      },
      // 是否生成 source-map 文件
      sourceMap: true,
      // 是否使用缓存 缓存默认存储在  `/.cache/terser-webpack-plugin`
      cache: true,
      // 是否开启多线程
      parallel: true,
      // 注释是否被拆分到一个单独的文件
      extractComments: false,
    }),
  ]
}

compress 具体参看 terser 配置。

plugins

vue-cli 配置:

plugins: [
  /* config.plugin('vue-loader') */
  new VueLoaderPlugin(),
  /* config.plugin('define') */
  new DefinePlugin({
    'process.env': {
      NODE_ENV: '"development"',
      BASE_URL: '"/"',
    },
  }),
  /* config.plugin('case-sensitive-paths') */
  new CaseSensitivePathsPlugin(),
  /* config.plugin('friendly-errors') */
  new FriendlyErrorsWebpackPlugin({
    additionalTransformers: [
      function () {
        /* omitted long function */
      },
    ],
    additionalFormatters: [
      function () {
        /* omitted long function */
      },
    ],
  }),
  /* config.plugin('html') */
  new HtmlWebpackPlugin({
    title: 'test',
    templateParameters: function () {
      /* omitted long function */
    },
    template: 'H:\\code\\own\\test\\public\\index.html',
  }),
  /* config.plugin('preload') */
  new PreloadPlugin({
    rel: 'preload',
    include: 'initial',
    fileBlacklist: [/\.map$/, /hot-update\.js$/],
  }),
  /* config.plugin('prefetch') */
  new PreloadPlugin({
    rel: 'prefetch',
    include: 'asyncChunks',
  }),
  /* config.plugin('copy') */
  new CopyPlugin([
    {
      from: 'H:\\code\\own\\test\\public',
      to: 'H:\\code\\own\\test\\dist',
      toType: 'dir',
      ignore: [
        '.DS_Store',
        {
          glob: 'index.html',
          matchBase: false,
        },
      ],
    },
  ]),
]

VueLoaderPlugin

vue-loader

// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  module: {
    rules: [
      // ... 其它规则
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
    ],
  },
  plugins: [
    // 请确保引入这个插件!
    new VueLoaderPlugin(),
  ],
}

vue-loader/lib/plugin 的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配 /.js$/ 的规则,那么它会应用到 .vue 文件里的

DefinePlugin

define-plugin

DefinePlugin 允许创建一个在编译时可以配置的全局常量。 如:process.env.NODE_ENV

CaseSensitivePathsPlugin

case-sensitive-paths-webpack-plugin

CaseSensitivePathsPlugin 要求被依赖的文件路径必须为精确匹配磁盘上的实际路径。

FriendlyErrorsWebpackPlugin

friendly-errors-webpack-plugin

FriendlyErrorsWebpackPlugin 可以识别 webpack 中的某些类错误,并对它们进行清理、聚合和排序,以提供更好的开发体验。

HtmlWebpackPlugin

html-webpack-plugin

HtmlWebpackPlugin 将会生成一个 HTML5 文件,简化了 HTML 文件的创建,这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。

如果有任何 CSS assets 在 webpack 的输出中(例如, 利用 ExtractTextPlugin 提取 CSS), 那么这些将被包含在 HTML head 中的 标签内。

HtmlWebpackPlugin 默认模板语法为 ejs,可以通过配置设置打包时间。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon" />
    <meta
      name="build-time"
      content="<%= htmlWebpackPlugin.options.buildTime %>"
    />
  </head>
  <body></body>
</html>
plugins: [
  new HtmlWebpackPlugin({
    template: 'src/index.html,
    buildTime: new Date().toString()
  })
]

多 html 文件

plugins: [
  new HtmlWebpackPlugin(), // Generates default index.html
  new HtmlWebpackPlugin({
    // Also generate a test.html
    filename: 'test.html',
    template: 'src/assets/test.html',
  }),
]

PreloadPlugin

preload-webpack-plugin

针对异步的 javascript 文件生成 代码块。

注: 此插件依赖 html-webpack-plugin

preload 与 prefetch

使用 preload 与 prefetch 可以有效的减少页面的首次加载时间,缩短用户的可交互时间,提高用户体验。

preload

preload 是声明式的 fetch,可以强制浏览器请求资源,同时不阻塞文档 onload 事件

prefetch

Prefetch 提示浏览器这个资源将来可能需要,但是把决定是否和什么时间加载这个资源的决定权交给浏览器

vue-cli3 中 入口文件使用 preload 处理文件的加载,对于异步的文件则使用 prefetch。

CopyPlugin

copy-webpack-plugin

将已经存在的文件拷贝到打包目录。