Skip to content
On this page

Webpack 实战案例

本章将通过实际项目案例来演示 Webpack 在不同场景下的应用。

案例一:React 单页应用

项目结构

react-spa/
├── src/
│   ├── components/
│   ├── pages/
│   ├── utils/
│   ├── styles/
│   └── index.js
├── public/
│   └── index.html
├── config/
│   ├── webpack.common.js
│   ├── webpack.dev.js
│   └── webpack.prod.js
└── package.json

基础配置

javascript
// config/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'),
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: {
                  browsers: ['last 2 versions']
                }
              }],
              ['@babel/preset-react', {
                runtime: 'automatic'  // React 17 新的 JSX 转换
              }]
            ]
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset/resource'
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],
  
  resolve: {
    extensions: ['.js', '.jsx'],
    alias: {
      '@': path.resolve(__dirname, '../src'),
      '@components': path.resolve(__dirname, '../src/components')
    }
  }
};
javascript
// config/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',
  devtool: 'eval-source-map',
  
  devServer: {
    static: './dist',
    hot: true,
    open: true,
    port: 3000,
    historyApiFallback: true
  },
  
  plugins: [
    ...common.plugins,
    new ReactRefreshWebpackPlugin()
  ],
  
  module: {
    rules: [
      ...common.module.rules.slice(0, 1),  // 复用 JS 规则
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: {
                  browsers: ['last 2 versions']
                }
              }],
              ['@babel/preset-react', {
                runtime: 'automatic'
              }]
            ],
            plugins: [
              require.resolve('react-refresh/babel')
            ]
          }
        }
      },
      ...common.module.rules.slice(1)  // 复用其他规则
    ]
  }
});
javascript
// config/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: [
      ...common.module.rules.slice(0, 1),  // 复用 JS 规则
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: {
                  browsers: [
                    '> 1%',
                    'last 2 versions',
                    'not dead'
                  ]
                },
                useBuiltIns: 'usage',
                corejs: 3
              }],
              ['@babel/preset-react', {
                runtime: 'automatic'
              }]
            ]
          }
        }
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,  // 生产环境提取 CSS
          'css-loader',
          'postcss-loader'
        ]
      },
      ...common.module.rules.slice(2)  // 复用其他规则
    ]
  },
  
  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
        }
      }
    },
    runtimeChunk: 'single'
  },
  
  plugins: [
    ...common.plugins,
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ],
  
  output: {
    ...common.output,
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js'
  }
});

React 组件示例

javascript
// src/components/Button.jsx
import React from 'react';
import './Button.css';

const Button = ({ children, onClick, variant = 'primary' }) => {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

export default Button;
javascript
// src/pages/Home.jsx
import React, { useState, useEffect } from 'react';
import Button from '@components/Button';

const Home = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // 动态导入 API 模块
    import('../utils/api').then(({ fetchData }) => {
      fetchData().then(setData);
    });
  }, []);

  return (
    <div className="home">
      <h1>首页</h1>
      <Button onClick={() => console.log('点击事件')}>点击我</Button>
      <ul>
        {data.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default Home;

案例二:Vue.js 多页面应用

项目结构

vue-mpa/
├── src/
│   ├── pages/
│   │   ├── home/
│   │   │   ├── main.js
│   │   │   ├── App.vue
│   │   │   └── components/
│   │   ├── about/
│   │   │   ├── main.js
│   │   │   └── App.vue
│   │   └── contact/
│   │       ├── main.js
│   │       └── App.vue
├── public/
│   └── index.html
└── webpack.config.js

Vue.js 配置

javascript
// webpack.config.js
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 自动获取入口
const getEntries = () => {
  const fs = require('fs');
  const pagesPath = path.resolve(__dirname, 'src/pages');
  const entries = {};
  
  fs.readdirSync(pagesPath).forEach(page => {
    const pagePath = path.join(pagesPath, page);
    if (fs.statSync(pagePath).isDirectory()) {
      entries[page] = path.join(pagePath, 'main.js');
    }
  });
  
  return entries;
};

const entries = getEntries();

module.exports = {
  mode: 'development',
  entry: entries,
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name]/js/[name].[contenthash:8].js',
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        },
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  
  plugins: [
    new VueLoaderPlugin(),
    
    // 为每个页面生成 HTML
    ...Object.keys(entries).map(name => 
      new HtmlWebpackPlugin({
        template: './public/index.html',
        filename: `${name}/index.html`,
        chunks: [name],
        inject: true
      })
    )
  ],
  
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
};

案例三:Node.js 服务端应用

项目结构

node-api/
├── src/
│   ├── controllers/
│   ├── routes/
│   ├── middleware/
│   ├── models/
│   └── server.js
├── webpack.config.js
└── package.json

Node.js 配置

