likes
comments
collection
share

写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

作者站长头像
站长
· 阅读数 17

1. 引言

webpack是前端开发中重要的打包工具。对初中级工程师来说可能难以入门,无从下手,不能顺利的配置属于自己项目webpack配置。没关系,本文将结合webpack的构建流程,梳理常用配置,带大家一起手写reactvue3项目的通用webpack配置。希望通过阅读本文,大家能够掌握编写适合自己项目的webpack配置。即使是零基础的新手,也可以直接复用最终配置到项目中,无需过度纠结于某个配置。

webpack由于版本迭代,会删除一些参数与配置,同时会新增一些参数与配置,那么我们怎么保证我们掌握的webpack知识,不会随着webpack版本的变更而变更,也就说让我们已经学到的webpack知识怎么更保值

写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

就我个人的理解来说,就是抓住主线,不过分纠结细节,什么是主线,那就是webpack的构建流程,如下图所示

写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

这个过程是一般不会有太大变化,所以我们只要根据这个构建过程来理解,那么就知道,构建项目大概需要哪些webpack配置参数

另外不需要过分纠结于细节是因为webpack会随着迭代会进行优化,而优化之后与之前可能会有大的变动,比如webpack4之前的chunk提取使用CommonsChunkPlugin,而webpack4之后就是内置的optimization.splitChunks参数,不仅从使用配置上变了,而且内部实现也变了,所以我们在使用webpack的时候可以不过份纠结于这些细节。

下面是我自己总结的关于webpack5相关的常用配置参数,然后在结合webpack5的构建流程,就能够封装满足自己业务项目的通用webpack5配置

写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

如果不想了解webpack5常用参数与进阶参数,可以直接跳过参数部分,直接阅读手写webpack5配置部分

  • 手写基于react项目的webpack配置
  • 手写基于vue3的webpack配置

备注:文末有具体的reactvue demo项目地址,后续webpack如果没有带版本,默认都是指webpack5

2. webpack常用配置

我们先了解一下webpack的常用配置,然后在针对reactvue项目搭建通用的webpack配置,零基础也可以轻松阅读,因为本篇基本不涉及原理,关注点在怎么使用

2.1 基础webpack配置

基础配置可以理解成实际项目使用的时候基本会用到的配置

2.1.1 指定模式(mode)

mode:指定webpack的构建模式,控制webpack的默认行为

mode参数是在webpack 4.0版本中增加的。该参数可以指定webpack构建时所处的环境,设置不同的mode会启用不同的内置优化策略,在webpack 4.0之前,开发者需要手动配置一些优化策略,如配置压缩代码的插件、使用DefinePlugin定义全局常量等。而在Webpack 4.0中引入了mode参数后,webpack会根据所设置的mode自动启用对应的优化策略与默认配置,更加方便快捷;说白了最终目的就是降低webpack配置难度

/*
development 开发环境模式
production 生产环境模式
development 与 production的区别是,production多了很多优化的行为,比如tree-shaking,还有一些moduleId等
开发环境构建mode设置为development,生产环境构建mode设置为production
*/ 

module.exports = {
  mode: 'development' || 'production'
};

⚠️如果没设置,且环境变量NODE_ENV不是developmentproduction对应的值,构建的时候会给出警告

2.1.2 指定入口(entry)

entry:指定webpack的入口文件。可以是一个路径字符串或一个对象等。如果是一个对象,则会对多个依赖关系进行分析。

webpack一直有的参数,如果没有值,默认值为src/index.[js|ts|jsx|tsx]

/*
entry有多种写法,主要还是多entry与单entry的区别
单页应用一般使用单entry
多页应用一般使用多entry
*/ 
module.exports = {
  // 单entry
  entry: path.join(__dirname, '../src/app.js'),

  // 多entry
  entry: {
    app: path.join(__dirname, '../src/app.js'),
    app2: path.join(__dirname, '../src/app2.js')
  }
};

2.1.3 指定输出配置(output)

output:指定webpack的输出配置。包括输出目录、输出chunk文件名,entry文件名,publicPath

webpack一直有的参数,非必填,如果不填webpack会有默认值,但是我们一般都会填,因为会涉及到cdn地址,最终生成的文件目录与文件名称,暴露的全局变量等

module.exports = {
  output: {
    // 输出目录
    path: path.join(__dirname, '../dist'),
    // entry chunk文件名
    filename: 'js/[name].[chunkhash].js',
    // 提取的chunk文件名
    chunkFilename: 'chunk/[name].[chunkhash].js',
    // 静态资源的公共url
    publicPath: '/'
  },
};

⚠️生成最终文件名的配置,需要注意三种hash值,hash、contenthash、chunkhash的区别:

  • Hash:每次webpack构建时,如果有文件变化,就会生成一个全新的hash值,在这次构建中使用的所有文件的hash值都是相同的。虽然这可以防止浏览器缓存不更新,但是无法做到精确控制。
  • Chunkhash:根据不同的入口文件进行依赖关系的解析,构建对应的Chunk,每个Chunk都有自己的hash值。这意味着,只有该chunk内的模块发生变化时,才会改变对应chunkhash值。因此,使用chunkhash可以更精确的控制缓存,避免浏览器缓存不更新(一般用于js、图片资源)。
  • Contenthash:与chunkhash类似,但它的作用对象是文件的内容,而非文件本身。只有文件的内容发生了变化,才会改变对应的hash值。因此,使用contenthash可以针对性地使浏览器缓存更加智能,避免浏览器缓存不更新,同时也避免了无用的重新构建(一般用于css资源)。

