Webpack 深入解析

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它将项目中的各种资源(JS、CSS、图片等)视为模块,通过依赖关系图进行打包。

学习路径:学习 Webpack 前需要掌握 JavaScript 基础模块化 概念。Webpack 与 工程化实践性能优化 密切相关。


📚 目录


核心概念

什么是 Webpack

Webpack 是一个模块打包器(Module Bundler),它的主要功能是:

  1. 依赖分析:从入口文件开始,递归分析所有依赖关系
  2. 资源转换:通过 Loader 将各种资源转换为 JavaScript 模块
  3. 代码打包:将所有模块打包成一个或多个 bundle 文件
  4. 优化处理:通过 Plugin 进行代码压缩、优化等处理

核心概念

1. Entry(入口)

指定 Webpack 从哪个文件开始构建依赖图。

module.exports = {
  entry: './src/index.js'
}

2. Output(输出)

指定打包后的文件输出位置和命名规则。

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

3. Loader(加载器)

用于转换非 JavaScript 文件(如 CSS、图片、TypeScript 等)。

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}

4. Plugin(插件)

用于执行更广泛的任务,如打包优化、资源管理、环境变量注入等。

const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}

5. Mode(模式)

指定构建模式:developmentproductionnone

module.exports = {
  mode: 'production'
}

基础配置

最小配置示例

const path = require('path')
 
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  mode: 'development'
}

完整配置结构

const path = require('path')
 
module.exports = {
  // 入口
  entry: './src/index.js',
  
  // 输出
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  
  // 模式
  mode: 'development',
  
  // 模块处理
  module: {
    rules: []
  },
  
  // 插件
  plugins: [],
  
  // 解析配置
  resolve: {
    extensions: ['.js', '.json'],
    alias: {}
  },
  
  // 开发服务器
  devServer: {},
  
  // 优化配置
  optimization: {}
}

Entry 和 Output

Entry 配置

单入口

module.exports = {
  entry: './src/index.js'
}

多入口

module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  }
}

动态入口

module.exports = {
  entry: () => './src/index.js'
}

Output 配置

基础配置

const path = require('path')
 
module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

多入口输出

module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js'  // main.bundle.js, vendor.bundle.js
  }
}

使用 Hash

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',  // 内容 hash
    chunkFilename: '[name].[chunkhash].js'  // chunk hash
  }
}

Hash 类型说明

  • [hash]:整个项目的 hash
  • [chunkhash]:chunk 的 hash
  • [contenthash]:内容的 hash(推荐用于生产环境)

Loader 系统

Loader 工作原理

Loader 是一个函数,接收源文件内容,返回转换后的内容。

module.exports = function(source) {
  // 转换逻辑
  return transformedSource
}

常用 Loader

1. Babel Loader(转译 JavaScript)

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
}

2. CSS Loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',  // 将 CSS 注入到 DOM
          'css-loader'     // 解析 CSS 文件
        ]
      }
    ]
  }
}

3. Sass/Less Loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  }
}

4. 文件资源 Loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[hash][ext]'
        }
      }
    ]
  }
}

5. URL Loader(小文件转 base64)

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024  // 8KB 以下转 base64
          }
        }
      }
    ]
  }
}

6. TypeScript Loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  }
}

Loader 执行顺序

Loader 从右到左(或从下到上)执行:

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'style-loader',   // 3. 最后执行
          'css-loader',     // 2. 然后执行
          'sass-loader'     // 1. 最先执行
        ]
      }
    ]
  }
}

自定义 Loader

// my-loader.js
module.exports = function(source) {
  // source 是源文件内容
  const result = source.replace(/console\.log\(/g, '// console.log(')
  return result
}

使用自定义 Loader:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: path.resolve(__dirname, 'loaders/my-loader.js')
      }
    ]
  }
}

Plugin 系统

Plugin 工作原理

Plugin 是一个类,通过 apply 方法注册到 Webpack 的生命周期钩子中。

class MyPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('构建完成!')
    })
  }
}

常用 Plugin

1. HtmlWebpackPlugin(生成 HTML)

const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true
      }
    })
  ]
}

2. CleanWebpackPlugin(清理输出目录)

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
 
module.exports = {
  plugins: [
    new CleanWebpackPlugin()
  ]
}

3. MiniCssExtractPlugin(提取 CSS)

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
 
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css'
    })
  ]
}

4. DefinePlugin(定义环境变量)

const webpack = require('webpack')
 
