Webpack

Webpack 是现代 JavaScript 应用最流行的静态模块打包工具,它能将各种资源(JS、CSS、图片等)打包成浏览器可识别的静态资源。

Webpack 核心概念

五大核心概念

概念 说明
Entry 入口,构建的起点
Output 输出,打包结果的配置
Loader 加载器,处理非 JS 文件
Plugin 插件,扩展 Webpack 功能
Mode 模式,development 或 production

基本配置

// webpack.config.js
const path = require('path')

module.exports = {
  // 入口
  entry: './src/index.js',
  
  // 输出
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true
  },
  
  // 模式
  mode: 'development',
  
  // 模块规则
  module: {
    rules: []
  },
  
  // 插件
  plugins: [],
  
  // 解析
  resolve: {
    extensions: ['.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

Entry 入口

单入口

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

多入口

module.exports = {
  entry: {
    main: './src/index.js',
    admin: './src/admin.js',
    vendor: ['lodash', 'axios']
  }
}

动态入口

const glob = require('glob')

module.exports = {
  entry: glob.sync('./src/pages/*/index.js').reduce((entries, path) => {
    const name = path.match(/\/pages\/(.*)\/index\.js/)[1]
    entries[name] = path
    return entries
  }, {})
}

Output 输出

基本配置

module.exports = {
  output: {
    // 输出目录
    path: path.resolve(__dirname, 'dist'),
    
    // 输出文件名
    filename: 'bundle.js',
    
    // 多入口文件名
    filename: '[name].[contenthash].js',
    
    // 静态资源文件名
    assetModuleFilename: 'assets/[hash][ext]',
    
    // 清理输出目录
    clean: true,
    
    // 公共路径
    publicPath: '/',
    
    // 库名称(库模式)
    library: {
      name: 'DongbaUtils',
      type: 'umd'
    }
  }
}

占位符

占位符 说明
[name] 入口名称
[id] chunk id
[hash] 构建哈希
[chunkhash] chunk 哈希
[contenthash] 内容哈希
[ext] 文件扩展名

Loader 加载器

Loader 用于处理非 JavaScript 文件。

CSS 处理

module.exports = {
  module: {
    rules: [
      // CSS
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      
      // Sass
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'sass-loader',
            options: {
              additionalData: `$primary: #1890ff;`
            }
          }
        ]
      },
      
      // Less
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      },
      
      // CSS Modules
      {
        test: /\.module\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]--[hash:base64:5]'
              }
            }
          }
        ]
      },
      
      // PostCSS
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: { importLoaders: 1 }
          },
          'postcss-loader'
        ]
      }
    ]
  }
}

JavaScript 处理

module.exports = {
  module: {
    rules: [
      // Babel
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-runtime']
          }
        }
      },
      
      // TypeScript
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      
      // ESLint
      {
        test: /\.js$/,
        exclude: /node_modules/,
        enforce: 'pre',
        use: 'eslint-loader'
      }
    ]
  }
}

资源处理

module.exports = {
  module: {
    rules: [
      // 图片(Webpack 5 资源模块)
      {
        test: /\.(png|jpg|gif|webp|svg)$/,
        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]'
        }
      },
      
      // 文件
      {
        test: /\.(pdf|docx?)$/,
        type: 'asset/resource',
        generator: {
          filename: 'files/[hash][ext]'
        }
      }
    ]
  }
}

其他 Loader

module.exports = {
  module: {
    rules: [
      // Vue
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      
      // HTML 模板
      {
        test: /\.html$/,
        loader: 'html-loader'
      },
      
      // Markdown
      {
        test: /\.md$/,
        use: [
          'html-loader',
          'markdown-loader'
        ]
      },
      
      // CSV/TSV
      {
        test: /\.(csv|tsv)$/,
        use: 'csv-loader'
      },
      
      // XML
      {
        test: /\.xml$/,
        use: 'xml-loader'
      },
      
      // YAML
      {
        test: /\.ya?ml$/,
        use: 'yaml-loader'
      }
    ]
  }
}

