Webpack 4在2018年2月正式发布带来了很多新特性和改进,比如零配置模式(mode)更快的构建速度更好的默认优化等等是目前最热门的前端构建工具之一。

但是从Webpack 3升级到Webpack 4或者从零开始配置Webpack 4也会遇到很多坑很多Webpack 3的配置在Webpack 4里不适用了,或者有变化很多Plugin和Loader也需要升级到支持Webpack 4的版本。

我最近在项目中使用Webpack 4踩了不少坑今天总结一下Webpack 4配置的踩坑经验和,实战技巧希望能帮大家少走弯路。

一、mode模式:Webpack 4最重要的新特性

Webpack 4最重要的新特性就是mode(模式)配置有三个可选值:developmentproductionnone不同的模式会自动开启不同的默认配置和优化。

development模式

开发模式会自动开启以下配置:

  • 开启调试工具(devtool: eval)
  • 开启缓存(cache: true)
  • 输出路径信息(output.pathinfo: true)
  • 关闭性能警告(performance.hints: false)
  • 开启NamedModulesPlugin和,NamedChunksPlugin方便调试
  • 不压缩代码不做Tree Shaking不做作用域提升

开发模式注重开发体验和调试方便构建速度快,但是打包体积大。

production模式

生产模式会自动开启以下配置:

  • 开启性能警告(performance.hints: warning)
  • 开启代码压缩(minimize: true使用TerserPlugin)
  • 开启Tree Shaking(usedExports: true)
  • 开启作用域提升(concatenateModules: trueModuleConcatenationPlugin)
  • 开启NoEmitOnErrorsPlugin编译出错不输出文件
  • 开启SplitChunksPlugin自动分割公共代码
  • 开启FlagDependencyUsagePlugin和,FlagIncludedChunksPlugin优化依赖

生产模式注重打包体积和性能优化打包体积小,但是构建速度慢。

none模式

不开启任何默认配置完全由用户自己配置适合高级用户自定义配置。

踩坑经验

  1. 一定要配置mode:Webpack 4如果不配置mode会给出警告,而且默认使用production模式这在开发环境会导致构建慢,而且代码被压缩不好调试,所以一定要根据环境配置正确的mode开发环境用development生产环境用production。
  1. 不要重复配置mode已经开启的功能:比如在production模式下Webpack 4已经自动开启了代码压缩Tree Shaking作用域提升代码分割等等不需要再手动配置这些Plugin了,否则可能会冲突,或者重复,比如不需要再手动引入UglifyJsPlugin了Webpack 4已经内置了TerserPlugin来做压缩。
  1. mode和环境变量的区别:mode是Webpack 4的配置会影响Webpack的内部优化而环境变量(process.env.NODEENV)是给业务代码用的两者不是一回事,但是Webpack 4会根据mode自动设置process.env.NODEENVdevelopment模式设置为developmentproduction模式设置为production所以一般不需要再手动用DefinePlugin设置NODE_ENV了。

二、入口和出口配置

入口(entry)和出口(output)是Webpack最基本的配置在,Webpack 4里也有一些变化和坑。

入口配置

入口配置告诉Webpack从哪个文件开始构建支持字符串数组对象三种形式。

// 单入口字符串
entry: './src/index.js'

// 多入口数组(合并为一个chunk)
entry: ['./src/index.js', './src/vendor.js']

// 多入口对象(每个入口一个chunk)
entry: {
  main: './src/index.js',
  vendor: './src/vendor.js'
}

踩坑经验

  1. 多入口的命名:多入口用对象形式时key就是chunk的名称会影响出口文件的命名和,代码分割的配置要注意命名规范。
  1. 入口文件不要太多:虽然Webpack支持多入口,但是入口不要太多,否则会导致构建慢,而且管理复杂一般一个页面一个入口就够了。

出口配置

出口配置告诉Webpack构建后的文件输出到哪里怎么命名。

output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].[hash:8].js',
  chunkFilename: '[name].[chunkhash:8].js',
  publicPath: '/',
  crossOriginLoading: 'anonymous'
}