看具体的例子,就是用上面的output配置,第一次构建 写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

不做任何改动第二次构建,hash值没有发生变化 写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

修改一个js文件,第三次构建,hash值发生变化 写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目) 从上面的例子看出

  • 第二次相比于第一次构建,hash值、所有chunkchunkhash值都没有发生变化,所以说明无文件变化时hashchunkhash不发生变化(css文件这里使用的是contenthash,暂且不关注)
  • 第三次相比第二次构建,hash值发生变化,app chunkhash发生变化、runtime chunkvendors chunkchunkhash没有发生变化,所以说明文件变化时,只有包含该文件的chunkchunkhash会发生变化,不包含该变化文件的chunkchunkhash不会发生变化

❤️小结一下: Hash:所有文件的hash值都是相同的,文件的内容和文件名的改变都会影响到所有文件的hash值。 Chunkhash:每个Chunk都有自己的hash值,同一个Chunk内文件的内容和文件名的改变都会影响到该Chunk的hash值。 Contenthash:只有文件的内容发生了变化,才会改变对应的hash值。 因此,在使用webpack构建项目时,我们通常会根据具体需求选择合适的hash策略,以达到更好的缓存效果。

涉及到hash的配置一般有

  • output:控制输出资源的文件名
  • generator:控制图片等静态资源的文件名
  • MiniCssExtractPlugin:控制提取的css文件名
output: {
  path: '/Users/wangks/Documents/yunke/frontend-template/dist',
  filename: 'js/[name].[chunkhash].js',
  chunkFilename: 'chunk/[name].[chunkhash].js',
  publicPath: '/'
},
{
  test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
  type: 'asset',
  parser: {
    dataUrlCondition: {
      maxSize: 10240
    }
  },
  generator: {
    filename: 'image/[name].[hash][ext]'
  }
},
new MiniCssExtractPlugin(
  {
    filename: 'css/[name].[contenthash].css',
    chunkFilename: 'css/[name].[contenthash].css'
  }
),

2.1.4 指定模块查找配置(resolve)

resolve:指定webpack查找模块时的配置,比如指定别名、npm包相关查找规则

webpack一直有的参数,非必填,但是我们一般都会填,比如设置别名alias,方便我们在开发的时候快速导入模块,比如设置extensions允许加载的拓展文件名,总体来说方便拓展加载的模块类型与提高开发效率,同时又因为涉及到模块加载的规则,所以会看到resolve是webpack优化时的其中一项

module.exports = {
  resolve: {
    symlinks: true,
    // 模块别名,作用我们可以像import npm包那样,import我们src下的模块
    alias: {
      process: 'process/browser',
      '@': path.join(__dirname, '../src'),
    },

    // 加载模块的时候,允许匹配的文件名后缀
    extensions: [
      '.tsx',
      '.ts',
      '.jsx',
      '.js'
    ],

    // 加载npm包的时候,通过哪个字段获取npm包入口文件
    mainFields: [
      'browser',
      'main:h5',
      'module',
      'main'
    ],

    // 加载npm包的时候,在哪些目录去查找
    modules: [
      path.join(__dirname, '../node_modules'),
    ]
  },
};

比如

// 没有别名之前,通过相对路径引入模块
import { token } from '../../../utils/index'

// 有别名之后,可以直接通过别名的方式导入模块,这样导入更清晰与方便
import { token } from '@/utils/index'

2.1.5 模块转化规则(module)

module: webpack默认是不能处理所有静态资源的,所以我们需要通过loader来帮助我们,将webpack不能识别的资源转化成webpack可以识别的内容,module指定loaderparse等配置

webpack一直有的参数,非必填,但是在真实的项目中都会配置,原因是需要处理js、css、图片等资源;主要包含rules参数,即使用正则表达式匹配特定文件类型,并使用相应的 loader 进行处理,noParse 来排除对某些模块的解析以提高构建性能,parser 选项来自定义 webpack 的模块解析器;其中rules参数是webpack2.0中引入的,在2.0之前的版本叫loaders;

我们在实际处理项目的时候一般会碰到以下需要loader处理资源的场景

  • ts 转化成 js
  • js es6+转化成es5
  • 处理css,比如提取、添加前缀等
  • 处理图片、音视频等静态资源
  • 提升loader转化速度

下面我们分别来看看

处理js

安装依赖

pnpm add babel-loader thread-loader -D
  • babel-loader:用于将ts转化为js,并将es6+语法转化为es5
  • thread-loader:加速babel-loader的转化速度

具体配置如下