module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
      'process.env.API_URL': JSON.stringify('https://api.example.com')
    })
  ]
}

5. ProvidePlugin(自动引入模块)

const webpack = require('webpack')
 
module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    })
  ]
}

6. BundleAnalyzerPlugin(分析打包结果)

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false
    })
  ]
}

自定义 Plugin

class MyPlugin {
  constructor(options) {
    this.options = options
  }
  
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 在生成文件之前执行
      console.log('准备生成文件...')
      callback()
    })
  }
}
 
module.exports = MyPlugin

代码分割与优化

代码分割方式

1. Entry 分割

module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  }
}

2. SplitChunks 自动分割

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
}

3. 动态导入(Dynamic Import)

// 使用 import() 动态导入
import('./module').then(module => {
  module.doSomething()
})
 
// 或使用 React.lazy
const LazyComponent = React.lazy(() => import('./LazyComponent'))

SplitChunks 配置详解

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',  // 'initial' | 'async' | 'all'
      minSize: 20000,  // 最小 chunk 大小
      maxSize: 0,  // 最大 chunk 大小
      minChunks: 1,  // 最小引用次数
      maxAsyncRequests: 30,  // 最大异步请求数
      maxInitialRequests: 30,  // 最大初始请求数
      cacheGroups: {
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        },
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10
        }
      }
    }
  }
}

开发环境配置

DevServer 配置

module.exports = {
  devServer: {
    contentBase: './dist',
    port: 3000,
    hot: true,  // 热模块替换
    open: true,  // 自动打开浏览器
    compress: true,  // 启用 gzip 压缩
    historyApiFallback: true,  // SPA 路由支持
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

热模块替换(HMR)

const webpack = require('webpack')
 
module.exports = {
  devServer: {
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}

在代码中使用 HMR:

if (module.hot) {
  module.hot.accept('./module', () => {
    // 模块更新后的处理逻辑
  })
}

Source Map 配置

module.exports = {
  devtool: 'source-map',  // 开发环境推荐
  // 或
  devtool: 'eval-source-map',  // 开发环境快速构建
  // 生产环境
  devtool: 'hidden-source-map'  // 或 false
}

Source Map 类型

  • source-map:生成独立的 .map 文件,最完整但最慢
  • eval-source-map:使用 eval 包裹,适合开发环境
  • cheap-module-source-map:不包含列信息,构建较快
  • hidden-source-map:生成 .map 文件但不引用,适合生产环境

生产环境优化

代码压缩

const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
 
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true  // 移除 console
          }
        }
      }),
      new CssMinimizerPlugin()
    ]
  }
}

Tree Shaking

Tree Shaking 用于移除未使用的代码。

module.exports = {
  mode: 'production',  // 自动启用 Tree Shaking
  optimization: {
    usedExports: true,
    sideEffects: false  // 标记无副作用
  }
}

package.json 中标记副作用:

{
  "sideEffects": ["*.css", "*.scss"]
}

作用域提升(Scope Hoisting)

module.exports = {
  optimization: {
    concatenateModules: true  // 启用作用域提升
  }
}

性能优化

构建速度优化

1. 使用缓存

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  }
}

2. 减少解析范围

module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
    extensions: ['.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,  // 排除 node_modules
        use: 'babel-loader'
      }
    ]
  }
}

3. 使用多进程构建

const TerserPlugin = require('terser-webpack-plugin')
 
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true  // 启用多进程
      })
    ]
  }
}

4. 使用 DllPlugin(动态链接库)

// webpack.dll.js
const webpack = require('webpack')
 
module.exports = {
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_library',
      path: path.resolve(__dirname, 'dll/[name].manifest.json')
    })
  ]
}
 
// webpack.config.js
const webpack = require('webpack')
 
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dll/vendor.manifest.json')
    })
  ]
}

运行时性能优化

1. 代码分割

参考 代码分割与优化 部分。

2. 懒加载

// 路由懒加载
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))

3. 预加载和预获取

// 预加载(高优先级)
import(/* webpackPreload: true */ './module')
 
// 预获取(低优先级)
import(/* webpackPrefetch: true */ './module')

常见问题排查

1. 路径解析问题

问题Module not found: Can't resolve 'xxx'

解决方案

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    },
    extensions: ['.js', '.json', '.jsx']
  }
}

2. 依赖冲突

问题:多个版本的同名依赖

解决方案

module.exports = {
  resolve: {
    alias: {
      'react': path.resolve(__dirname, 'node_modules/react')
    }
  }
}

