Webpack 4在2018年2月25日正式发布了,带来了很多新特性和性能提升,号称"零配置"开箱即用,构建速度比Webpack 3快了很多。

作为一个前端工程师,我对Webpack 4期待已久。发布之后,我就迫不及待地把项目从Webpack 3升级到了Webpack 4。本以为会很顺利,结果踩了一堆坑,熬了好几个通宵才搞定。

今天,我想记录一下我Webpack 4配置过程中踩过的坑,以及我是怎么解决这些问题的。希望能给正在升级或者使用Webpack 4的朋友一些参考,让你们少踩坑,少熬夜。

一、为什么要升级到Webpack 4

先说说我为什么要升级到Webpack 4。

我们的项目,之前用的是Webpack 3,用了快一年了,整体还可以,但是有几个痛点:

  1. 构建速度慢:项目大了之后,构建速度越来越慢,开发环境热更新要等好几秒,生产环境构建要好几分钟,严重影响开发效率。
  2. 配置复杂:Webpack 3的配置很复杂,loader、plugin、各种配置项,加起来好几百行,新人来了根本看不懂,维护起来很麻烦。
  3. 代码分割不够灵活:Webpack 3的代码分割(CommonsChunkPlugin)配置起来比较麻烦,而且不够灵活,很难做到按需加载和最优的代码分割。
  4. Tree Shaking效果不好:Webpack 3虽然支持Tree Shaking,但是效果不好,很多没用的代码还是被打包进去了,产物体积大。

Webpack 4针对这些痛点,做了很多改进:

  • 构建速度大幅提升:官方说比Webpack 3快了40%-98%,实际使用下来,确实快了很多
  • 零配置开箱即用:提供了mode配置,很多默认配置都帮你做好了,不需要写一大堆配置
  • 更灵活的代码分割:用SplitChunksPlugin替代了CommonsChunkPlugin,配置更灵活,效果更好
  • 更好的Tree Shaking:改进了Tree Shaking算法,能更有效地剔除没用的代码
  • 更好的性能优化:提供了很多性能优化的配置和工具,帮助你优化产物体积和加载速度

因为这些改进,我决定把项目升级到Webpack 4。现在看来,虽然踩了很多坑,但是升级是值得的,构建速度和产物体积都有了明显的改善。

二、升级过程中踩的坑

下面说说我升级过程中踩的具体的坑。

坑一:mode配置,必须设置

Webpack 4新增了mode配置,有三个可选值:development、production、none。这个配置是必须的,如果你不设置,Webpack会报警告,而且会默认用production模式。

我最开始升级的时候,不知道mode是必须的,没有设置,结果Webpack报警告:"The 'mode' option has not been set, webpack will fallback to 'production' for this value."而且,开发环境的构建产物被压缩了,没法调试,热更新也有问题。

后来我才知道,mode配置很重要,它会根据不同的模式,自动设置很多默认配置:

  • development模式:开启调试工具,不压缩代码,不做Tree Shaking,热更新更快,适合开发环境
  • production模式:压缩代码,做Tree Shaking,优化产物体积,适合生产环境
  • none模式:不做任何默认优化,所有配置都自己来

解决方法很简单,在配置文件里设置mode就好了:

module.exports = {
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
  // ...
}

或者,在命令行里用--mode参数:

webpack --mode development
webpack --mode production

设置了mode之后,很多默认配置Webpack都帮你做好了,不需要像Webpack 3那样手动配置了,比如:

  • development模式自动开启NamedModulesPlugin、HotModuleReplacementPlugin
  • production模式自动开启UglifyJsPlugin、ModuleConcatenationPlugin、NoEmitOnErrorsPlugin等

这就是Webpack 4"零配置"的体现,虽然不是真的零配置,但是确实少写了很多配置。

坑二:CommonsChunkPlugin被移除了

Webpack 4移除了CommonsChunkPlugin,用SplitChunksPlugin和RuntimeChunkPlugin替代了。

我最开始升级的时候,配置里还留着CommonsChunkPlugin,结果Webpack直接报错:"Error: webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead."

这个改动比较大,因为SplitChunksPlugin的配置方式和CommonsChunkPlugin完全不一样。我花了不少时间,才把CommonsChunkPlugin的配置迁移到SplitChunksPlugin。