踩坑经验

  1. hash、chunkhash、contenthash的区别:这是最容易搞混的地方:

- hash:整个项目的hash任何一个文件修改整个项目的hash都会变所有文件的缓存都失效不推荐使用。 - chunkhash:每个chunk的hash只有这个chunk的内容变化hash才会变其他chunk的缓存不受影响推荐用于JS文件。 - contenthash:每个文件内容的hash只有这个文件的内容变化hash才会变推荐用于CSS文件(因为CSS是从JS中提取出来的,如果用chunkhashJS变化CSS的hash也会变,即使CSS内容没变)。

  1. publicPath的配置:publicPath是静态资源的公共路径会影响图片字体CSSJS等资源的引用路径,如果配置错了会导致资源加载404开发环境一般用'/'生产环境,如果部署在CDN或者子路径下需要配置为对应的路径,比如'/assets/'或者'https://cdn.example.com/'。
  1. path必须是绝对路径:output.path必须是绝对路径不能是相对路径,否则会报错一般用path.resolve(__dirname, 'dist')来生成绝对路径。

三、Loader配置

Loader负责处理非JS文件,比如CSS图片字体VueReact等等是Webpack配置的重要部分在Webpack 4里也有一些变化和坑。

常用Loader

  • babel-loader:处理JS/JSX转译ES6+为ES5
  • css-loader:处理CSS文件解析@import和url()
  • style-loader:把CSS插入到DOM中(开发环境用)
  • postcss-loader:处理CSS后处理,比如自动加前缀(autoprefixer)
  • sass-loader/less-loader:处理Sass/Less预处理器
  • file-loader:处理文件(图片字体等等)输出文件返回路径
  • url-loader:处理文件小文件转base64大文件用file-loader输出
  • vue-loader:处理Vue单文件组件
  • ts-loader:处理TypeScript文件

踩坑经验

  1. Loader的执行顺序:Loader的执行顺序是从右到左从下到上,比如:
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']

执行顺序是sass-loader -> postcss-loader -> css-loader -> style-loader不要搞反顺序,否则会出错,或者不生效。

  1. url-loader和file-loader的配合:url-loader是file-loader的增强支持把小文件转成base64减少HTTP请求,但是大文件还是需要用file-loader输出,所以一般配置url-loader的limit参数小于limit的文件转base64大于limit的文件自动调用file-loader输出注意需要,同时安装url-loader和file-loader。
  1. babel-loader的exclude:babel-loader要配置exclude排除nodemodules目录,否则会把nodemodules里的JS也转译导致构建非常慢一般配置exclude: /node_modules/。
  1. CSS提取:生产环境需要把CSS从JS中提取出来单独生成CSS文件Webpack 4推荐使用mini-css-extract-plugin而不是以前的extract-text-webpack-plugin因为extract-text-webpack-plugin不支持Webpack 4的很多新特性,而且有bugmini-css-extract-plugin是Webpack官方推荐的CSS提取插件支持Webpack 4的所有新特性。
  1. 图片压缩:图片一般占网页体积的大部分需要压缩图片可以用image-webpack-loader配合url-loader或者file-loader使用在图片输出前压缩图片减少体积注意image-webpack-loader依赖很多二进制工具安装可能会慢,或者失败需要注意。

四、Plugin配置

Plugin负责扩展Webpack的功能,比如打包优化资源管理环境变量注入等等是Webpack配置的重要部分在Webpack 4里很多Plugin有变化,或者被内置了。

常用Plugin

  • HtmlWebpackPlugin:自动生成HTML文件自动引入打包后的JS/CSS
  • MiniCssExtractPlugin:提取CSS为单独文件(生产环境用)
  • CleanWebpackPlugin:构建前清理输出目录
  • CopyWebpackPlugin:复制文件到输出目录
  • DefinePlugin:注入全局常量(环境变量等等)
  • HotModuleReplacementPlugin:热模块替换(开发环境用)
  • BundleAnalyzerPlugin:分析打包体积可视化展示
  • TerserPlugin:代码压缩(Webpack 4内置production模式自动开启)
  • OptimizeCSSAssetsPlugin:CSS压缩