module.exports = {
  module: {
    rules: [
      {
        test: /\.[tj]sx?$/i,
        exclude: [
          /(node_modules|bower_components)/
        ],
        use: [
          {
            loader: 'thread-loader',
          }
          {
            loader: 'babel-loader'
          }
        ]
      },
      
  },
}

为什么不推荐使用ts-loader及怎么使用esbuild-loaderswc-loader可以查看日常开发,我该掌握哪些webpack loader知识

处理css

安装依赖

pnpm add css-loader style-loader mini-css-extract-plugin postcss-loader less-loader -D
  • css-loader:将css资源转化成webpack能够处理的资源
  • style-loader:用于开发环境将css-loader处理后的样式通过style等方式插入到html
  • mini-css-extract-plugin.loader: 用于生成环境构建时将css-loader处理后的样式抽离到单独的文件中
  • postcss-loader: 用于处理css,将css添加前缀,支持css新语法等
  • less-loader: 将less转化为css-loader能够处理样式
  • sass-loader: 将sass转化为css-loader能够处理样式

一般项目配置如下所示

module.exports = {
	...
  module: {
    rules: [
      {
        test: /\.(css|less|s[a|c]ss)(\?.*)?$/i,
        use: [
          {
            loader: process.env.NODE_ENV === 'development' ? 'style-loader' : miniCssExtractPlugin.loader 
          },
        ],
      },
      {
        test: /\.css$/i,
        use: [
        	{
            loader: 'css-loader'
          },
          {
            loader: 'postcss-loader'
          },
        ],
      },
      {
        test: /\.less$/i,
        use: [
          {
            loader: 'css-loader'
          },
          {
            loader: 'postcss-loader'
          },
        	{
            loader: 'less-loader'
          },
        ],
      },
      {
        test: /\.s[a|c]ss$/i,
        use: [
          {
            loader: 'css-loader'
          },
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                require('autoprefixer')
              ]
            }
          },
        	{
            loader: 'sass-loader'
          },
        ],
      },
    ],
  },
}

处理图片等静态资源

处理静态资源可以直接依赖webpack5自带的能力

module.exports = {
	module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10240
          }
        },
        generator: {
          filename: 'image/[name].[hash][ext]'
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10240
          }
        },
        generator: {
          filename: 'static/fonts/[name].[hash][ext]'
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|m4a|wav|flac|aac)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10240
          }
        },
        generator: {
          filename: 'static/media/[name].[hash][ext]'
        }
      },
      {
        resourceQuery: /raw/,
        type: 'asset/source'
      }
    ]
  },
}

2.1.5 功能增强(plugins)

webpack一直有的参数,非必填,但是真实项目内使用时会配置插件;其作用是拓展webpack的功能

基础插件

html-webpack-plugin(html插件)

html-webpack-plugin支持webpack4+版本,其作用是指定入口html文件,然后将webpack构建好的js、css等静态资源按照加载顺序,插入到html内

安装依赖

pnpm add html-webpack-plugin -D

具体使用如下所示,更多配置参数查看文档

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [
    new HtmlWebpackPlugin(
      {
        filename: 'index.html',
        template: path.join(__dirname, '../public/index.html'),
        minify: {
          collapseWhitespace: true,
          minifyJS: true,
          html5: true,
          minifyCSS: true,
          removeComments: true,
          removeTagWhitespace: false
        },
      }
    ),
  ]
}

mini-css-extract-plugin

mini-css-extract-plugin支持webpack4+版本,其作用是提取css内容成单独文件

安装依赖

pnpm add mini-css-extract-plugin -D

使用方式

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  module: {
    rules: [
      {
        test: /\.(css|less|s[a|c]ss)(\?.*)?$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          }
        ]
      },
    ]
  },
  plugins: [
    new MiniCssExtractPlugin(
      {
        filename: 'css/[name].[contenthash].css',
        chunkFilename: 'css/[name].[contenthash].css'
      }
    )
  ]
}

⚠️这里的filenamechunkFilename为什么使用的是contenthash而不是chunkhash,原因是因为css文件是从js里面提取出来的,当我们某个js文件变动了而css文件实际上没有变动,但是最终的css chunkhash还是发生了变化,原因就是js变了,从而导致提取的css时的css module也发生了变化,最终提取的css chunkhash也发生变化,具体实例如下所示

看一个具体的例子,第一次构建 写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

第二次构建,没有文件发生变化 写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

修改js文件,第三次构建 写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目) 从上面的例子看出

  • 第二次相比于第一次构建,hash值,所有chunkchunkhash值都会没有发生变化的,所以说明无文件变化时hashchunkhash不发生变化
  • 第三次相比第二次构建,hash值发生变化,app chunkhashcss chunkhash发生变化、runtime chunkvendors chunkchunkhash没有发生变化,所以说明文件变化时,包含该文件的chunkhash会发生变化,不包含该变化文件的chunkhash不会发生变化
  • 为了避免因为js的变化,导致最终生成的css文件名也发生变化,所以css这里使用的是contenthash而不是chunkhash
webpack.DefinePlugin

webpack.DefinePlugin webpack4+内置插件,其作用是编译时 将代码中的变量替换为其他值或表达式,方便我们做一些根据环境来区分运行的代码

module.exports = {
  plugins: [
    new webpack.DefinePlugin(
      {
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
      }
    ),
  ]
}
webpack.ProvidePlugin

webpack.ProvidePlugin webpack4+内置插件,其作用是为每个模块自动导入某个依赖,而不需要手动require or import导入

module.exports = {
  plugins: [
    new webpack.ProvidePlugin(
      {
        process: 'process/browser',
        $: 'jquery',
      }
    ),
  ]
}

辅助插件

webpack.ProgressPlugin

webpack.ProgressPlugin webpack4+内置插件,其作用是显示webpack构建进度

module.exports = {
  plugins: [
    new webpack.ProgressPlugin(
      {
        percentBy: 'entries',
        profile: false
      }
    ),
  ]
}
clean-webpack-plugin

clean-webpack-plugin插件支持webpack4+版本,其作用是清除上次打包产物

安装依赖

pnpm add clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  plugins: [
    new CleanWebpackPlugin(
      {
        dry: false
      }
    ),
  ]
}
FriendlyErrorsWebpackPlugin