SplitChunksPlugin的默认配置其实已经很不错了,大部分情况下,你只需要这样配置就够了:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
    runtimeChunk: true,
  },
}

这样,Webpack会自动把公共代码提取出来,把运行时代码单独打包,效果比CommonsChunkPlugin好很多。

如果你需要更精细的控制,可以配置更多参数:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      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,
        },
      },
    },
    runtimeChunk: {
      name: 'runtime',
    },
  },
}

SplitChunksPlugin比CommonsChunkPlugin灵活很多,功能也更强大。虽然迁移的时候花了点时间,但是迁移完之后,代码分割的效果更好了,产物体积也更小了。

坑三:很多插件不兼容Webpack 4

这是升级过程中最头疼的问题。Webpack 4的API有一些变化,导致很多第三方插件不兼容,需要升级到支持Webpack 4的版本,或者替换成其他插件。

我遇到的不兼容的插件有:

  1. extract-text-webpack-plugin:这个插件用来把CSS从JS中提取出来,但是它不兼容Webpack 4,而且官方已经不推荐用了,推荐用mini-css-extract-plugin替代。

- 解决方法:换成mini-css-extract-plugin,配置方式类似,但是API有一些区别。

  1. html-webpack-plugin:这个插件用来生成HTML文件,旧版本不兼容Webpack 4,需要升级到最新版本。

- 解决方法:升级到html-webpack-plugin@3.x以上版本。

  1. webpack-dev-server:旧版本的webpack-dev-server不兼容Webpack 4,需要升级到最新版本。

- 解决方法:升级到webpack-dev-server@3.x以上版本。

  1. uglifyjs-webpack-plugin:Webpack 4内置了UglifyJsPlugin,不需要再手动引入了。如果你还手动引入旧版本的uglifyjs-webpack-plugin,可能会有冲突。

- 解决方法:移除手动引入的uglifyjs-webpack-plugin,用Webpack 4内置的就好了。如果需要自定义配置,可以在optimization.minimizer里配置。

  1. copy-webpack-plugin:旧版本不兼容Webpack 4,需要升级。

- 解决方法:升级到最新版本。

  1. 一些比较小众的插件:有些比较小众的、维护不活跃的插件,可能还没有支持Webpack 4,这时候就需要找替代方案,或者自己fork一份修改。

我的建议是,升级Webpack 4之前,先检查一下你用的所有插件,看看有没有支持Webpack 4的版本。如果有,就一起升级;如果没有,就找替代方案。不要只升级Webpack,不升级插件,那样肯定会出问题。

坑四:loader配置的变化

Webpack 4对loader的配置也有一些变化,主要是module.rules的配置更严格了。

我遇到的问题有:

  1. loader的写法:Webpack 4推荐用"use"数组的方式来配置loader,而不是"loader"字符串的方式。虽然旧的写法还支持,但是会有警告。

- 推荐写法: ``javascript module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], } ``

  1. json-loader不再需要了:Webpack 4内置了对JSON文件的支持,不需要再配置json-loader了。如果你还配置了json-loader,会有警告。

- 解决方法:移除json-loader的配置。

  1. url-loader和file-loader的配置:这两个loader的配置基本没变,但是要注意limit参数的设置,合理设置图片转base64的阈值,优化产物体积。
  1. babel-loader的配置:babel-loader的配置基本没变,但是要注意babel的版本,推荐用babel 7(虽然babel 7还在beta阶段,但是已经可以用了),配合babel-preset-env,根据目标浏览器自动转译,产物体积更小。

坑五:开发环境的配置变化

Webpack 4对开发环境的配置也有一些变化,主要是webpack-dev-server和热更新的配置。

我遇到的问题有:

  1. 热更新的配置:Webpack 4的热更新配置更简单了,在development模式下,只需要在webpack-dev-server的配置里设置hot: true,就可以开启热更新,不需要再手动引入HotModuleReplacementPlugin了。

