Appearance
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。