跳到主要内容

最新 Webpack 的常规配置怎么写?

很多人一说 Webpack 配置,就开始堆 loader、堆 plugin、堆“黑魔法”。但面试官真正想听的,通常不是你会不会背几十个配置项,而是你能不能讲清楚:一个当前主流的 Webpack 5 项目,开发和生产分别应该怎么配,为什么这么配,哪些旧写法已经不值得再用了。

本文按 截至 2026-04 官方文档仍以 Webpack 5 为主线 来讲,示例使用最常见的 SPA 常规工程基线。如果你的项目是 SSR、组件库、Module Federation、Monorepo 或多入口场景,这套配置可以作为骨架继续扩展,但不要生搬硬套。

面试速答(30 秒版 TL;DR)

  • 现在讲“最新 Webpack 常规配置”,默认讲 Webpack 5,而不是 Webpack 4 那套 file-loader / url-loader / raw-loader 思路。
  • 常规项目最好拆成 webpack.common + webpack.dev + webpack.prod,避免开发和生产目标混在一起。
  • 必配核心项通常是:modeentryoutputresolvemodule.rulespluginsdevtooldevServeroptimizationcache
  • 开发环境重点是:启动快、热更新快、定位问题快;生产环境重点是:包体可控、缓存稳定、Source Map 策略清晰
  • Webpack 5 里,静态资源优先用 Asset Modules;构建提速优先看 filesystem cache;拆包先从 splitChunks.chunks = 'all' 起步,不要一上来就过度手写 cacheGroups

一、先把“常规配置”理解成三层

常规 Webpack 配置,不是一个大而全的 webpack.config.js,而是按职责拆:

这个拆法的价值是:

  • 公共层只放跨环境都成立的事实,比如入口、输出目录、路径别名、JS 规则、图片字体规则。
  • 开发层只解决本地开发效率问题,比如 devServer、快速 Source Map、样式热更新。
  • 生产层只解决线上产物问题,比如 CSS 抽离、稳定 hash、拆包、运行时代码分离。

如果你把所有配置都揉在一个文件里,短期能跑,长期很难维护。


二、先给一套当前还能落地的基线配置

下面这套不是“最炫配置”,而是更接近企业项目里常见的 Webpack 5 基线。

2.1 webpack.common.cjs

如果你的项目使用 "type": "module",或者配置文件后缀是 .mjs,可以改成 import/export。这里先用兼容面更广的 CommonJS。

const path = require('node:path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: path.resolve(__dirname, 'src/main.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[contenthash:8][ext][query]',
publicPath: '/',
clean: true,
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.[jt]sx?$/i,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
},
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
}),
],
cache: {
type: 'filesystem',
},
stats: 'errors-warnings',
};

2.2 webpack.dev.cjs

const {merge} = require('webpack-merge');
const path = require('node:path');
const common = require('./webpack.common.cjs');

module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
devServer: {
static: {
directory: path.resolve(__dirname, 'public'),
},
port: 3000,
open: true,
hot: true,
compress: true,
historyApiFallback: true,
client: {
overlay: true,
},
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});

2.3 webpack.prod.cjs

const {merge} = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const common = require('./webpack.common.cjs');

module.exports = merge(common, {
mode: 'production',
devtool: 'hidden-source-map',
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css',
}),
],
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
},
minimizer: ['...', new CssMinimizerPlugin()],
},
});

三、这套配置为什么是“常规基线”?

3.1 mode:不是装饰项,而是默认策略开关

很多人把 mode 当成“顺手一写”,这是不对的。

它背后会影响很多默认行为,比如:

  • 是否压缩
  • 是否启用更激进的优化
  • 是否更偏向调试体验
  • 某些缓存和性能相关默认值

面试里一句话要讲清楚:

  • development 追求构建反馈速度和可调试性
  • production 追求产物体积、运行性能和缓存稳定性

3.2 entry + output:定义输入和产物边界

entry 很好理解,就是从哪开始构建。

output 里常规项目最该讲的不是 path,而是这几个点:

  • filename / chunkFilename[contenthash],是为了让浏览器缓存更稳定
  • assetModuleFilename 是 Webpack 5 里统一管理静态资源输出路径的关键位
  • clean: true 可以在每次构建前清理旧产物
  • publicPath: '/' 对前端路由和静态资源基路径很重要