@soda/friendly-errors-webpack-plugin插件支持webpack4+版本,其作用是美化webpack构建过程中抛出的错误

安装依赖

pnpm add @soda/friendly-errors-webpack-plugin -D

使用方式

const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin')

module.exports = {
  plugins: [
    new FriendlyErrorsWebpackPlugin(
      {
        percentBy: 'entries',
        profile: false
      }
    ),
  ]
}
copy-webpack-plugin

copy-webpack-plugin插件支持webpack4+版本,其作用是copy文件

安装依赖

pnpm add copy-webpack-plugin -D

使用方式

const CopyPlugin = require('copy-webpack-plugin')

module.exports = {
  plugins: [
    new CopyPlugin(
      {
        patterns: [
          {
            from: path.join(__dirname, 'public'),
            to: path.join(__dirname, 'dist'),
            globOptions: {
              ignore: [
                '**/*.html'
              ]
            },
            noErrorOnMissing: true
          }
        ]
      }
    ),
  ]
}
case-sensitive-paths-webpack-plugin

case-sensitive-paths-webpack-plugin插件支持webpack3+版本,其作用是检查文件的大小写,目的是为了解决osx系统上大小写不敏感,而其它系统上大小写敏感导致的bug

安装依赖

pnpm add case-sensitive-paths-webpack-plugin -D

使用方式

const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')

module.exports = {
  plugins: [
    new CaseSensitivePathsPlugin(),
  ]
}

2.1.6 开发服务(devServer)

不属于webpack构建时的参数,webpack3.0引入,对webpack-dev-server生效,其作用是为项目添加开发服务器,提供本地开发服务访问,热更新等能力

安装依赖

pnpm add webpack-dev-server -D

使用方式

devServer: {
  static: {
    directory: path.join(__dirname, 'public'),
    publicPath
  },
  compress: true,
  port: 9000,
  host: 'localhost',
  hot: true,
  https: false,
  open: ['/react-webpack-config-demo/'],
  historyApiFallback: {
    rewrites: [{
      from: /./,
      to: publicPath,
    }],
  },
},

2.2 进阶webpack配置

进阶配置可以理解成优化配置,每个人的理解不一样,不强求,上面的参数基本上满足了大部分场景,但是我们在实际的使用的时候,考虑到环境、性能、缓存、调试等问题,会进一步使用一些webpack提供的配置

  • chunk优化:对应optimization配置
  • source-map:对应devtool配置
  • 缓存: 对应cache配置

2.2.1 指定chunk提取规则(optimization)

webpack4.0引入的一个参数,非必填,但是webpack会根据mode设置一些默认值;在webpack 4.0之前,很多优化选项都是作为独立的插件存在,需要手动添加和配置。而optimization 参数则将这些常见的优化策略整合到一个对象中,使得配置变得更加简单和清晰,比如压缩(minimize)、chunk提取(splitChunks)、提取runtime chunk(runtimeChunk)、标记导入导出用于tree-shaking(usedExports)等

⚠️在了解为什么要进行chunk提取之前,先了解几个概念

  • Module(模块):webpack将所有的代码都视为模块,每个模块都有各自独立的作用域。可以是JS文件、CSS文件、图片等等。
  • Chunk(代码块):代码块指由多个模块组合而成的代码块,通常用于代码的分割和按需加载。可以是按页面划分的代码块、按路由划分的代码块、公共代码块等等。
  • Asset(资源文件):webpack打包后产生的输出文件,可以是JS文件、CSS文件、图片等。Asset通常是由多个Chunk组合而成的,每个Chunk都会生成一个或多个Asset文件。

简单来说,Module就是Webpack中的基本单元,每一个文件差不多就是一个ModuleChunk则是将多个Module组合在一起形成的一个代码块,可用于按需加载和优化性能;Asset则是Chunk编译后得到的资源文件,包括JSCSS、图片等。

webpack中chunk分为三类

  • initial chunk 也就是entry chunk
  • async chunk 也就是通过动态加载的entry chunk
  • split chunk 也就是提取出来的chunk

在了解了chunk之后,我们在了解为什么需要拆分chunk?看一个例子,伪代码如下所示

import common from './common.js'

console.log(common);
import common from './common.js'

console.log(common);

webpack配置

module.exports = {
  // 多entry
  entry: {
    app: path.join(__dirname, 'entry.js'),
    app2: path.join(__dirname, 'entry2.js')
  }
};

上述的代码打包的时候,最开始会有两个initial chunk,二这两个initial chunk都会包含common.js这个模块内容,显然当这种公共模块存在于多个chunk的时候,明显会让总包体积变大,也存在重复内容,所以才会进行chunk拆分,将initial chunkasync chunk中存在多份的模块,提取到一个公共的chunk里面,这样就可以减少重复代码,降低总包体积

chunk提取(optimization.splitChunks)

主要作用就是将各个chunk中的公共module,提取出来只保留一份,最终目的就是尽可能的减少公共代码存在的分数,缩小代码尺寸

splitChunks:可以设置最小尺寸、最小Chunks数、按需加载等。可参考官方文档了解更多细节。


module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 102400,
      minChunks: 3,
      maxSize: 307200,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      automaticNameDelimiter: '~',
      cacheGroups: {// 提取chunk规则不固定,根据自己项目提取,但是一般的提取规则为node_modules下的包(还可以抽一个react类orvue类的),common相关的抽一个包
        'default': false,
        vendors: {
          test: /[\\\/]node_modules[\\\/]/,
          priority: -10,
          reuseExistingChunk: true,
          name: 'vendors'
        },
        common: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
          name: 'common'
        },
        react: {
          test: /[\\\/]node_modules[\\\/](core-js|react.*|redux.*|props-type|immer|history|@reduxjs\/toolkit)[\\\/]/,
          priority: 0,
          reuseExistingChunk: true,
          name: 'react',
          minSize: 0
        }
      }
    },
  }
};