踩坑经验

  1. Webpack 4内置了很多Plugin:Webpack 4在production模式下已经内置了很多优化Plugin比如TerserPlugin(代码压缩)ModuleConcatenationPlugin(作用域提升)NoEmitOnErrorsPlugin(出错不输出)SplitChunksPlugin(代码分割)等等不需要再手动引入这些Plugin了,否则可能会冲突,或者重复配置。
  1. extract-text-webpack-plugin不要用了:很多人从Webpack 3升级上来还在用extract-text-webpack-plugin来提取CSS但是这个Plugin不支持Webpack 4的很多新特性,而且有很多bug比如CSS的contenthash不生效等等Webpack 4推荐使用mini-css-extract-plugin来提取CSS这是官方推荐的支持Webpack 4的所有新特性。
  1. CommonsChunkPlugin被移除了:Webpack 3用CommonsChunkPlugin来分割公共代码,但是Webpack 4移除了CommonsChunkPlugin改用optimization.splitChunks来做代码分割功能更强大配置更灵活从Webpack 3升级的时候,要注意把CommonsChunkPlugin的配置改成splitChunks的配置。
  1. UglifyJsPlugin被TerserPlugin取代了:Webpack 3用UglifyJsPlugin来压缩JS但是Webpack 4改用TerserPlugin来压缩JS因为Terser支持ES6+的压缩而UglifyJS不支持ES6+Webpack 4内置了TerserPluginproduction模式自动开启不需要手动引入UglifyJsPlugin了。
  1. HtmlWebpackPlugin的版本:HtmlWebpackPlugin要升级到支持Webpack 4的版本(3.x以上)旧版本不支持Webpack 4会报错,或者不生效。

五、代码分割(Code Splitting)

代码分割是Webpack 4最重要的优化功能之一能把代码分割成多个文件按需加载减少首屏加载体积提升页面加载速度。

Webpack 4用optimization.splitChunks来配置代码分割功能强大配置灵活。

默认配置

Webpack 4在production模式下会自动开启splitChunks默认配置如下:

optimization: {
  splitChunks: {
    chunks: 'async', // 只分割异步加载的chunk
    minSize: 30000, // 最小30KB才分割
    maxSize: 0, // 最大体积0表示不限制
    minChunks: 1, // 最少被引用1次才分割
    maxAsyncRequests: 5, // 最大异步请求数
    maxInitialRequests: 3, // 最大初始请求数
    automaticNameDelimiter: '~', // 文件名分隔符
    name: true, // 自动生成文件名
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}

默认配置只分割异步加载的chunk(chunks: 'async')同步加载的chunk不分割这对首屏优化不够一般需要修改配置。

推荐配置

一般推荐修改为以下配置分割所有chunk包括同步和异步:

optimization: {
  splitChunks: {
    chunks: 'all', // 分割所有chunk
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendor',
        priority: 10,
        enforce: true
      },
      common: {
        name: 'common',
        minChunks: 2,
        priority: 5,
        reuseExistingChunk: true
      }
    }
  },
  runtimeChunk: {
    name: 'runtime'
  }
}

这个配置会:

  • 把node_modules里的第三方库分割为vendor.js
  • 把被多个chunk引用的公共代码分割为common.js
  • 把Webpack的运行时代码分割为runtime.js
  • 业务代码按入口,或者路由分割为单独的文件

这样首屏只加载需要的代码第三方库和公共代码可以长期缓存提升页面加载速度。

踩坑经验

  1. chunks配置:chunks有三个可选值:'async'(只分割异步)'initial'(只分割同步)'all'(分割所有)推荐用'all'能最大程度分割代码,但是要注意配置合理避免分割太细导致请求太多。
  1. cacheGroups的优先级:cacheGroups里的每个组都有priority(优先级)优先级高的先匹配一个模块只能属于一个组,所以要合理设置优先级,比如vendor的优先级要比common高,因为第三方库应该优先分到vendor组。
  1. runtimeChunk的配置:runtimeChunk会把Webpack的运行时代码单独分割出来这样业务代码变化不会影响运行时代码的缓存推荐开启配置为{ name: 'runtime' }就可以。
  1. 不要过度分割:代码分割不是越细越好分割太细会导致请求太多反而影响性能要在缓存和请求数之间,找平衡一般分割为vendor(第三方库)common(公共代码)runtime(运行时)业务代码就够了。

