从实践中寻找webpack4最优配置

6,860

笔者最近在准备给fle-cli升级到webpack4版本,觉得有必要将探索过程的经验分享给大家,遂决定写这篇文章。(不知道fle-cli看这里

webpack4是大趋势,升级是必然的,那么为什么现在才升级?

原因有以下几个方面:

  • 刚发布的版本还不稳定,潜在风险大,而目前版本已更新到4.8.3,基本处于稳定;
  • webpack社区工具未完全跟上节奏,好多工具都得自己搞,劳心劳力(其实主要就是懒哈哈);
  • webpack本身及社区工具存在或多或少的问题,未经时间沉淀,维护成本高。

然而现在,笔者认为以上这些已经成熟,是时候来一波升级了。

前言

本文不会讲解webpack配置的每个细节点,因为这些官方文档都可以看到。笔者会挑一些难以理解的新概念、可能会碰到的问题,以及笔者总结下来的优化方案来分享,希望可以给大家带来一些帮助。

配置

mode

mode是webpack4新增的参数选项,它的值有3个:development、production、none,能够帮助我们加载一些默认配置,none即不加载默认配置。下面将对应的默认配置列出来供大家参考,以免重复配置。

development

注重提升代码构建速度和开发体验

module.exports = {
  cache: true,
  devtools: "eval",
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") })
  ]
}

prodution

提供代码优化,如压缩、作用域提升等

var UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJsPlugin(/* ... */),
    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
}

optimization

这个选项也是webpack4新增的,主要是用来自定义一些优化打包策略。

minimizer

在production模式,该配置会默认为我们压缩混淆代码,但这显然满足不了我们对于优化代码的诉求。下面笔者分享一套自身实践总结下来的配置及解释:


var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      // 自定义js优化配置,将会覆盖默认配置
      new UglifyJsPlugin({
        exclude: /\.min\.js$/, // 过滤掉以".min.js"结尾的文件,我们认为这个后缀本身就是已经压缩好的代码,没必要进行二次压缩
        cache: true,
        parallel: true, // 开启并行压缩,充分利用cpu
        sourceMap: false,
        extractComments: false, // 移除注释
        uglifyOptions: {
          compress: {
            unused: true,
            warnings: false,
            drop_debugger: true
          },
          output: {
            comments: false
          }
        }
      }),
      // 用于优化css文件
      new OptimizeCssAssetsPlugin({
        assetNameRegExp: /\.css$/g,
        cssProcessorOptions: {
          safe: true,
          autoprefixer: { disable: true }, // 这里是个大坑,稍后会提到
          mergeLonghand: false,
          discardComments: {
            removeAll: true // 移除注释
          }
        },
        canPrint: true
      })
    ]
  }
}

UglifyJsPlugin这款插件相信大家也是经常用到,这里不再多说,这里的亮点是过滤掉本身已经是压缩的js文件,能够提升我们的编译效率以及避免二次混淆压缩而造成的未知bug。

OptimizeCssAssetsPlugin这款插件主要用来优化css文件的输出,默认使用cssnano,其优化策略主要包括:摈弃重复的样式定义、砍掉样式规则中多余的参数、移除不需要的浏览器前缀等,更多优化规则看这里。前文我们提到这里有个大坑,相信你已经察觉到了,没错,就是这货把我们通过autoprefixer加好了前缀给移除了。笔者查阅了许多资料,依旧没有找到满意的答案,没办法,只有硬着头皮去源码中找答案了,于是便有了这段配置autoprefixer: { disable: true },禁用掉cssnano对于浏览器前缀的处理。

runtimeChunk

分离出webpack编译运行时的代码,也就是我们先前称为manifest的代码块,好处是方便我们做文件的持久化缓存。它可以设置多种类型的值,具体可以看这里,其中single即将所有chunk的运行代码打包到一个文件中,multiple就是给每一个chunk的运行代码打包一个文件。