提取runtime chunk(optimization.runtimeChunk)

每个 entry chunk 都包含一个 runtime chunk,这会导致多个入口文件重复包含相同的运行时代码,从而增加了打包后的文件大小和加载时间,为了避免这种情况,webpack 从 4.x 版本开始提供了一个新特性,称为 runtimeChunk。通过配置 runtimeChunk,我们可以将所有 entry chunks 共享的运行时代码抽离出来,并将其保存到一个单独的文件中。这样就可以避免多个入口文件重复包含相同的运行时代码,从而减少了打包后的文件大小和加载时间。

module.exports = {
  optimization: {
    runtimeChunk: {
      name: 'runtime'
    },
  },
}

压缩js && css

配置压缩的插件,如果不配置会使用默认的压缩插件terser

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin(
        {
          parallel: true,
          terserOptions: {
            output: {
              comments: false,
              keep_quoted_props: false,
              quote_keys: false,
              beautify: false
            },
            keep_fnames: true,
            warnings: false,
            compress: {
              drop_console: true,
              drop_debugger: true
            }
          }
        }
      )
      new CssMinimizerPlugin(
        {
          parallel: true
        }
      )
    ]
  },
}

2.2.2 指定缓存配置(cache)

webpack5中的重磅功能,相比cache-loader等缓存方式,提供了更全面及更多样的缓存方式

使用cache参数可以启用Webpack的持久化缓存。开启缓存后,Webpack会将每次构建结果缓存到磁盘中,提高下一次构建的速度。

module.exports = {
  cache: true
};

2.2.3 指定source-map(devtool)

webpack一直存在的参数,非必填,不填时webpack有默认配置,用于指定在生成代码时应该为每个模块添加什么类型的 Source Map。Source Map 是一种映射,将编译后的代码映射回原始源代码,以便在调试时可以使用原始源代码而不是编译后的代码。每种生成方式都有其优点和缺点

使用devtool参数可以配置Source Map的生成策略。常用的配置项有:

  • source-map:使用单独的文件生成完整的Source Map,方便调试,适用于开发环境。
  • nosources-source-map:生成单独的Source Map文件,但是不包含源代码,适用于生产环境。
module.exports = {
  // entry、output、module、devServer等配置与之前相同,此处省略
  devtool: 'source-map'
};

写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目)

3. 手写基于react项目的webpack配置

到这里我们已经了解了常用配置有哪些及其作用,那么我们在配置项目的webpack配置时,一般会包含如下配置

  • mode: 用于指定模式
  • entry: 用于指定入口文件
  • output: 用于指定输出目录、文件配置、cdn链接
  • resolve: 用于指定模块查找配置,提高开发效率
  • module: 用于指定处理相应资源的loader
  • plugins: 用于指定html插件、css提取等插件
  • devServer: 用于指定开发环境的开发服务配置
  • optimization: 用于指定chunk提取规则及压缩插件等
  • cache: 用于指定缓存方式
  • devtool: 用于指定生成source-map方式

当我们使用上面的配置,在结合一些框架专用的loader或者plugin,那么就可以写出满足项目的webpack配置

下面是满足react项目的通用webpack配置,分为三份方便阅读与维护

  • 公共配置
  • 开发环境配置
  • 生产环境配置

3.1 公共配置

包含生产环境与开发环境公用的webpack配置

const path = require('path');
const { ProgressPlugin, DefinePlugin, ProvidePlugin } = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin')

const resolveDir = (dir) => path.join(__dirname, `../${dir}`)

const isDevelopment = process.env.NODE_ENV === 'development'

const cssLoader = {
  loader: 'css-loader',
  options: {
    importLoaders: 1,
    sourceMap: true,
    modules: {
      auto: true
    }
  }
}

const postcssLoader = {
  loader: 'postcss-loader',
  options: {
    sourceMap: true,
    postcssOptions: {
      plugins: [require('autoprefixer')],
    }
  }
}