六、Tree Shaking

Tree Shaking是Webpack 4的重要优化功能能移除代码中没有,用到的导出(export)减少打包体积。

Tree Shaking的原理

Tree Shaking基于ES6模块的静态结构(import/export)在,编译时分析哪些导出被使用了哪些没有被使用把没有被使用的导出移除减少打包体积。

注意Tree Shaking只对ES6模块有效对CommonJS模块(require/module.exports)无效,因为CommonJS模块是动态的无法在编译时静态分析。

Webpack 4的Tree Shaking

Webpack 4在production模式下会自动开启Tree Shaking(usedExports: true)不需要手动配置,但是需要满足一些条件才能生效。

Tree Shaking生效的条件

  1. 使用ES6模块:所有代码必须用ES6模块(import/export)不能用CommonJS(require/module.exports)如果用了CommonJSTree Shaking不生效。
  2. package.json配置sideEffects:在package.json里配置"sideEffects": false或者,指定哪些文件有副作用告诉Webpack哪些文件可以安全地Tree Shaking。
  3. 不要把模块转成CommonJS:Babel默认会把ES6模块转成CommonJS这会导致Tree Shaking失效需要配置Babel的modules: false让Babel不要转换模块语法保留ES6模块给Webpack处理。

踩坑经验

  1. Babel的modules配置:这是最常见的Tree Shaking不生效的原因Babel默认会把ES6模块转成CommonJS导致Tree Shaking失效需要在.babelrc里配置:
{
  "presets": [
    ["env", { "modules": false }]
  ]
}

这样Babel就不会转换模块语法保留ES6模块给Webpack做Tree Shaking。

  1. sideEffects的配置:如果不配置sideEffectsWebpack会保守地认为所有文件都有,副作用不敢Tree Shaking导致Tree Shaking效果不好需要在package.json里配置:
{
  "sideEffects": false
}

如果有一些文件确实有副作用(比如CSS文件polyfill等等)可以指定:

{
  "sideEffects": ["*.css", "*.scss", "./src/polyfill.js"]
}
  1. CSS的Tree Shaking:CSS也能Tree Shaking移除没有用到的CSS规则需要用purgecss-webpack-plugin或者uncss等工具,但是要注意配置正确避免误删用到的CSS。
  1. 不要动态import:动态import(import())虽然能做代码分割,但是也会影响Tree Shaking因为动态import的模块是异步加载的Webpack无法完全分析哪些导出被使用,所以Tree Shaking效果会打折扣要注意平衡代码分割和Tree Shaking。

七、开发环境配置

开发环境的配置注重开发体验和调试方便需要配置devServer热更新source map等等。

devServer配置

Webpack 4用webpack-dev-server来启动开发服务器支持热更新自动刷新代理等等。

devServer: {
  contentBase: path.resolve(__dirname, 'dist'),
  port: 8080,
  hot: true,
  open: true,
  proxy: {
    '/api': {
      target: 'http://localhost:3000',
      changeOrigin: true
    }
  },
  historyApiFallback: true
}

踩坑经验

  1. hot和hotOnly的区别:hot: true开启热模块替换,但是,如果模块不支持热更新会自动刷新页面hotOnly: true只开启热模块替换不自动刷新页面推荐用hot: true开发体验更好。
  1. proxy的配置:代理配置要注意target和pathRewrite的配置,如果后端接口没有/api前缀需要用pathRewrite去掉前缀,比如:
proxy: {
  '/api': {
    target: 'http://localhost:3000',
    changeOrigin: true,
    pathRewrite: { '^/api': '' }
  }
}
  1. historyApiFallback:如果是单页应用(SPA)用了HTML5 History API需要配置historyApiFallback: true否则刷新页面会404。
  1. devServer的contentBase:contentBase是静态资源的目录一般配置为输出目录(dist)但是开发环境文件都在内存里contentBase主要是为了提供一些不经过Webpack处理的静态资源,比如favicon等等。