- 配置示例: ``javascript devServer: { hot: true, // ... } ``

  1. devtool的选择:Webpack 4推荐在development模式下用cheap-module-eval-source-map,构建速度快,调试也方便;在production模式下用source-map或者不生成source map。

- 配置示例: ``javascript devtool: process.env.NODE_ENV === 'production' ? 'source-map' : 'cheap-module-eval-source-map', ``

  1. webpack-dev-server的contentBase:contentBase的配置基本没变,但是要注意路径的设置,确保静态资源能正确访问。

坑六:生产环境的性能优化

Webpack 4在生产环境的性能优化方面,做了很多改进,但是也有一些需要注意的地方。

我做的优化有:

  1. 代码压缩:Webpack 4的production模式默认会用UglifyJsPlugin压缩代码,但是默认配置可能不是最优的。你可以自定义UglifyJsPlugin的配置,比如开启多进程压缩、缓存、去除console.log等。

- 配置示例: ```javascript const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = { optimization: { minimizer: [ new UglifyJsPlugin({ cache: true, parallel: true, uglifyOptions: { compress: { drop_console: true, // 去除console.log }, }, }), ], }, } ```

  1. Tree Shaking:Webpack 4的Tree Shaking效果更好了,但是要注意,Tree Shaking只对ES6模块生效,对CommonJS模块不生效。所以,要确保你的代码用的是ES6模块(import/export),而不是CommonJS(require/module.exports)。另外,babel-preset-env默认会把ES6模块转成CommonJS,要设置modules: false,保留ES6模块,这样Tree Shaking才能生效。

- babel配置示例: ``json { "presets": [ ["env", { "modules": false }] ] } ``

  1. Scope Hoisting:Webpack 4的production模式默认会开启ModuleConcatenationPlugin(Scope Hoisting),把模块合并到一个函数里,减少函数声明,提升运行时性能,也能减小产物体积。这个功能默认开启,不需要手动配置,但是要注意,Scope Hoisting只对ES6模块生效,对CommonJS模块不生效。
  1. 持久化缓存:为了提升构建速度,可以开启缓存,比如babel-loader的cacheDirectory,uglifyjs-webpack-plugin的cache等。这样,第二次构建的时候,会快很多。
  1. 多进程构建:可以用happypack或者thread-loader,把loader的处理放到多进程里,提升构建速度。对于大项目来说,效果很明显。

坑七:构建产物的文件名和路径

Webpack 4对构建产物的文件名和路径的配置基本没变,但是有一些需要注意的地方。

我遇到的问题有:

  1. contenthash的使用:Webpack 4推荐用contenthash来命名静态资源,这样文件内容变化的时候,文件名才会变化,有利于浏览器缓存。

- 配置示例: ``javascript output: { filename: '[name].[contenthash:8].js', chunkFilename: '[name].[contenthash:8].js', } ``

  1. 静态资源的路径:要注意publicPath的设置,确保静态资源的路径正确,特别是部署到CDN或者子路径的时候。
  1. html-webpack-plugin的模板:html-webpack-plugin的配置基本没变,但是要注意模板的路径和变量的使用,确保HTML能正确生成。

三、我的最终配置

经过一番踩坑和调优,我最终的Webpack 4配置大概是这样的(简化版):

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

const isProd = process.env.NODE_ENV === 'production';

module.exports = {
  mode: isProd ? 'production' : 'development',
  
  entry: {
    app: './src/index.js',
  },
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: isProd ? '[name].[contenthash:8].js' : '[name].js',
    chunkFilename: isProd ? '[name].[contenthash:8].js' : '[name].js',
    publicPath: '/',
  },
  
  devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
  
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
          },
        },
      },
      {
        test: /\.css$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          'postcss-loader',
        ],
      },
      {
        test: /\.less$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader',
        ],
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              name: 'images/[name].[hash:8].[ext]',
            },
          },
        ],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: 'fonts/[name].[hash:8].[ext]',
            },
          },
        ],
      },
    ],
  },
  
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: isProd ? {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true,
      } : false,
    }),
    ...(isProd ? [new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
    })] : []),
  ],
  
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
        commons: {
          name: 'commons',
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
    runtimeChunk: {
      name: 'runtime',
    },
  },
  
  devServer: {
    hot: true,
    port: 3000,
    historyApiFallback: true,
    proxy: {
      '/api': 'http://localhost:8080',
    },
  },
  
  resolve: {
    extensions: ['.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
};

这个配置,比我之前Webpack 3的配置简洁了很多,很多默认配置Webpack都帮我做好了。而且,构建速度快了很多,开发环境热更新从原来的3-5秒降到了1秒以内,生产环境构建从原来的5分钟降到了2分钟左右。产物体积也小了不少,JS体积减少了约20%,CSS体积减少了约15%。

四、升级建议和经验总结

最后,总结一下我升级Webpack 4的建议和经验。

建议一:先看官方文档和迁移指南

升级之前,一定要先看Webpack 4的官方文档和迁移指南,了解有哪些breaking changes,有哪些新特性,有哪些配置变化。这样升级的时候,心里有数,不会踩太多坑。

Webpack 4的迁移指南在这里:https://webpack.js.org/migrate/4/

建议二:逐步升级,不要一步到位

如果你的项目很大,配置很复杂,建议逐步升级,不要一步到位。可以先升级Webpack和核心插件,确保能跑起来,然后再逐步优化配置,做性能优化。这样风险小,出了问题也容易定位。

建议三:插件一起升级

升级Webpack的时候,一定要把相关的插件和loader也一起升级,确保它们都支持Webpack 4。不要只升级Webpack,不升级插件,那样肯定会出问题。

升级之前,可以先检查一下每个插件的最新版本,看看有没有支持Webpack 4。如果有,就一起升级;如果没有,就找替代方案。

建议四:利用mode配置,减少手动配置

Webpack 4的mode配置很强大,会根据不同的模式自动设置很多默认配置。要充分利用mode配置,不要像Webpack 3那样,什么都手动配置。这样不仅配置更简洁,而且不容易出错,也能享受到Webpack官方的优化。

建议五:用SplitChunksPlugin替代CommonsChunkPlugin

Webpack 4移除了CommonsChunkPlugin,用SplitChunksPlugin替代了。虽然迁移的时候有点麻烦,但是SplitChunksPlugin更灵活,功能更强大,效果也更好。建议花点时间学习一下SplitChunksPlugin的配置,把代码分割做好,这对产物体积和加载性能影响很大。

建议六:开启Tree Shaking和Scope Hoisting

Webpack 4的Tree Shaking和Scope Hoisting效果都很好,能有效减小产物体积,提升运行时性能。要确保这两个功能生效,需要注意:

  • 用ES6模块(import/export),不要用CommonJS
  • babel-preset-env设置modules: false,保留ES6模块
  • production模式下,这两个功能默认开启

建议七:做好构建性能优化

Webpack 4的构建速度已经比Webpack 3快很多了,但是对于大项目来说,还是可以进一步优化。比如:

  • 开启缓存(babel-loader的cacheDirectory,uglifyjs的cache等)
  • 用多进程构建(happypack、thread-loader)
  • 合理设置include/exclude,减少loader处理的文件
  • 用DllPlugin预编译第三方库(虽然Webpack 4的性能已经很好了,但是对于超大项目,还是有用)

建议八:做好产物体积分析

升级之后,建议用webpack-bundle-analyzer分析一下产物体积,看看哪些模块比较大,有没有可以优化的地方。比如,有没有把整个lodash都打包进去(可以用lodash-es或者按需引入),有没有把moment的所有语言包都打包进去(可以只引入需要的语言包),等等。

通过体积分析,可以发现很多可以优化的点,进一步减小产物体积。

结语

Webpack 4的升级,虽然踩了很多坑,熬了好几个通宵,但是最终的结果是好的。构建速度快了,产物体积小了,配置也更简洁了。

Webpack作为前端工程化的核心工具,一直在不断进化。Webpack 4的"零配置"理念,虽然还不是真正的零配置,但是已经比之前简单了很多。相信未来的Webpack会越来越好用,越来越智能。

如果你还在用Webpack 3,或者更老的版本,我建议你找个时间升级到Webpack 4。虽然升级的过程可能会踩一些坑,但是升级之后的收益是实实在在的。

希望我的踩坑记录,能给正在升级或者使用Webpack 4的朋友一些参考,让你们少踩坑,少熬夜。

最后,用一句话总结:"Webpack 4,值得升级。虽然有坑,但是坑后的风景很美。"

愿每一个前端工程师,都能玩转Webpack,构建出高性能的前端应用。