module.exports = {
  resolve: {
    symlinks: true,
    alias: { // 设置别名
      process: 'process/browser',
      '@': resolveDir('src'),
    },
    extensions: [
      '.tsx',
      '.ts',
      '.jsx',
      '.js'
    ],
    mainFields: [
      'browser',
      'main:h5',
      'module',
      'main'
    ],
  },
  module: {
    rules: [
      // 处理css最终生效的方式
      {
        test: /\.(css|less|s[a|c]ss)(\?.*)?$/,
        use: [
          {
            loader: isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader
          }
        ]
      },
      // 处理css
      {
        test: /\.css$/,
        use: [
          cssLoader,
          postcssLoader
        ]
      },
      // 处理less
      {
        test: /\.less$/,
        use: [
          cssLoader,
          postcssLoader,
          {
            loader: 'less-loader',
            options: {
              lessOptions: {
                javascriptEnabled: true
              }
            }
          }
        ]
      },
      // 处理sass
      {
        test: /\.s[a|c]ss$/,
        use: [
          cssLoader,
          postcssLoader,
          {
            loader: 'sass-loader'
          }
        ]
      },
      // 处理ts、js、tsx、jsx
      {
        test: /\.[tj]sx?$/i,
        exclude: [
          /(node_modules|bower_components)/
        ],
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    debug: false,
                    useBuiltIns: 'usage', // https://babeljs.io/docs/en/babel-preset-env
                    corejs: 3,
                  },
                ],
                ['@babel/preset-react'],
                ['@babel/preset-typescript'],
              ],
              plugins: [
                isDevelopment && [
                  require.resolve('react-refresh/babel'),
                  {
                    skipEnvCheck: true
                  }
                ],
                [
                  '@babel/plugin-transform-runtime',
                  {
                    corejs: false,
                    helpers: true,
                    regenerator: true,
                  },
                ],
              ].filter(Boolean),
            }
          }
        ]
      },
      // 处理图片
      {
        test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10240
          }
        },
        generator: {
          filename: 'image/[name].[hash][ext]'
        }
      },
      // 处理字体
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10240
          }
        },
        generator: {
          filename: 'static/fonts/[name].[hash][ext]'
        }
      },
      // 处理视频
      {
        test: /\.(mp4|webm|ogg|mp3|m4a|wav|flac|aac)(\?.*)?$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10240
          }
        },
        generator: {
          filename: 'static/media/[name].[hash][ext]'
        }
      },
    ]
  },
  plugins: [
    new ProgressPlugin(
      {
        percentBy: 'entries',
        profile: false
      }
    ),
    new FriendlyErrorsWebpackPlugin(
      {}
    ),
    new DefinePlugin(
      {
        FOO: process.env.FOO
      }
    ),
    new ProvidePlugin(
      {
        process: 'process/browser'
      }
    ),
  ],
  entry: {
    app: [
      resolveDir('src/app')
    ]
  },
  stats: {
    hash: true,
  }
}

3.2 开发环境配置

仅包含开发环境使用的webpack配置

const path = require('path')
const { merge } = require('webpack-merge')
const { WatchIgnorePlugin } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const baseConfig = require('./webpack.base.config')

const resolveDir = (dir) => path.join(__dirname, `../${dir}`)

const publicPath = '/'

module.exports = merge(baseConfig, {
  mode: 'development',
  devtool: 'source-map', // 设置生成source-map方式
  cache: { // 设置缓存方式
    type: 'filesystem',
    name: 'dev-cache',
    version: 'development',
  },
  output: { // 设置输出
    path: resolveDir('dist'),
    filename: 'js/[name].js',
    chunkFilename: 'chunk/[name].js',
    publicPath
  },
  devServer: { // 设置开发服务配置
    static: {
      directory: path.join(__dirname, 'public'),
      publicPath
    },
    compress: true,
    port: 9000,
    host: 'localhost',
    hot: true,
    https: false,
    open: ['/react-webpack-config-demo/'],
    historyApiFallback: {
      rewrites: [{
        from: /./,
        to: publicPath,
      }],
    },
  },
  plugins: [
    new WatchIgnorePlugin(
      {
        paths: [
          /(css|less|s[a|c]ss)\.d\.ts$/
        ]
      }
    ),
    new HtmlWebpackPlugin(
      {
        filename: 'index.html',
        template: resolveDir('public/index.html'),
      }
    ),
    // react热更新插件
    new ReactRefreshPlugin(
      {
        overlay: false,
        exclude: /node_modules/i,
        include: /\.([cm]js|[jt]sx?|flow)$/i
      }
    ),
  ],
})

3.3 生产环境配置

仅包含生产环境使用的webpack配置

const path = require('path')
const { merge } = require('webpack-merge')
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const baseConfig = require('./webpack.base.config')

const resolveDir = (dir) => path.join(__dirname, `../${dir}`)

const config = merge(baseConfig, {
  mode: 'production',
  devtool: 'nosources-source-map',
  cache: {
    type: 'filesystem',
    name: 'production-cache',
    version: 'production',
  },
  output: {
    path: resolveDir('dist'),
    filename: 'js/[name].[chunkhash].js', // 这里需要配置chunkhash
    chunkFilename: 'chunk/[name].[chunkhash].js',
    publicPath: '/'
  },
  optimization: {
    minimize: true,
    splitChunks: { // chunk提取规则
      chunks: 'all',
      minSize: 1024 * 100,
      minChunks: 1,
      maxSize: 307200,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      automaticNameDelimiter: '~',
      cacheGroups: { // 设置chunk提取方式
        'default': false,
        vendors: {
          test: /[\\\/]node_modules[\\\/]/,
          priority: -10,
          reuseExistingChunk: true,
          name: 'vendors'
        },
        common: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
          name: 'common'
        }
      }
    },
    runtimeChunk: {
      name: 'runtime'
    },
    emitOnErrors: true,
    minimizer: [
      // js压缩插件
      new TerserPlugin(
        {
          parallel: true
        }
      ),
      // css压缩插件
      new CssMinimizerPlugin(
        {
          parallel: true
        }
      )
    ]
  },
  plugins: [
    // css提取插件
    new MiniCssExtractPlugin(
      {
        filename: 'css/[name].[chunkhash].css',
        chunkFilename: 'css/[name].[chunkhash].css'
      }
    ),
    // 清除上一次构建产物
    new CleanWebpackPlugin(
      {
        dry: false
      }
    ),
    new CopyPlugin(
      {
        patterns: [
          {
            from: resolveDir('public'), // 将public目录下的内容copy到dist
            to: resolveDir('dist'),
            globOptions: {
              ignore: [
                '**/*.html'
              ]
            },
            noErrorOnMissing: true
          }
        ]
      }
    ),
    new HtmlWebpackPlugin(
      {
        filename: 'index.html',
        template: resolveDir('public/index.html'),
      }
    ),
  ],
});