source map配置

开发环境需要配置source map方便调试Webpack 4有很多种source map可选开发环境推荐用cheap-module-eval-source-map构建快调试方便生产环境推荐用source-map或者hidden-source-map不暴露源码。

// 开发环境
devtool: 'cheap-module-eval-source-map'

// 生产环境
devtool: 'source-map'

八、生产环境优化

生产环境的配置注重打包体积和性能优化需要做很多优化工作。

代码压缩

Webpack 4production模式自动开启JS压缩(TerserPlugin)但是CSS压缩需要手动配置用optimize-css-assets-webpack-plugin。

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')

optimization: {
  minimizer: [
    new TerserPlugin({
      cache: true,
      parallel: true,
      sourceMap: true
    }),
    new OptimizeCSSAssetsPlugin({})
  ]
}

注意,如果自定义了minimizerWebpack 4的默认JS压缩就会被覆盖需要手动加上TerserPlugin否则JS不会被压缩。

文件哈希

生产环境需要给文件加哈希实现长期缓存JS用chunkhashCSS用contenthash图片字体用hash。

output: {
  filename: '[name].[chunkhash:8].js',
  chunkFilename: '[name].[chunkhash:8].js'
},
plugins: [
  new MiniCssExtractPlugin({
    filename: '[name].[contenthash:8].css'
  })
]

gzip压缩

生产环境可以生成gzip压缩版本的文件配合Nginx的gzip_static开启直接返回gzip文件减少服务器压缩的CPU开销用compression-webpack-plugin。

const CompressionPlugin = require('compression-webpack-plugin')

plugins: [
  new CompressionPlugin({
    test: /\.(js|css|html|svg)$/,
    threshold: 8192,
    minRatio: 0.8
  })
]

打包体积分析

用webpack-bundle-analyzer分析打包体积可视化展示每个模块的大小找到体积大的模块针对性优化。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

plugins: [
  new BundleAnalyzerPlugin()
]

九、常见踩坑总结

最后总结一下Webpack 4配置最常见的坑:

  1. mode没配置:不配置mode默认production开发环境构建慢不好调试一定要配置正确的mode。
  2. 用了旧版Plugin:extract-text-webpack-pluginCommonsChunkPluginUglifyJsPlugin等等旧版Plugin不支持Webpack 4要换成新版,或者用内置功能。
  3. Babel没配置modules: false:导致Tree Shaking失效打包体积大要配置modules: false。
  4. hash/chunkhash/contenthash搞混:导致缓存策略不对要根据文件类型选择合适的hash。
  5. Loader顺序搞反:导致Loader不生效,或者报错要注意从右到左的执行顺序。
  6. 代码分割过度:导致请求太多影响性能要合理配置splitChunks。
  7. publicPath配置错:导致静态资源404要根据部署路径配置正确的publicPath。
  8. devServer代理配置错:导致接口请求失败要正确配置proxy和,pathRewrite。
  9. 自定义minimizer覆盖了默认JS压缩:导致JS没被压缩要手动加上TerserPlugin。
  10. 没有配置sideEffects:导致Tree Shaking效果不好要在,package.json里配置sideEffects。

十、写在最后

以上就是我在Webpack 4配置过程中总结的踩坑经验和实战技巧包括mode入口出口LoaderPlugin代码分割Tree Shaking开发环境生产环境等等方面的配置和注意事项。

Webpack 4相比Webpack 3有很大的改进零配置模式更快的构建速度更好的默认优化等等,但是也有一些变化和坑需要注意特别是从Webpack 3升级的同学要注意配置的变化和Plugin的替换。

希望我的踩坑经验能帮大家少走弯路更快地上手Webpack 4配置出高效稳定的构建流程。

最后用一句话结束这篇文章:"Webpack配置没有最好,只有最合适根据项目需求合理配置不断优化才是正确的之道。"

愿大家都能配置出适合自己项目的Webpack构建流程提升开发效率和用户体验。