3. 构建速度慢

解决方案

  • 使用缓存
  • 减少解析范围
  • 使用多进程构建
  • 使用 DllPlugin

4. 内存溢出

问题JavaScript heap out of memory

解决方案

# 增加 Node.js 内存限制
node --max-old-space-size=4096 node_modules/.bin/webpack

5. 样式不生效

问题:CSS 样式未正确加载

解决方案

  • 检查 Loader 配置顺序
  • 检查 sideEffects 配置
  • 使用 MiniCssExtractPlugin 提取 CSS

最佳实践

1. 配置文件组织

// webpack.common.js - 公共配置
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}
 
// webpack.dev.js - 开发环境
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
 
module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map',
  devServer: {
    hot: true
  }
})
 
// webpack.prod.js - 生产环境
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
 
module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map',
  optimization: {
    minimize: true
  }
})

2. 环境变量管理

// 使用 dotenv
require('dotenv').config()
 
module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL)
    })
  ]
}

3. 性能监控

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
 
module.exports = smp.wrap({
  // webpack 配置
})

4. 代码规范

  • 使用 ESLint 检查代码
  • 使用 Prettier 格式化代码
  • 配置 pre-commit 钩子

5. 版本管理

  • 使用 [contenthash] 实现长期缓存
  • 合理配置 splitChunks
  • 使用 runtimeChunk 分离运行时代码
module.exports = {
  optimization: {
    runtimeChunk: 'single',  // 分离运行时代码
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
}

实战案例

案例一:从零搭建基础 Webpack 项目

1. 项目初始化

# 创建项目目录
mkdir webpack-demo && cd webpack-demo
 
# 初始化 package.json
npm init -y
 
# 安装 Webpack 核心依赖
npm install --save-dev webpack webpack-cli
 
# 安装常用 Loader 和 Plugin
npm install --save-dev html-webpack-plugin
npm install --save-dev style-loader css-loader
npm install --save-dev babel-loader @babel/core @babel/preset-env

2. 项目结构

webpack-demo/
├── src/
│   ├── index.js          # 入口文件
│   ├── index.html        # HTML 模板
│   ├── styles/
│   │   └── main.css      # 样式文件
│   └── utils/
│       └── helper.js     # 工具函数
├── dist/                 # 输出目录(自动生成)
├── webpack.config.js     # Webpack 配置
└── package.json

3. 创建源文件

src/index.js

import './styles/main.css'
import { greet } from './utils/helper'
 
console.log('Webpack 项目启动成功!')
greet('Webpack')

src/utils/helper.js

export function greet(name) {
  console.log(`Hello, ${name}!`)
}

src/styles/main.css

body {
  margin: 0;
  padding: 20px;
  font-family: Arial, sans-serif;
  background: #f5f5f5;
}
 
h1 {
  color: #333;
}

src/index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Webpack Demo</title>
</head>
<body>
  <div id="app">
    <h1>Webpack 项目</h1>
  </div>
</body>
</html>

4. 基础 Webpack 配置

webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  // 入口文件
  entry: './src/index.js',
  
  // 输出配置
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.[contenthash].js',
    clean: true  // 清理输出目录
  },
  
  // 模式
  mode: 'development',
  
  // 开发工具
  devtool: 'eval-source-map',
  
  // 模块处理
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  
  // 插件
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html'
    })
  ],
  
  // 开发服务器
  devServer: {
    static: './dist',
    port: 3000,
    hot: true,
    open: true
  }
}

5. 配置 package.json 脚本

{
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack serve --mode development",
    "watch": "webpack --watch --mode development"
  }
}

6. 运行项目

# 开发模式
npm run dev
 
# 生产构建
npm run build

案例二:React 项目完整配置

1. 安装依赖

npm install react react-dom
npm install --save-dev @babel/preset-react
npm install --save-dev @babel/preset-typescript
npm install --save-dev typescript ts-loader
npm install --save-dev sass-loader sass
npm install --save-dev file-loader url-loader

2. 项目结构

react-app/
├── src/
│   ├── index.tsx         # 入口文件
│   ├── App.tsx           # 根组件
│   ├── components/       # 组件目录
│   ├── styles/           # 样式目录
│   └── assets/           # 静态资源
├── public/
│   └── index.html
├── tsconfig.json
├── webpack.config.js
└── package.json

3. TypeScript 配置

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "jsx": "react-jsx",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

