Webpack 最佳实践

本章总结 Webpack 的最佳实践,帮助你构建更高效、更易维护的项目配置。这些实践来自实际项目经验,能够提升开发体验和项目质量。


📋 学习目标

  • ✅ 掌握配置文件组织的最佳方式
  • ✅ 学会管理环境变量
  • ✅ 了解性能监控方法
  • ✅ 掌握代码规范和版本管理策略
  • ✅ 学习项目结构优化

1. 配置文件组织

分离开发和生产配置

将配置拆分为公共配置、开发配置和生产配置,使用 webpack-merge 合并。

项目结构

project/
├── webpack/
│   ├── webpack.common.js    # 公共配置
│   ├── webpack.dev.js       # 开发环境配置
│   ├── webpack.prod.js      # 生产环境配置
│   └── webpack.config.js    # 主配置文件
└── ...

公共配置(webpack.common.js)

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
 
module.exports = {
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    publicPath: '/',
    clean: true
  },
  
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, '../src'),
      '@components': path.resolve(__dirname, '../src/components'),
      '@utils': path.resolve(__dirname, '../src/utils')
    }
  },
  
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[hash][ext]'
        }
      }
    ]
  },
  
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html'
    })
  ]
}

开发环境配置(webpack.dev.js)

const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
 
module.exports = merge(common, {
  mode: 'development',
  
  output: {
    filename: 'js/[name].js',
    chunkFilename: 'js/[name].chunk.js'
  },
  
  devtool: 'eval-source-map',
  
  module: {
    rules: [
      {
        test: /\.(css|scss|sass)$/,
        use: [
          'style-loader',
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  },
  
  plugins: [
    new ReactRefreshWebpackPlugin()
  ],
  
  devServer: {
    static: {
      directory: path.join(__dirname, '../public')
    },
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: true,
    compress: true
  },
  
  optimization: {
    runtimeChunk: 'single'
  }
})

生产环境配置(webpack.prod.js)

const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
 
module.exports = merge(common, {
  mode: 'production',
  
  devtool: 'source-map',
  
  module: {
    rules: [
      {
        test: /\.(css|scss|sass)$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  },
  
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    })
  ],
  
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: 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'
  }
})

主配置文件(webpack.config.js)

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

2. 环境变量管理

使用 dotenv

安装依赖:

npm install --save-dev dotenv-webpack

创建环境变量文件

.env.development

NODE_ENV=development
API_URL=http://localhost:8080
PUBLIC_PATH=/

.env.production

NODE_ENV=production
API_URL=https://api.production.com
PUBLIC_PATH=/

配置 Webpack

const Dotenv = require('dotenv-webpack')
 
module.exports = {
  plugins: [
    new Dotenv({
      path: `.env.${process.env.NODE_ENV || 'development'}`,
      safe: true,  // 如果 .env.example 存在,使用它作为模板
      systemvars: true  // 加载系统变量
    }),
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
  ]
}

在代码中使用

// src/config/api.js
const API_URL = process.env.API_URL || 'http://localhost:8080'
 
export default {
  baseURL: API_URL,
  timeout: 10000
}

使用 DefinePlugin

const webpack = require('webpack')
 
module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
      '__DEV__': JSON.stringify(process.env.NODE_ENV === 'development'),
      '__PROD__': JSON.stringify(process.env.NODE_ENV === 'production')
    })
  ]
}

3. 性能监控

使用 Speed Measure Plugin

安装依赖:

npm install --save-dev speed-measure-webpack-plugin

配置

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
 
module.exports = smp.wrap({
  // webpack 配置
  entry: './src/index.js',
  // ... 其他配置
})

输出示例

SMP  ⏱  Loaders
modules with no loaders took 0.23 secs
  module count = 1
modules with loaders took 1.23 secs
  module count = 124
    babel-loader took 0.89 secs
      module count = 124
    css-loader took 0.15 secs
      module count = 45

使用 Bundle Analyzer

安装依赖:

npm install --save-dev webpack-bundle-analyzer

配置

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
 
module.exports = {
  plugins: [
    ...(process.env.ANALYZE ? [
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        openAnalyzer: true,
        reportFilename: 'bundle-report.html'
      })
    ] : [])
  ]
}

使用

{
  "scripts": {
    "build:analyze": "ANALYZE=true npm run build"
  }
}

使用 Webpack Stats

module.exports = {
  stats: {
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false,
    entrypoints: false,
    warnings: false,
    errors: true,
    errorDetails: true
  }
}

4. 代码规范

ESLint 集成

安装依赖:

npm install --save-dev eslint eslint-loader
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

配置

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        enforce: 'pre',
        use: [
          {
            loader: 'eslint-loader',
            options: {
              fix: true,
              cache: true
            }
          }
        ]
      }
    ]
  }
}

Prettier 集成

安装依赖:

npm install --save-dev prettier

配置

.prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100
}

Pre-commit 钩子

安装依赖:

npm install --save-dev husky lint-staged

配置

package.json

{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,md,json}": [
      "prettier --write"
    ]
  }
}

5. 版本管理和缓存策略

使用 Content Hash

module.exports = {
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js'
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    })
  ]
}

分离运行时代码