module.exports = config

写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目) react-webpack-config-demo

4. 手写基于vue3的webpack配置

下面是满足vue3项目的通用webpack配置,分为三份方便阅读与维护,支持单文件vue,支持tsxjsx

  • 公共配置
  • 开发环境配置
  • 生产环境配置

整体配置与react项目大体一致,主要区分点

  • vue项目相对react项目多了vue-loader及对应的plugin
  • vue项目相对react项目在css处理的部分,多了oneOf用来处理单文件.vuestyle module
  • babel配置有点差异

4.1 公共配置

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const { DefinePlugin } = require('webpack')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin')

const resolveDir = (dir) => path.join(__dirname, `../${dir}`)

const cssAutoLoader = {
  loader: 'css-loader',
  options: {
    sourceMap: false,
    importLoaders: 2,
    modules: {
      localIdentName: '[name]_[local]_[hash:base64:8]',
      auto: true // 由文件名名中是否包含.module字段来判断是否是css module
    }
  }
}

const cssModuleLoader = {
  loader: 'css-loader',
  options: {
    sourceMap: false,
    importLoaders: 2,
    modules: {
      localIdentName: '[name]_[local]_[hash:base64:8]',
      auto: () => true // 一定是css module
    }
  }
}

const postcssLoader = {
  loader: 'postcss-loader',
  options: {
    sourceMap: false,
    postcssOptions: {
      plugins: [
        'autoprefixer'
      ]
    }
  }
}

const styleLoader = process.env.NODE_ENV === 'development' ? {
  loader: 'vue-style-loader',
  options: {
    sourceMap: false,
    shadowMode: false
  }
} : {
  loader: MiniCssExtractPlugin.loader,
}

const sassLoader = {
  loader: 'sass-loader',
  options: {
    sourceMap: false,
    sassOptions: {
      indentedSyntax: true
    }
  }
}

const lessLoader = {
  loader: 'less-loader',
  options: {
    sourceMap: false
  }
}

module.exports = {
  entry: {
    app: [
      './src/main.ts'
    ]
  },
  resolve: {
    alias: {
      '@': resolveDir('src'),
      vue$: 'vue/dist/vue.runtime.esm-bundler.js'
    },
    extensions: [
      '.tsx',
      '.ts',
      '.mjs',
      '.js',
      '.jsx',
      '.vue',
      '.json',
      '.wasm'
    ]
  },
  module: {
    // vue、vue-router这些库跳过解析
    noParse: /^(vue|vue-router|vuex|vuex-router-sync)$/,
    rules: [
      // 避免导入模块缺少拓展名或者目录时,webpack主动抛出错误
      {
        test: /\.m?jsx?$/,
        resolve: {
          fullySpecified: false
        }
      },
      // 处理.vue单文件
      {
        test: /\.vue$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              babelParserPlugins: [
                'jsx',
                'classProperties',
                'decorators-legacy'
              ]
            }
          }
        ]
      },
      // 表示.vue中的style那部分内容时无副作用的
      {
        test: /\.vue$/,
        resourceQuery: /type=style/,
        sideEffects: true
      },
      // 处理svg
      {
        test: /\.(svg)(\?.*)?$/,
        type: 'asset/resource',
        generator: {
          filename: 'img/[name].[hash:8][ext]'
        }
      },
      // 处理png等图片
      {
        test: /\.(png|jpe?g|gif|webp|avif)(\?.*)?$/,
        type: 'asset',
        generator: {
          filename: 'img/[name].[hash:8][ext]'
        }
      },
      // 处理mp4等视频资源
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        type: 'asset',
        generator: {
          filename: 'media/[name].[hash:8][ext]'
        }
      },
      // 处理字体文件
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
        type: 'asset',
        generator: {
          filename: 'fonts/[name].[hash:8][ext]'
        }
      },
      // 处理css
      {
        test: /\.css$/,
        oneOf: [
          {
            resourceQuery: /module/,
            use: [
              styleLoader,
              cssModuleLoader,
              postcssLoader
            ]
          },
          {
            use: [
              styleLoader,
              cssAutoLoader,
              postcssLoader
            ]
          }
        ]
      },
      // 处理sass
      {
        test: /\.sass$/,
        oneOf: [
          {
            resourceQuery: /module/,
            use: [
              styleLoader,
              cssModuleLoader,
              postcssLoader,
              sassLoader
            ]
          },
          {
            use: [
              styleLoader,
              cssAutoLoader,
              postcssLoader,
              sassLoader
            ]
          }
        ]
      },
      // 处理less
      {
        test: /\.less$/,
        oneOf: [
          {
            resourceQuery: /module/,
            use: [
              styleLoader,
              cssModuleLoader,
              postcssLoader,
              lessLoader
            ]
          },
          {
            use: [
              styleLoader,
              cssAutoLoader,
              postcssLoader,
              lessLoader
            ]
          }
        ]
      },
      // 处理js、ts、jsx、tsx
      {
        test: /\.m?(js|jsx|ts|tsx)$/,
        exclude: function (filepath) {
          const SHOULD_SKIP = true
          const SHOULD_TRANSPILE = false

          if (!filepath) {
            return SHOULD_SKIP
          }

          // Always transpile js in vue files
          if (/\.vue\.jsx?$/.test(filepath)) {
            return SHOULD_TRANSPILE
          }

          return /node_modules/.test(filepath) ? SHOULD_SKIP : SHOULD_TRANSPILE
        },
        use: [
          {
            loader: 'babel-loader',
          }
        ]
      },
    ],

  },
  plugins: [
    new VueLoaderPlugin(),
    new DefinePlugin(
      {
        __VUE_OPTIONS_API__: true, // vue3 开启 options api
        __VUE_PROD_DEVTOOLS__: false, // vue3 在生产环境中禁用 devtools 支持
        'process.env': {
          NODE_ENV: JSON.stringify(process.env.NODE_ENV),
          BASE_URL: JSON.stringify(process.env.BASE_URL || '/'),
        }
      }
    ),
    new CaseSensitivePathsPlugin(),
    new FriendlyErrorsWebpackPlugin(),
  ]
}