4. Webpack 配置(开发环境)

webpack.dev.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
 
module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: '/'
  },
  
  devtool: 'eval-source-map',
  
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.jsx'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env',
              '@babel/preset-react',
              '@babel/preset-typescript'
            ],
            plugins: [
              'react-refresh/babel'  // HMR 支持
            ]
          }
        }
      },
      {
        test: /\.(css|scss|sass)$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]__[hash:base64:5]'
              }
            }
          },
          'sass-loader'
        ]
      },
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[hash][ext]'
        }
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext]'
        }
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html'
    }),
    new ReactRefreshWebpackPlugin()  // React 热更新
  ],
  
  devServer: {
    static: {
      directory: path.join(__dirname, 'public')
    },
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: true,  // SPA 路由支持
    compress: true
  }
}

5. Webpack 配置(生产环境)

webpack.prod.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
 
module.exports = {
  mode: 'production',
  entry: './src/index.tsx',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash].js',
    chunkFilename: 'js/[name].[contenthash].chunk.js',
    publicPath: '/',
    clean: true
  },
  
  devtool: 'source-map',
  
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.jsx'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.(css|scss|sass)$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]__[hash:base64:5]'
              }
            }
          },
          'sass-loader'
        ]
      },
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[hash][ext]'
        }
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true
      }
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
      chunkFilename: 'css/[name].[contenthash].chunk.css'
    })
  ],
  
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true
          }
        }
      }),
      new CssMinimizerPlugin()
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          reuseExistingChunk: true
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    runtimeChunk: 'single'
  }
}

6. 合并配置(webpack-merge)

webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  entry: './src/index.tsx',
  
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.jsx'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
}

webpack.config.js

const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
 
module.exports = (env, argv) => {
  if (argv.mode === 'production') {
    return merge(common, require('./webpack.prod.js'))
  }
  return merge(common, require('./webpack.dev.js'))
}

案例三:Vue 项目完整配置

1. 安装依赖

npm install vue@next
npm install --save-dev vue-loader vue-template-compiler
npm install --save-dev @vue/compiler-sfc

2. Webpack 配置

webpack.config.js

const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
 
module.exports = {
  mode: 'development',
  entry: './src/main.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true
  },
  
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'vue$': 'vue/dist/vue.esm-bundler.js'
    }
  },
  
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: [
          process.env.NODE_ENV !== 'production'
            ? 'style-loader'
            : MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.scss$/,
        use: [
          process.env.NODE_ENV !== 'production'
            ? 'style-loader'
            : MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  },
  
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    ...(process.env.NODE_ENV === 'production'
      ? [new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash].css' })]
      : [])
  ],
  
  devServer: {
    static: './dist',
    port: 8080,
    hot: true,
    open: true
  }
}

3. Vue 组件示例

src/App.vue

<template>
  <div class="app">
    <h1>{{ message }}</h1>
    <button @click="handleClick">点击我</button>
  </div>
</template>
 
<script>
export default {
  name: 'App',
  data() {
    return {
      message: 'Hello Vue with Webpack!'
    }
  },
  methods: {
    handleClick() {
      this.message = '按钮被点击了!'
    }
  }
}
</script>
 
<style scoped>
.app {
  padding: 20px;
  text-align: center;
}
 
h1 {
  color: #42b983;
}
</style>

案例四:多页面应用(MPA)配置

1. 项目结构

mpa-project/
├── src/
│   ├── pages/
│   │   ├── home/
│   │   │   ├── index.js
│   │   │   └── index.html
│   │   ├── about/
│   │   │   ├── index.js
│   │   │   └── index.html
│   │   └── contact/
│   │       ├── index.js
│   │       └── index.html
│   └── shared/
│       ├── utils.js
│       └── styles.css
└── webpack.config.js

2. 动态生成多入口配置

webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const glob = require('glob')
 
// 动态获取所有页面入口
function getEntries() {
  const entries = {}
  const htmlPlugins = []
  
  glob.sync('./src/pages/**/index.js').forEach(file => {
    const name = file.match(/\/pages\/(.+)\/index\.js$/)[1]
    entries[name] = file
    
    htmlPlugins.push(
      new HtmlWebpackPlugin({
        template: file.replace('index.js', 'index.html'),
        filename: `${name}.html`,
        chunks: [name]
      })
    )
  })
  
  return { entries, htmlPlugins }
}
 
const { entries, htmlPlugins } = getEntries()
 