module.exports = {
  optimization: {
    runtimeChunk: 'single'  // 分离运行时代码,避免 vendor 变化导致所有 chunk 重新构建
  }
}

合理的代码分割

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      cacheGroups: {
        // 框架代码(React、Vue 等)
        framework: {
          test: /[\\/]node_modules[\\/](react|react-dom|vue|vue-router)[\\/]/,
          name: 'framework',
          priority: 40,
          reuseExistingChunk: true
        },
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          reuseExistingChunk: true
        },
        // 公共代码
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
}

6. 项目结构优化

推荐的目录结构

project/
├── public/              # 静态资源
│   ├── index.html
│   └── favicon.ico
├── src/                 # 源代码
│   ├── components/      # 组件
│   ├── pages/          # 页面
│   ├── utils/          # 工具函数
│   ├── styles/          # 样式
│   ├── assets/          # 资源文件
│   ├── config/          # 配置文件
│   └── index.js         # 入口文件
├── webpack/             # Webpack 配置
│   ├── webpack.common.js
│   ├── webpack.dev.js
│   └── webpack.prod.js
├── .env.development     # 开发环境变量
├── .env.production      # 生产环境变量
├── .eslintrc.js         # ESLint 配置
├── .prettierrc          # Prettier 配置
├── tsconfig.json        # TypeScript 配置
└── package.json

路径别名配置

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '../src'),
      '@components': path.resolve(__dirname, '../src/components'),
      '@pages': path.resolve(__dirname, '../src/pages'),
      '@utils': path.resolve(__dirname, '../src/utils'),
      '@styles': path.resolve(__dirname, '../src/styles'),
      '@assets': path.resolve(__dirname, '../src/assets'),
      '@config': path.resolve(__dirname, '../src/config')
    }
  }
}

在 TypeScript 中使用

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@pages/*": ["src/pages/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

7. 构建优化实践

使用缓存

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    },
    cacheDirectory: path.resolve(__dirname, '.webpack-cache')
  }
}

减少解析范围

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

使用多进程构建

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

8. 错误处理

友好的错误提示

module.exports = {
  stats: {
    errorDetails: true,
    errorStack: true,
    warnings: true
  },
  devServer: {
    client: {
      overlay: {
        errors: true,
        warnings: false
      }
    }
  }
}

自定义错误处理

class FriendlyErrorsPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('FriendlyErrorsPlugin', (stats) => {
      if (stats.hasErrors()) {
        const errors = stats.compilation.errors
        errors.forEach(error => {
          console.error('\n❌', error.message)
        })
      }
    })
  }
}

9. 安全实践

避免暴露敏感信息

// ❌ 错误:暴露敏感信息
module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.API_KEY': JSON.stringify('secret-key-123')
    })
  ]
}
 
// ✅ 正确:只在服务端使用
// 敏感信息应该通过环境变量在服务端处理

内容安全策略

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      meta: {
        'Content-Security-Policy': {
          'http-equiv': 'Content-Security-Policy',
          content: "default-src 'self'; script-src 'self' 'unsafe-inline'"
        }
      }
    })
  ]
}

10. 文档和注释

配置文件注释

/**
 * Webpack 公共配置
 * 包含开发和生产环境的共同配置
 */
module.exports = {
  // 入口文件
  entry: './src/index.js',
  
  // 输出配置
  output: {
    // 输出目录
    path: path.resolve(__dirname, '../dist'),
    // 输出文件名(使用 contenthash 实现长期缓存)
    filename: 'js/[name].[contenthash:8].js'
  }
}

README 文档

创建 webpack/README.md

# Webpack 配置说明
 
## 配置文件结构
 
- `webpack.common.js` - 公共配置
- `webpack.dev.js` - 开发环境配置
- `webpack.prod.js` - 生产环境配置
- `webpack.config.js` - 主配置文件
 
## 环境变量
 
- `.env.development` - 开发环境变量
- `.env.production` - 生产环境变量
 
## 构建命令
 
- `npm run dev` - 开发模式
- `npm run build` - 生产构建
- `npm run build:analyze` - 分析打包结果

完整配置示例

企业级配置模板

// webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const Dotenv = require('dotenv-webpack')
 
module.exports = {
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    publicPath: process.env.PUBLIC_PATH || '/',
    clean: true
  },
  
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, '../src')
    }
  },
  
  plugins: [
    new CleanWebpackPlugin(),
    new Dotenv({
      path: `.env.${process.env.NODE_ENV || 'development'}`
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      minify: process.env.NODE_ENV === 'production'
    })
  ]
}

总结

核心最佳实践

  1. 配置文件分离 - 使用 webpack-merge 分离开发和生产配置
  2. 环境变量管理 - 使用 dotenv 管理环境变量
  3. 性能监控 - 使用工具监控构建性能
  4. 代码规范 - 集成 ESLint 和 Prettier
  5. 版本管理 - 使用 contenthash 实现长期缓存
  6. 代码分割 - 合理配置 splitChunks
  7. 项目结构 - 清晰的目录结构和路径别名
  8. 构建优化 - 使用缓存和多进程构建
  9. 错误处理 - 友好的错误提示
  10. 文档完善 - 清晰的注释和文档

下一步


Webpack 最佳实践 配置管理 性能优化 代码规范