Plugin 插件

常用插件

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')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const DefinePlugin = require('webpack').DefinePlugin
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  plugins: [
    // HTML 模板
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      title: '东巴文',
      minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    }),
    
    // 多页面
    new HtmlWebpackPlugin({
      template: './src/admin.html',
      filename: 'admin.html',
      chunks: ['admin']
    }),
    
    // 提取 CSS
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
      chunkFilename: 'css/[id].[contenthash].css'
    }),
    
    // 清理目录
    new CleanWebpackPlugin(),
    
    // 复制文件
    new CopyPlugin({
      patterns: [
        { from: 'public', to: 'public' },
        { from: 'static', to: 'static' }
      ]
    }),
    
    // 环境变量
    new DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_URL': JSON.stringify('https://api.db-w.cn')
    }),
    
    // 打包分析
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false
    })
  ]
}

优化配置

module.exports = {
  optimization: {
    // 压缩 JS
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true
          }
        }
      }),
      new CssMinimizerPlugin()
    ],
    
    // 代码分割
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: 'vendors'
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    },
    
    // 运行时代码
    runtimeChunk: 'single',
    
    // 模块 ID
    moduleIds: 'deterministic'
  }
}

开发服务器

基本配置

module.exports = {
  devServer: {
    // 静态文件目录
    static: {
      directory: path.join(__dirname, 'public'),
      publicPath: '/public'
    },
    
    // 端口
    port: 3000,
    
    // 自动打开浏览器
    open: true,
    
    // 热更新
    hot: true,
    
    // 启用 gzip
    compress: true,
    
    // history 路由回退
    historyApiFallback: true,
    
    // 代理
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    },
    
    // CORS
    headers: {
      'Access-Control-Allow-Origin': '*'
    },
    
    // 控制台输出
    client: {
      logging: 'info',
      overlay: {
        errors: true,
        warnings: false
      }
    }
  }
}

Source Map

module.exports = {
  // 开发环境
  devtool: 'eval-cheap-module-source-map',
  
  // 生产环境
  // devtool: 'source-map'
}
类型 构建速度 重建速度 质量 适用场景
eval 最快 最快 开发
eval-cheap-source-map 行映射 开发
eval-cheap-module-source-map 较快 行映射 开发推荐
source-map 完整 生产
hidden-source-map 完整 生产
nosources-source-map 无源码 生产

缓存优化

module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js'
  },
  
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    },
    cacheDirectory: path.resolve(__dirname, '.webpack_cache')
  },
  
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
}

环境配置

配置分离

// webpack.common.js
const { merge } = require('webpack-merge')
const baseConfig = {
  // 公共配置
}

// webpack.dev.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    hot: true
  }
})

// webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map',
  optimization: {
    minimize: true
  }
})

package.json 脚本

{
  "scripts": {
    "dev": "webpack serve --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js",
    "build:analyze": "webpack --config webpack.prod.js --profile --json > stats.json"
  }
}

完整配置示例

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')
const { DefinePlugin } = require('webpack')

const isProduction = process.env.NODE_ENV === 'production'

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',
    assetModuleFilename: 'assets/[hash][ext]',
    clean: true,
    publicPath: '/'
  },
  
  mode: isProduction ? 'production' : 'development',
  
  devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',
  
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.scss$/,
        use: [
          isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024
          }
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/resource'
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      title: '东巴文',
      minify: isProduction
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[id].[contenthash:8].css'
    }),
    new DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
  ],
  
  optimization: {
    minimize: isProduction,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin()
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    },
    runtimeChunk: 'single'
  },
  
  resolve: {
    extensions: ['.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  devServer: {
    static: './public',
    hot: true,
    port: 3000,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  
  performance: {
    hints: isProduction ? 'warning' : false,
    maxAssetSize: 244 * 1024,
    maxEntrypointSize: 244 * 1024
  }
}

下一步

掌握 Webpack 后,你可以继续学习:


东巴文(db-w.cn)—— 让 Webpack 配置更简单