很多线上 404、资源路径错乱、发布后缓存不生效,本质都和这一层有关。

3.3 resolve:减少导入噪声,统一工程语义

常规项目里,resolve 主要解决两类问题:

  • 路径别名 例如 @/components/Button
  • 自动补全扩展名 避免每次都写 .js.ts.tsx

它不是性能优化主角,但对代码可维护性很重要。

3.4 module.rules:Webpack 5 时代先记住“资源优先走 Asset Modules”

Webpack 5 的一个明显变化是:静态资源不再优先依赖 file-loader / url-loader / raw-loader

现在更常规的思路是:

  • 图片:type: 'asset'
  • 字体:type: 'asset/resource'
  • 文本原始内容:按需用 asset/source

为什么这算“最新常规配置”?

因为 Webpack 5 已经把这套能力内建进模块系统了。你继续沿用老 loader 不是不能跑,但从认知上已经不属于当前最推荐的基础写法。

3.5 CSS 为什么开发用 style-loader,生产用 MiniCssExtractPlugin

这是非常经典的面试点。

开发环境:

  • 更关注热更新快
  • 样式直接注入页面更方便
  • 调试链路更短

生产环境:

  • 更关注 CSS 独立缓存
  • 更关注首屏资源加载策略
  • 更适合把 CSS 抽成单独文件

所以:

  • 开发用 style-loader
  • 生产用 MiniCssExtractPlugin

这不是“两个都能用,随便选”,而是目标不同。

3.6 devServer:只解决开发期体验,不参与正式打包

常规配置里,devServer 高概率会写这些:

  • static:本地静态目录
  • port:端口
  • open:启动后自动打开
  • hot:热更新
  • historyApiFallback:SPA 刷新路由不 404
  • proxy:把 /api 代理到后端服务
  • client.overlay:把编译错误直接打到页面上

这里的高频误区是:

  • 误区:devServer 会影响生产环境。 不会。它是开发服务器行为配置,不是线上服务配置。

3.7 devtool:不要用一句“开 source map”糊弄过去

devtool 的本质是 调试精度、构建速度、源码暴露程度 的平衡。

常规项目里可以这样回答:

  • 开发环境常用 eval-source-map 质量高,适合本地调试
  • 生产环境如果只给错误监控平台用,常用 hidden-source-map 生成 map,但不把引用暴露给浏览器
  • 如果你不打算保留线上 Source Map,可以直接 false

真正稳的表达不是背某一个字符串,而是能解释:为什么开发和生产不能共用一套 Source Map 策略。

3.8 cache:Webpack 5 里最值得主动开的常规提速项之一

很多人只盯 Loader 数量,却忘了 Webpack 5 现在有比较成熟的持久化缓存能力。

常规项目里,把:

cache: {
type: 'filesystem',
}

作为基线是合理的,因为它能显著改善二次构建速度。

如果你继续往前答,可以补一句:

  • 当配置文件本身变化时,最好补 buildDependencies.config
  • 多配置场景下,可以给不同构建定义不同 cache name

3.9 optimization:先做稳定拆包,再谈精细策略

常规项目里,最常见也最稳的一步是:

optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
},
}

它解决的是:

  • 运行时代码和业务代码解耦
  • 公共依赖不要重复打进多个 chunk
  • 浏览器缓存命中率更稳定

面试里有个很容易加分的点:

  • 先用通用拆包基线
  • 再根据打包分析结果决定要不要细拆 cacheGroups

一上来就手写一堆 vendors、commons、react、chart、editor 分组,不一定更专业,很多时候只是把缓存策略复杂化了。


四、常规项目里,哪些配置现在已经不算“首选”?

4.1 不要把 Webpack 5 还讲成 file-loader / url-loader 时代

如果面试官问“图片怎么处理”,你第一反应还是:

  • file-loader
  • url-loader
  • raw-loader

那你的知识点大概率停在 Webpack 4。

Webpack 5 常规回答应该优先讲:

  • asset/resource
  • asset/inline
  • asset/source
  • asset

4.2 不要把所有环境都放进一个配置文件里

单文件大配置的常见问题是:

  • 条件分支越来越多
  • 可读性越来越差
  • 不同环境互相污染
  • 改动风险变大