javascript
// webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'production',
  target: 'node',  // 针对 Node.js 环境
  entry: './src/server.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'server.js',
    libraryTarget: 'commonjs2'  // 输出为 CommonJS 模块
  },
  
  externals: [
    nodeExternals({  // 排除 node_modules 中的模块
      allowlist: ['webpack/hot/poll?100']
    })
  ],
  
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: {
                  node: '14'  // 目标 Node.js 版本
                }
              }]
            ]
          }
        },
        exclude: /node_modules/
      }
    ]
  },
  
  optimization: {
    minimize: false  // Node.js 应用通常不需要压缩
  },
  
  plugins: [
    // 可以添加 Node.js 相关插件
  ]
};

案例四:库打包

项目结构

my-library/
├── src/
│   ├── index.js
│   ├── utils.js
│   └── components/
├── webpack.config.js
└── package.json

库打包配置

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

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-library.js',
    library: {
      name: 'MyLibrary',
      type: 'umd',  // 支持多种模块系统
      umdNamedDefine: true
    },
    globalObject: 'this'
  },
  
  optimization: {
    minimize: true
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                modules: false  // 不转换模块系统,让 Webpack 处理
              }]
            ]
          }
        },
        exclude: /node_modules/
      }
    ]
  },
  
  externals: {
    // 外部依赖,不打包进库中
    lodash: {
      commonjs: 'lodash',
      commonjs2: 'lodash',
      amd: 'lodash',
      root: '_'
    }
  }
};

库入口文件

javascript
// src/index.js
import { debounce, throttle } from 'lodash';
import { formatDate, validateEmail } from './utils';

class MyLibrary {
  static debounce = debounce;
  static throttle = throttle;
  static formatDate = formatDate;
  static validateEmail = validateEmail;
  
  static init(options = {}) {
    console.log('MyLibrary initialized with:', options);
    return this;
  }
  
  static doSomething(data) {
    return `Processed: ${data}`;
  }
}

// 支持 ES6 模块导入
export default MyLibrary;

// 也支持命名导出
export { formatDate, validateEmail };

// 支持 CommonJS
if (typeof module !== 'undefined' && module.exports) {
  module.exports = MyLibrary;
  module.exports.default = MyLibrary;
}

案例五:PWA 应用

PWA 配置

javascript
// webpack.pwa.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { GenerateSW } = require('workbox-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                useBuiltIns: 'usage',
                corejs: 3
              }]
            ]
          }
        },
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true
      }
    }),
    
    // 生成 Service Worker
    new GenerateSW({
      swDest: 'sw.js',
      clientsClaim: true,
      skipWaiting: true,
      runtimeCaching: [
        {
          urlPattern: /^https:\/\/api\./,
          handler: 'StaleWhileRevalidate',
          options: {
            cacheName: 'api-cache',
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 300  // 5分钟
            }
          }
        },
        {
          urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
          handler: 'CacheFirst',
          options: {
            cacheName: 'images',
            expiration: {
              maxEntries: 60,
              maxAgeSeconds: 30 * 24 * 60 * 60  // 30天
            }
          }
        }
      ]
    })
  ],
  
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        }
      }
    },
    runtimeChunk: 'single'
  }
};

案例六:微前端架构

主应用配置

javascript
// 主应用 webpack 配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'http://localhost:3000/',  // 主应用地址
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        },
        exclude: /node_modules/
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],
  
  optimization: {
    splitChunks: false  // 微前端中通常禁用代码分割
  },
  
  // 模块联邦配置
  plugins: [
    new (require('webpack')).container.ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        mfe1: 'mfe1@http://localhost:3001/remoteEntry.js',
        mfe2: 'mfe2@http://localhost:3002/remoteEntry.js'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
};

案例七:TypeScript 项目

TypeScript 配置

javascript
// webpack.typescript.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.tsx',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx']
  },
  
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
};

TypeScript 类型定义

typescript
// src/types/index.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

构建脚本配置

package.json 脚本

json
{
  "scripts": {
    "dev": "webpack serve --config config/webpack.dev.js",
    "build": "webpack --config config/webpack.prod.js",
    "build:analyze": "ANALYZE=true webpack --config config/webpack.prod.js",
    "build:stats": "webpack --config config/webpack.prod.js --json > stats.json",
    "start": "node dist/server.js",
    "lint": "eslint src/",
    "test": "jest"
  }
}

部署配置

Docker 部署

text
# Dockerfile
FROM node:16-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:16-alpine
WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY . .

RUN npm run build

EXPOSE 3000
CMD ["npm", "start"]

Nginx 配置

nginx
# nginx.conf
server {
    listen 80;
    server_name example.com;
    
    location / {
        root /var/www/dist;
        try_files $uri $uri/ /index.html;
    }
    
    location /api/ {
        proxy_pass http://api-server:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

小结

这些实战案例展示了 Webpack 在不同场景下的应用,包括单页应用、多页应用、服务端应用、库打包、PWA、微前端和 TypeScript 项目。每个案例都有其特定的配置需求和最佳实践,理解这些案例有助于在实际项目中灵活运用 Webpack。