module.exports = {
  entry: entries,
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash].js',
    clean: true
  },
  
  plugins: [
    ...htmlPlugins
  ],
  
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        common: {
          name: 'common',
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
}

案例五:实际项目配置示例

完整的企业级配置

webpack.config.js

const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
 
const isProduction = process.env.NODE_ENV === 'production'
 
module.exports = {
  mode: isProduction ? 'production' : 'development',
  
  entry: {
    main: './src/index.js',
    vendor: ['react', 'react-dom']  // 单独打包第三方库
  },
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: isProduction 
      ? 'js/[name].[contenthash:8].js'
      : 'js/[name].js',
    chunkFilename: isProduction
      ? 'js/[name].[contenthash:8].chunk.js'
      : 'js/[name].chunk.js',
    publicPath: '/',
    clean: true,
    assetModuleFilename: 'assets/[hash][ext][query]'
  },
  
  devtool: isProduction ? 'source-map' : 'eval-source-map',
  
  resolve: {
    extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
      '@assets': path.resolve(__dirname, 'src/assets')
    },
    modules: [path.resolve(__dirname, 'src'), 'node_modules']
  },
  
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            presets: [
              ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }],
              '@babel/preset-react',
              '@babel/preset-typescript'
            ],
            plugins: [
              '@babel/plugin-proposal-class-properties',
              !isProduction && 'react-refresh/babel'
            ].filter(Boolean)
          }
        }
      },
      {
        test: /\.css$/,
        use: [
          isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[local]__[hash:base64:5]'
              },
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      },
      {
        test: /\.(png|jpg|jpeg|gif|svg|webp)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024  // 8KB 以下转 base64
          }
        },
        generator: {
          filename: 'images/[hash][ext]'
        }
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext]'
        }
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html',
      minify: isProduction ? {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        minifyJS: true,
        minifyCSS: true
      } : false
    }),
    
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_URL': JSON.stringify(process.env.API_URL || '')
    }),
    
    ...(isProduction
      ? [
          new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash:8].css',
            chunkFilename: 'css/[name].[contenthash:8].chunk.css'
          })
        ]
      : [new webpack.HotModuleReplacementPlugin()]),
    
    // 分析打包结果(可选)
    ...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : [])
  ],
  
  optimization: {
    minimize: isProduction,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: isProduction,
            drop_debugger: isProduction
          }
        }
      }),
      new CssMinimizerPlugin()
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          reuseExistingChunk: true
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    runtimeChunk: {
      name: 'runtime'
    }
  },
  
  devServer: {
    static: {
      directory: path.join(__dirname, 'public')
    },
    port: 3000,
    hot: true,
    open: true,
    compress: true,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  },
  
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  },
  
  performance: {
    hints: isProduction ? 'warning' : false,
    maxEntrypointSize: 512000,
    maxAssetSize: 512000
  }
}

package.json 脚本

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve",
    "build": "cross-env NODE_ENV=production webpack",
    "build:analyze": "cross-env NODE_ENV=production ANALYZE=true webpack",
    "build:prod": "cross-env NODE_ENV=production webpack --mode production"
  }
}

实战技巧总结

1. 环境变量管理

创建 .env.development.env.production

# .env.development
NODE_ENV=development
API_URL=http://localhost:8080
 
# .env.production
NODE_ENV=production
API_URL=https://api.production.com

使用 dotenv-webpack 加载:

const Dotenv = require('dotenv-webpack')
 
module.exports = {
  plugins: [
    new Dotenv({
      path: `.env.${process.env.NODE_ENV || 'development'}`
    })
  ]
}

2. 路径别名配置

resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src'),
    '@components': path.resolve(__dirname, 'src/components'),
    '@utils': path.resolve(__dirname, 'src/utils'),
    '@styles': path.resolve(__dirname, 'src/styles')
  }
}

3. 代码分割策略

optimization: {
  splitChunks: {
    chunks: 'all',
    maxInitialRequests: 25,
    minSize: 20000,
    cacheGroups: {
      framework: {
        test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
        name: 'framework',
        priority: 40
      },
      lib: {
        test: /[\\/]node_modules[\\/]/,
        name: 'lib',
        priority: 30
      },
      common: {
        minChunks: 2,
        priority: 10,
        reuseExistingChunk: true
      }
    }
  }
}

4. 性能监控

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
 
module.exports = smp.wrap({
  // webpack 配置
})

📖 相关资源


Webpack 构建工具 工程化 前端工具链