4.2 开发环境配置

const path = require('path');
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin');

const baseConfig = require('./webpack.base.config')

const resolveDir = (dir) => path.join(__dirname, `../${dir}`)

module.exports = merge(baseConfig, {
  mode: 'development',
  devtool: 'source-map', // 指定生产source-map的方式
  output: {
    path: resolveDir('dist'),
    filename: 'js/[name].js', // 使用name即可,无需使用chunkhash等
    publicPath: '/',
    chunkFilename: 'js/[name].js' // 使用name即可,无需使用chunkhash等
  },
  cache: {
    type: 'filesystem', // 开发环境也可以使用内存模式
    name: 'dev-cache',
    version: process.env.NODE_ENV,
  },
  devServer: {
    static: { // 保证public下的文件能过支持访问
      directory: path.join(__dirname, 'public'),
    },
    compress: true,
    port: 9000,
    historyApiFallback: true, // 保证刷新浏览器的时候前端路由能够正常命中不会出现404
  },
  plugins: [
    new HtmlWebpackPlugin(
      {
        title: 'webpack-vue3-demo',
        scriptLoading: 'defer',
        template: resolveDir('public/index.html') // 指定入口html文件
      }
    ),
  ]
})

4.3 生产环境配置

const path = require('path');
const { merge } = require('webpack-merge')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

const baseConfig = require('./webpack.base.config')

const resolveDir = (dir) => path.join(__dirname, `../${dir}`)

module.exports = merge(baseConfig, {
  mode: 'production',
  devtool: 'nosources-source-map',
  output: {
    path: resolveDir('dist'),
    filename: 'js/[name].[chunkhash:8].js', // 使用chunkhash即可
    publicPath: '/',
    chunkFilename: 'js/[name].[chunkhash:8].js' // 使用chunkhash即可
  },
  cache: {
    type: 'filesystem', // 推荐文件缓存,方便CI场景复用缓存
    name: 'prod-cache',
    version: process.env.NODE_ENV,
  },
  optimization: {
    splitChunks: { // 提取两个chunk,一个是chunk-vendors、一个是chunk-common,只针对initial chunk进行提取
      cacheGroups: {
        defaultVendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    },
    minimizer: [
      // 设置js压缩插件
      new TerserPlugin(
        {
          parallel: true,
          extractComments: false
        }
      ),
      // 设置css压缩插件
      new CssMinimizerPlugin(
        {
          parallel: true,
          minimizerOptions: {
            preset: [
              'default'
            ]
          }
        }
      )
    ]
  },
  plugins: [
    // 清除上传构建产物
    new CleanWebpackPlugin(
      {
        dry: false
      }
    ),
    // 提取css插件
    new MiniCssExtractPlugin(
      {
        filename: 'css/[name].[contenthash:8].css', // 注意这里一定要使用contenthash,不然无法保证最好的缓存效果
        chunkFilename: 'css/[name].[contenthash:8].css' // 注意这里一定要使用contenthash,不然无法保证最好的缓存效果
      }
    ),
    new HtmlWebpackPlugin(
      {
        title: 'webpack-vue3-demo',
        scriptLoading: 'defer',
        template: resolveDir('public/index.html')
      }
    ),
    new CopyPlugin(
      {
        patterns: [
          {
            from: resolveDir('public'), // 将public目录下的内容copy到dist目录
            to: resolveDir('dist'),
            toType: 'dir',
            noErrorOnMissing: true,
            globOptions: {
              ignore: [
                '**/.DS_Store',
                resolveDir('public/index.html')
              ]
            }
          }
        ]
      }
    ),
  ],
})

写给初中级工程师的进阶指南,打造属于自己的webpack配置(包含react、vue项目) webpack-vue3-demo

5. 总结

本文介绍了webpack的基本使用方法,并提供了一份简单易懂的webpack配置指南。同时,还介绍了webpack的常用配置参数及其作用,希望本文可以帮助初中级的同学,打造自己的webpack配置;

本篇主要讲怎么使用,没有过多的设计原理,如果想深入了解webpack的,可以看看下列文章,希望对你有所帮助

最后webpack版本及插件会随着时间的变化而变化,但是最开始提到的思路 ,也就是根据构建流程来理解webpack配置,是不会大的变化,只要我们记住了webpack的大体构建流程,在结合参数,那么写出适合自己项目的webpack是不难的

👉下一篇,就是将本篇总结的webpack配置封装成npm包,打造自己的react-scripts及@vue/cli-service

参考链接

vue-cli

vue-loader

react-scripts