所以常规工程实践才会强调 common/dev/prod 拆分。

4.3 不要把 splitChunks 当成“随便拆越多越好”

拆得太碎会带来:

  • 请求数增加
  • chunk 关系复杂
  • 缓存命中策略更难推理
  • 首屏链路反而变长

Webpack 优化不是“越多配置越强”,而是“刚好够用”。

4.4 不要把前端密钥硬塞进 DefinePlugin

前端里能被打包进去的变量,本质都能被用户拿到。

所以你可以注入:

  • 接口基地址
  • 构建标识
  • feature flag

但不要注入:

  • 私有 token
  • 数据库密码
  • 服务端密钥

这是工程边界问题,不是 Webpack 语法问题。


五、典型题 & 标准答法

Q1:最新 Webpack 常规配置里,最核心的几项是什么?

可以按“输入、转换、输出、开发体验、生产优化”来答:

  • 输入:entry
  • 输出:output
  • 转换:module.rules
  • 扩展:plugins
  • 开发:devServerdevtool
  • 生产:optimizationMiniCssExtractPlugin
  • 提速:cache.filesystem

这样答比单纯背配置项更有结构。

Q2:为什么要把开发和生产配置拆开?

因为两者目标冲突。开发追求反馈快、热更新快、调试友好;生产追求体积小、缓存稳定、资源可控。如果强行共用一套,最后通常是谁都照顾不好。

Q3:Webpack 5 里图片和字体常规怎么配?

优先用 Asset Modules。图片通常用 type: 'asset',小图转 data URI、大图输出文件;字体通常用 type: 'asset/resource',直接发射到输出目录。

Q4:生产环境为什么常配 runtimeChunk: 'single'

因为这样能把运行时代码单独拆出去,减少业务 chunk 因 runtime 变化而一起失效,有利于长期缓存。

Q5:线上 Source Map 应该怎么配?

看你的目标:

  • 只想最快构建:devtool: false
  • 要给错误监控平台做堆栈还原:hidden-source-map
  • 要保留完整线上调试能力:source-map

但后两者都要考虑源码暴露和部署策略。


六、常见追问

6.1 contenthashchunkhashhash 有什么区别?

面试里常规项目优先回答:

  • 现在最常用的是 contenthash
  • 它更贴近文件内容变化,有利于长期缓存

不要把重点放在死记历史细节,而是讲清楚为什么现代项目更偏向 contenthash

6.2 babel-loader 和 Babel 是什么关系?

  • Babel 是实际做语法转换的编译器
  • babel-loader 是 Webpack 接入 Babel 的桥梁

Loader 负责接进 Webpack,Babel 负责转换代码。

6.3 devServer.proxy 和 Nginx 反向代理是什么关系?

  • devServer.proxy 解决的是本地开发联调
  • Nginx 反向代理解决的是线上流量入口和转发

前者是开发期工具能力,后者是正式部署能力,不能混为一谈。


七、易错点 / 坑

  • 坑 1:开发也抽离 CSS。 通常会让 HMR 体验变差,调试收益也不大。

  • 坑 2:生产还用很重的内联 Source Map。 容易让产物膨胀,还可能暴露源码。

  • 坑 3:长期缓存只开 [contenthash],却不拆 runtime。 这样缓存稳定性往往还不够理想。

  • 坑 4:继续沿用老资源 loader,又没处理好重复发射。 在 Webpack 5 里容易把资源链路弄得更混乱。

  • 坑 5:把构建慢全部归因于 Webpack。 很多时候真正慢的是 Babel、PostCSS、大量无效依赖、过度拆包、Source Map 策略太重。


八、速记要点(可背诵)

  • 最新常规 Webpack 配置,默认讲 Webpack 5
  • 配置拆分:common / dev / prod
  • 静态资源:优先 Asset Modules
  • 开发样式:style-loader
  • 生产样式:MiniCssExtractPlugin
  • 开发调试:高质量、快反馈的 devtool
  • 生产调试:按是否需要线上还原堆栈决定 hidden-source-map / source-map / false
  • 常规提速:cache.type = 'filesystem'
  • 常规拆包:splitChunks.chunks = 'all' + runtimeChunk = 'single'
  • 核心原则:开发快、生产稳、缓存准、资源路径清晰