我们可以配合InlineManifestWebpackPlugin插件将运行代码直接插入html文件中,因为这段代码非常少,这样做可以避免一次请求的开销,但是新版插件的配置和之前有些不太一样,接下来详细讲解一下如何配置。

var HtmlWebpackPlugin = require('html-webpack-plugin')
var InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')

module.exports = {
  entry: {
    app: 'src/index.js'
  },
  optimization: {
    runtimeChunk: 'single'
    // 等价于
    // runtimeChunk: {
    //   name: 'runtime'
    // }
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'fle-cli',
      filename: 'index.html',
      template: 'xxx',
      inject: true,
      chunks: ['runtime', 'app'], // 将runtime插入html中
      chunksSortMode: 'dependency',
      minify: {/* */}
    }),
    new InlineManifestWebpackPlugin('runtime')
  ]
}

这段配置会产生一个叫做runtime的代码块,和老版本不同的是,我们并不需要在html模版中添加<%= htmlWebpackPlugin.files.webpackManifest %>,只需将runtime加入chunks即可。这里有一个点要注意,InlineManifestWebpackPlugin插件的顺序一定要在HtmlWebpackPlugin之后,否则会导致编译失败。

splitChunks

终于要讲到重头戏了,也是笔者个人认为最难以理解的一个配置项。webpack4移除了CommonsChunkPlugin插件,取而代之的是splitChunks。

我们先来看下默认配置:

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
    }
  }
}

默认配置只会作用于异步加载的代码块,它限制了分离文件的最小体积,即30KB(注意这个体积是压缩之前的),这个是前提条件,然后它有两个分组:属于node_modules模块,或者被至少2个入口文件引用,它才会被打包成独立的文件。

为什么要限制最小体积呢?因为webpack认为小于30KB的代码块分离出来,还要额外消耗一次请求去加载它,成本太高,当然这个值也不是随便意淫出来的,而是经过大量的实践总结得到的,笔者个人认为这是一个非常好策略。

maxAsyncRequests(最大的异步请求数)和maxInitialRequests(最大的初始请求数)这两个参数则是为了限制代码块划分的过于细致,导致大量的文件请求。

但是只分离异步代码块显然满足不了我们的需求,因此接下来笔者分享一套相对来说比较优雅的分离打包配置:

splitChunks: {
  cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      name: 'vendors',
      minSize: 30000,
      minChunks: 1,
      chunks: 'initial',
      priority: 1 // 该配置项是设置处理的优先级,数值越大越优先处理
    },
    commons: {
      test: /[\\/]src[\\/]common[\\/]/,
      name: 'commons',
      minSize: 30000,
      minChunks: 3,
      chunks: 'initial',
      priority: -1,
      reuseExistingChunk: true // 这个配置允许我们使用已经存在的代码块
    }
  }
}

首先是将node_modules的模块分离出来,这点就不再累述了。异步加载的模块将会继承默认配置,这里我们就不需要二次配置了。

第二点是分离出共享模块,笔者认为一个优雅的项目结构,其公共代码(或者称为可复用的代码)应该是放置于同一个根目录下的,基于这点我们可以将src/common中的公用代码提取出来。

当然你还可以有另外一种选择,将后缀为.js且使用次数超过3次的文件提取出来,但是笔者不建议这个做,因为这不利于持久化缓存,新增或删除文件都有可能影响到使用次数,从而导致原先的公共文件失效。

文末

原先还想着讲一下css插件部分的配置,限于篇幅,本文就不再进行讲解说明了,感兴趣的小哥哥小姐姐可以在这里翻看源码:webpack4-test

顺便在这里推荐一款好用的全局通用脚手架fle-cli:旨在帮助我们从复杂繁琐的编译配置中解放出来,全身心地投入业务开发中,提高开发效率;同时它也是真正意义上的全局脚手架,区别于市面上其他的全局脚手架,它不会在项目工程中生成各种编译配置文件,也不会给你安装一系列编译的依赖包,这意味着你的项目工程可以非常干净纯粹。