| 维度 | v1 (原始版本) | v2 (当前版本) |
|---|---|---|
| Webpack 版本 | Webpack 5.x (通用) | Webpack 5.107 (最新稳定版) |
| React 版本 | React 16/17 | React 18+ / 19 (并发特性、Server Components) |
| 热更新方案 | react-hot-loader (已废弃) | @pmmmwh/react-refresh-webpack-plugin v0.6.2 (Fast Refresh) |
| JSX 转换 | @babel/preset-react (classic/automatic) | automatic runtime + jsxImportSource + react-refresh/babel |
| TypeScript 方案 | ts-loader / @babel/preset-typescript | ts-loader / swc-loader (swc-transformer-react) / experiments.typescript |
| CSS 方案 | css-loader + style-loader | experiments.css (原生支持) 或 traditional 方案 |
| 脚手架工具 | Create React App (推荐) | CRA (维护模式) → 推荐 Vite 或 Remix |
| CLI 工具 | webpack-cli 4.x | webpack-cli 7 + create-webpack-app |
| SSR 方案 | 基础 SSR 示例 | 支持 React 19 Server Components + Actions |
| 架构图 | 无 | Mermaid 构建架构图 + 决策树 |
传统 Web 开发强调样式、结构、逻辑分离,以此降低技术复杂度。但 React 认为渲染逻辑本质上与其它 UI 逻辑存在内在耦合关系,所以提倡将结构、逻辑与样式共同存放在同一文件中,以"组件"这种松散耦合结构实现关注点分离,并为此设计实现了一套 JavaScript-XML(JSX) 技术,以支持在 JavaScript 中编写 Template 代码。
// React 19 新特性示例
import { use, useActionState, useOptimistic } from 'react';
// 1. use() Hook - 在组件中读取 Promise 和 Context
function Comment({ commentPromise }) {
const comment = use(commentPromise); // 自动 suspense
return <p>{comment.text}</p>;
}
// 2. Actions - 表单处理简化
async function updateName(name) {
const error = await updateUserName(name);
if (error) throw error;
return '成功更新';
}
// 3. useOptimistic - 乐观更新
function LikeButton({ initialLikes }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, delta) => state + delta
);
// ...
}绝大多数情况下,我们都会使用 JSX 方式编写 React 组件,但问题在于浏览器并不支持这种代码,为此我们首先需要借助构建工具将 JSX 等价转化为标准 JavaScript 代码。
在 Webpack 中可以借助 babel-loader,并使用 React 预设规则集 @babel/preset-react ,完成 JSX 到 JavaScript 的转换。
yarn add -D webpack@5.107.0 webpack-cli@7 babel-loader@9 @babel/core@7 @babel/preset-react@7
yarn add react@19 react-dom@19
yarn add -D @pmmmwh/react-refresh-webpack-plugin@0.6.2 react-refresh@0.14
yarn add -D typescript@5 @babel/preset-typescript@7const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
entry: './src/index.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isDevelopment ? '[name].js' : '[name].[contenthash].js',
clean: true,
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-react', {
runtime: 'automatic', // ✅ 推荐使用 automatic runtime
development: isDevelopment,
importSource: undefined, // 自定义 JSX 运行时源
}],
'@babel/preset-typescript', // TypeScript 支持
],
plugins: [
// ✅ 仅在开发环境启用 Fast Refresh
isDevelopment && require.resolve('react-refresh/babel'),
].filter(Boolean),
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
// ✅ Fast Refresh 插件(仅开发环境)
isDevelopment && new ReactRefreshWebpackPlugin({
overlay: {
sockIntegration: 'webpack-dev-server',
module: isDevelopment,
},
}),
].filter(Boolean),
devServer: {
hot: true, // 启用 HMR
open: true,
historyApiFallback: true,
client: {
overlay: {
errors: true,
warnings: false,
},
},
},
};// 源代码 - 需要 import React
import React from 'react';
const Component = () => {
return <div className="hello">Hello World</div>;
};
// 编译结果 - classic runtime 调用 React.createElement
var _react = require("react");
var Component = function Component() {
return /*#__PURE__*/ _react.createElement("div", {
className: "hello",
children: "Hello World",
});
};// 源代码 - 无需 import React
const Component = () => {
return <div className="hello">Hello World</div>;
};
// 编译结果 - 自动导入 jsx-runtime
import { jsx as _jsx } from "react/jsx-runtime";
const Component = () => {
return _jsx("div", {
className: "hello",
children: "Hello World",
});
};Automatic 模式的优势:
import Reactjsx-runtime(体积更小)重要变更:
react-hot-loader已于 2020 年停止维护,官方推荐使用 Fast Refresh(由@pmmmwh/react-refresh-webpack-plugin提供)。这是 React 官方支持的热更新方案,提供更快的更新速度和更好的状态保持能力。
| 特性 | react-hot-loader (旧) | Fast Refresh (新) ✅ |
|---|---|---|
| 维护状态 | ❌ 已废弃 (2020) | ✅ 活跃维护 (v0.6.2) |
| 状态保持 | ⚠️ 部分 | ✅ 完善(Hook 状态、ref、闭包) |
| 更新速度 | 较慢 | 快 10-50x |
| 错误恢复 | ❌ 需要刷新页面 | ✅ 自动恢复 |
| React 版本 | 16.x | 16.13+ / 17 / 18 / 19 |
| TypeScript | 需要额外配置 | ✅ 原生支持 |
| Server Components | ❌ 不支持 | ✅ 支持 |
// webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-react', {
runtime: 'automatic',
development: isDevelopment,
}],
],
plugins: [
isDevelopment && 'react-refresh/babel',
].filter(Boolean),
},
},
],
},
],
},
plugins: [
isDevelopment && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
};Babel 配置文件 (.babelrc.json):
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic",
"development": true
}]
],
"env": {
"development": {
"plugins": ["react-refresh/babel"]
}
}
}优势:SWC 使用 Rust 编写,比 Babel 快 20-70 倍,特别适合大型项目。
yarn add -D swc-loader@0.2 @swc/core@1.7// webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
development: isDevelopment,
refresh: isDevelopment, // ✅ 启用 Fast Refresh
},
},
},
},
},
},
],
},
plugins: [
isDevelopment && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
};SWC 性能对比:
| 操作 | Babel | SWC | 提升 |
|---|---|---|---|
| 1000 个文件编译 | ~12s | ~0.3s | 40x |
| 首次启动 | ~8s | ~1.5s | 5x |
| 增量编译 | ~500ms | ~20ms | 25x |
| 内存占用 | ~400MB | ~80MB | 5x |
上述数据为数量级参考(不同硬件、项目结构下差异较大),实际收益请以自身项目实测为准。
yarn add -D ts-loader@9 react-refresh-typescript// webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript').default;
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: isDevelopment, // ✅ 开发模式跳过类型检查
getCustomTransformers: () => ({
before: [
isDevelopment && ReactRefreshTypeScript(),
].filter(Boolean),
}),
},
},
},
],
},
plugins: [
isDevelopment && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
};配合 ForkTsCheckerWebpackPlugin 进行类型检查:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
plugins: [
isDevelopment && new ForkTsCheckerWebpackPlugin({
typescript: {
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}),
].filter(Boolean),
};new ReactRefreshWebpackPlugin({
// 错误覆盖层配置
overlay: {
sockIntegration: 'webpack-dev-server', // or 'webpack-hot-middleware'
sockHost: undefined,
sockPort: undefined,
sockPath: '/ws',
module: isDevelopment,
entry: undefined,
moduleUrlPrefix: '',
warnings: false, // 是否显示警告
errors: true, // 是否显示错误
},
// 排除特定模块
exclude: [
/node_modules\/some-package/,
],
// 包含特定模块(默认所有 .[jt]sx? 文件)
include: undefined,
// 自定义资源根目录
resourceRoot: '/src',
});| 问题 | 原因 | 解决方案 |
|---|---|---|
| Fast Refresh 不生效 | 忘记添加 react-refresh/babel 插件 | 检查 Babel 配置 |
| 状态丢失 | 组件导出方式改变 | 保持稳定的导出结构 |
| 全屏刷新而非局部更新 | Hook 数量或顺序改变 | 使用 eslint-plugin-react-hooks |
| 错误后无法恢复 | 未正确配置 overlay | 确保 overlay.module: true |
| TypeScript 类型错误 | transpileOnly: true 跳过检查 | 使用 ForkTsCheckerWebpackPlugin |
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
// ...其他配置
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
inject: true, // 自动注入 script 标签
minify: !isDevelopment ? {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
} : false,
meta: {
viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no',
},
}),
],
};HTML 模板文件 (public/index.html):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="React App with Webpack 5.107" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>module.exports = {
devServer: {
// 基础配置
static: {
directory: path.join(__dirname, 'public'),
publicPath: '/',
serveIndex: true,
watch: {
ignored: /node_modules/,
},
},
// 服务器配置
port: 3000,
host: 'localhost',
hot: true, // ✅ 启用 HMR
open: true,
// 路由配置
historyApiFallback: true, // SPA 路由支持
compress: true, // 启用 gzip 压缩
// 代理配置(API 请求)
proxy: [
{
context: ['/api'],
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
pathRewrite: { '^/api': '' },
},
],
// 客户端配置
client: {
logging: 'info',
overlay: {
errors: true,
warnings: false,
},
progress: true, // 显示编译进度
reconnect: 5, // 断线重连次数
},
// 性能优化
devMiddleware: {
writeToDisk: false, // 不写入磁盘(内存中)
stats: 'minimal',
},
// 安全配置
headers: {
'Access-Control-Allow-Origin': '*',
'X-Custom-Header': 'dev',
},
// HTTPS(可选)
// https: true,
},
};客户端入口 (src/index.jsx):
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// ✅ 启用 Hot Module Acceptance(可选,用于非组件模块)
if (module.hot) {
module.hot.accept();
}App 组件 (src/App.jsx):
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<div className="app">
<h1>Hello React 19 + Webpack 5.107!</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}
export default App;{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx", // ✅ React 17+ automatic runtime
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true // ✅ 让 Babel 处理输出
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Webpack 配置:
{
test: /\.[jt]sx?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
['@babel/preset-react', {
runtime: 'automatic',
}],
],
},
},
}{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'swc-loader',
options: {
jsc: {
target: 'es2020',
parser: {
syntax: 'typescript',
tsx: true,
decorators: true,
dynamicImport: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
},
}module.exports = {
experiments: {
// ✅ Webpack 5.107+ 内置 TypeScript 支持(无需额外 loader)
typescript: true,
asyncWebAssembly: true,
layers: true,
lazyCompilation: {
entries: false,
imports: true,
},
outputModule: true,
syncWebAssembly: true,
topLevelAwait: true,
// 注意:实验性功能请查看具体版本支持
},
};yarn add -D less less-loadermodule.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: isDevelopment
? '[path][name]__[local]'
: '[hash:base64:8]',
},
},
},
{
loader: 'postcss-loader', // 可选:PostCSS 处理
},
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#1890ff',
},
},
},
},
],
},
],
},
};yarn add -D sass sass-loader{
test: /\.scss$/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
sassOptions: {
fiber: false, // Sass 不再需要 fiber 加速
},
},
},
],
}{
test: /\.module\.(css|less|scss)$/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
auto: true, // 自动识别 .module.xxx 文件
localIdentName: isDevelopment
? '[local]_[hash:base64:5]'
: '[hash:base64:8]',
exportLocalsConvention: 'camelCaseOnly',
},
},
},
'postcss-loader',
'less-loader', // 或 sass-loader
],
}module.exports = {
module: {
rules: [
// 图片资源
{
test: /\.(png|jpe?g|gif|webp|avif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 小于 8kb 转 base64
},
},
generator: {
filename: 'images/[name].[hash:8][ext]',
},
},
// 字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]',
},
},
// SVG(作为 React 组件使用)
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
// 音视频资源
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i,
type: 'asset/resource',
generator: {
filename: 'media/[name].[hash:8][ext]',
},
},
],
},
};SVG 作为 React 组件使用:
import { ReactComponent as Logo } from './logo.svg';
function Header() {
return <Logo width={100} height={40} />;
}yarn add -D style-loader css-loader postcss-loader mini-css-extract-pluginconst MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
oneOf: [
// CSS Modules
{
test: /\.module\.css$/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: isDevelopment
? '[path][name]__[local]'
: '[hash:base64:8]',
},
importLoaders: 1,
},
},
'postcss-loader',
],
},
// 普通 CSS
{
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
],
},
],
},
],
},
plugins: [
!isDevelopment && new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[id].[contenthash:8].css',
}),
].filter(Boolean),
};PostCSS 配置 (postcss.config.js):
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-preset-env')({ stage: 3 }),
...(isDevelopment ? [] : [
require('cssnano')({ preset: 'default' }),
]),
],
};注意:此功能仍在实验阶段,具体支持情况请参考 Webpack 5.107 官方文档。以下是预期配置:
module.exports = {
experiments: {
// 如果支持原生 CSS 处理
css: true, // 或具体配置对象
},
module: {
rules: [
{
test: /\.css$/,
type: 'asset/source', // 或 'asset/resource'
// 可能不需要额外的 loader
},
],
},
};yarn add -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -ptailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
corePlugins: {
preflight: false, // 禁用默认 reset(如果需要)
},
};PostCSS 配置:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};CSS 文件:
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 自定义样式 */
.custom-button {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 小型项目 | style-loader + css-loader | 简单快速 |
| 中型项目 | CSS Modules + PostCSS | 作用域隔离 |
| 大型项目 | Tailwind CSS | 原子化 CSS,高性能 |
| 设计系统 | CSS-in-JS (styled-components) | 动态主题 |
| 性能优先 | Vanilla Extract | 零运行时开销 |
React 19 引入了 Server Components 和 Actions,使得 SSR 方案更加现代化。以下是基于 Webpack 5.107 的完整 SSR 配置。
项目结构:
react-ssr-example/
├── package.json
├── server.js # Express 服务器
├── src/
│ ├── App.jsx # 根组件
│ ├── App.css # 样式文件
│ ├── entry-client.jsx # 客户端入口
│ └── entry-server.jsx # 服务端入口
├── webpack.base.js # 公共配置
├── webpack.client.js # 客户端构建配置
└── webpack.server.js # 服务端构建配置entry-client.jsx)import { createRoot, hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
// React 19 支持 hydrateRoot
hydrateRoot(container, <App />);entry-server.jsx)import React from 'react';
import App from './App';
import { renderToString } from 'react-dom/server';
export default () => renderToString(<App />);server.js)const express = require('express');
const fs = require('fs');
const path = require('path');
const renderToString = require('./dist/server').default;
const server = express();
// 读取 manifest 文件获取客户端资源路径
const clientManifest = JSON.parse(
fs.readFileSync(path.join(__dirname, './dist/manifest-client.json'), 'utf-8')
);
server.get('/', (req, res) => {
const html = renderToString();
res.send(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React SSR Example</title>
${clientManifest['client.css']
? `<link rel="stylesheet" href="${clientManifest['client.css']}" />`
: ''}
</head>
<body>
<div id="root">${html}</div>
<!-- 注入客户端脚本 -->
<script src="${clientManifest['client.js']}"></script>
</body>
</html>
`);
});
server.use(express.static('./dist'));
server.listen(3000, () => {
console.log('✅ SSR Server running at http://localhost:3000');
});webpack.base.js)const path = require('path');
module.exports = {
mode: 'none',
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: 'node >= 14',
modules: false,
}],
'@babel/preset-typescript',
['@babel/preset-react', {
runtime: 'automatic',
}],
],
},
},
},
],
},
optimization: {
minimize: false, // SSR 不需要压缩
},
};webpack.client.js)const path = require('path');
const { merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const base = require('./webpack.base');
module.exports = merge(base, {
mode: 'production',
entry: {
client: path.resolve(__dirname, './src/entry-client.jsx'),
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[contenthash:8].js',
publicPath: '/',
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
},
],
},
plugins: [
// ✅ 生成 manifest 文件供服务端使用
new WebpackManifestPlugin({
fileName: 'manifest-client.json',
filter: (file) => file.isInitial || file.path.endsWith('.css'),
}),
// 抽离 CSS
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[id].[contenthash:8].css',
}),
// 生成 HTML 模板(可选)
new HtmlWebpackPlugin({
template: './public/index.html',
inject: true,
minify: {
collapseWhitespace: true,
removeComments: true,
},
}),
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
});webpack.server.js)const path = require('path');
const { merge } = require('webpack-merge');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const base = require('./webpack.base');
module.exports = merge(base, {
mode: 'none',
target: 'node', // ✅ 目标环境为 Node.js
entry: {
server: path.resolve(__dirname, './src/entry-server.jsx'),
},
output: {
path: path.resolve(__dirname, './dist'),
filename: 'server.js',
libraryTarget: 'commonjs2', // ✅ CommonJS 格式
clean: false, // 不清除客户端产物
},
externals: [
// ✅ 不打包 Node.js 内置模块
require('webpack-node-externals')(),
],
module: {
rules: [
// 服务端不需要 CSS 内容,只需导出路径
{
test: /\.css$/,
loader: path.resolve(__dirname, './loaders/null-loader.js'),
},
],
},
optimization: {
nodeEnv: false, // 不注入 process.env.NODE_ENV
},
});自定义 Null Loader (loaders/null-loader.js):
module.exports = function (source) {
this.callback(null, '', null);
return;
};{
"scripts": {
"build:client": "webpack --config ./webpack.client.js",
"build:server": "webpack --config ./webpack.server.js",
"build": "npm run build:client && npm run build:server",
"start": "node ./dist/server.js",
"dev": "concurrently \"npm run build -- --watch\" \"nodemon ./dist/server.js\""
}
}注意:React Server Components (RSC) 是 React 19 的重大特性,需要特殊的构建配置和运行时支持。以下是基于 Next.js 15 的推荐方案。
// webpack.config.rsc.js (概念性配置)
module.exports = {
experiments: {
// 未来可能的原生 RSC 支持
rsc: true,
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
use: {
loader: 'react-server-conditionals/loader',
options: {
// 区分 Server/Client 组件
},
},
},
],
},
};实际项目中建议使用成熟的框架:
| 方案 | 适用场景 | 学习曲线 | 性能 | 定制性 | 维护成本 |
|---|---|---|---|---|---|
| Vite | 新项目、原型、中小型应用 | ⭐ 低 | ⚡⚡⚡ 极高 | ⭐⭐⭐ 高 | ⭐ 低 |
| 手动 Webpack | 大型企业应用、特殊需求 | ⭐⭐⭐⭐ 高 | ⚡⚡ 可控 | ⭐⭐⭐⭐⭐ 最高 | ⭐⭐⭐ 高 |
| Next.js | SSR/SSG、内容网站、电商 | ⭐⭐⭐ 中 | ⚡⚡⚡ 高 | ⭐⭐ 中 | ⭐⭐ 中 |
| Remix | 全栈应用、嵌套路由 | ⭐⭐⭐ 中 | ⚡⚡⚡ 高 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| CRA | 遗留项目、简单迁移 | ⭐ 低 | ⚡⚡ 一般 | ⭐ 低 | ⭐⭐ 中(维护模式) |
当前状态:❌ 维护模式(2022 年起)
npx create-react-app my-app
cd my-app
npm start优点:
缺点:
适用场景:
核心优势:完全掌控、可扩展性强
完整配置示例:
// webpack.config.js (完整版)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const { DefinePlugin, ProvidePlugin } = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devtool: isDevelopment ? 'eval-cheap-module-source-map' : 'source-map',
entry: {
main: './src/index.jsx',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: isDevelopment
? '[name].js'
: '[name].[contenthash:8].js',
chunkFilename: isDevelopment
? '[name].chunk.js'
: '[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[hash:8][ext]',
publicPath: '/',
clean: true,
},
cache: {
type: 'filesystem',
version: '1.0',
buildDependencies: {
config: [__filename],
},
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@assets': path.resolve(__dirname, 'src/assets'),
'@styles': path.resolve(__dirname, 'src/styles'),
},
symlinks: false,
},
module: {
rules: [
// JSX/TSX 处理
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false,
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead',
modules: false,
useBuiltIns: 'usage',
corejs: 3.30,
}],
'@babel/preset-typescript',
['@babel/preset-react', {
runtime: 'automatic',
development: isDevelopment,
importSource: undefined,
}],
],
plugins: [
isDevelopment && 'react-refresh/babel',
'@babel/plugin-transform-runtime',
].filter(Boolean),
},
},
},
// CSS 处理
{
test: /\.css$/,
oneOf: [
{
test: /\.module\.css$/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: isDevelopment
? '[path][name]__[local]'
: '[hash:base64:8]',
exportLocalsConvention: 'camelCaseOnly',
},
importLoaders: 2,
sourceMap: isDevelopment,
},
},
'postcss-loader',
],
},
{
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2,
sourceMap: isDevelopment,
},
},
'postcss-loader',
],
},
],
},
// Less 处理
{
test: /\.less$/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
},
},
},
],
},
// 静态资源
{
test: /\.(png|jpe?g|gif|webp|avif|ico|bmp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
],
},
plugins: [
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(isDevelopment ? 'development' : 'production'),
__DEV__: isDevelopment,
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
favicon: path.resolve(__dirname, 'public/favicon.ico'),
inject: true,
minify: !isDevelopment && {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}),
!isDevelopment && new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[id].[contenthash:8].css',
ignoreOrder: true,
}),
!isDevelopment && new WebpackManifestPlugin({
fileName: 'manifest.json',
publicPath: '',
generate: (seed, entries, entrypoints) => {
const manifest = {};
for (const [key, value] of Object.entries(entries)) {
manifest[key] = value.map(item => item.path);
}
return { ...seed, ...manifest };
},
}),
isDevelopment && new ReactRefreshWebpackPlugin({
overlay: {
sockIntegration: 'webpack-dev-server',
module: isDevelopment,
},
}),
isDevelopment && new ForkTsCheckerWebpackPlugin({
typescript: {
diagnosticOptions: {
semantic: true,
syntactic: true,
},
mode: 'write-references',
},
}),
new ProvidePlugin({
React: 'react',
}),
].filter(Boolean),
optimization: {
minimize: !isDevelopment,
minimizer: [
new TerserPlugin({
parallel: true,
extractComments: false,
terserOptions: {
parse: {
ecma: 2020,
},
compress: {
ecma: 2015,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 2015,
comments: false,
ascii_only: true,
},
},
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
maxInitialRequests: 25,
minSize: 20000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)?.[1];
return `vendor.${packageName.replace('@', '')}`;
},
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
name: 'common',
},
},
},
runtimeChunk: {
name: 'runtime',
},
},
performance: {
hints: isDevelopment ? false : 'warning',
maxEntrypointSize: 512 * 1024,
maxAssetSize: 512 * 1024,
},
devServer: {
static: {
directory: path.join(__dirname, 'public'),
publicPath: '/',
},
port: 3000,
hot: true,
open: true,
historyApiFallback: true,
compress: true,
client: {
overlay: {
errors: true,
warnings: false,
},
progress: true,
},
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
},
},
},
};优点:
缺点:
适用场景:
安装:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev优点:
缺点:
适用场景:
安装:
npx create-remix@latest my-remix-app
cd my-remix-app
npm run dev优点:
缺点:
适用场景:
| 评估维度 | CRA | 手动 Webpack | Vite | Remix |
|---|---|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 性能 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 定制性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 可扩展性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 社区活跃度 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 未来发展 | ⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| React 19 支持 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 学习成本 | 低 | 高 | 中 | 中高 |
| 推荐指数 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
runtime: 'automatic' 模式,无需手动导入 React@pmmmwh/react-refresh-webpack-plugin 替代废弃的 react-hot-loaderfrontend:
framework: React 19
bundler: Webpack 5.107 / Vite 5
language: TypeScript 5.4+
styling: Tailwind CSS 3.4 / CSS Modules
testing: Vitest + Testing Library + Playwright
development:
hmr: @pmmmwh/react-refresh-webpack-plugin 0.6.2
linter: ESLint 9 + Prettier 3
formatter: Prettier 3
package_manager: pnpm 9 / yarn 4
build:
code_splitting: 动态 import + SplitChunks
caching: Filesystem Cache(内置持久化缓存)
optimization: Terser + CssMinimizer
monitoring: Bundle Analyzer + Speed Measure
deployment:
hosting: Vercel / Netlify / Cloudflare Pages
ci_cd: GitHub Actions / GitLab CI
cdn: Cloudflare / AWS CloudFrontQ1: 应该选择 Babel 还是 SWC? A: 新项目推荐 SWC(性能优势明显),遗留项目可逐步迁移。如果团队熟悉 Babel 且无性能瓶颈,继续使用即可。
Q2: Fast Refresh 和 HMR 有什么区别? A: Fast Refresh 是 HMR 的超集,专门为 React 优化。它能在保留组件状态的情况下进行热更新,而传统 HMR 可能导致状态丢失。
Q3: CRA 还能用吗? A: 可以,但不再推荐新项目使用。CRA 已进入维护模式,不会获得重大更新。建议新项目使用 Vite 或手动配置 Webpack。
Q4: 如何从 CRA 迁移到 Vite?
A: 可以使用 vite-plugin-react-cra-migrate 等工具辅助迁移,主要工作是调整配置文件和路径别名。
Q5: React 19 的 Server Components 如何配置? A: 目前推荐使用 Next.js 15 或 Remix 等框架来获得完整的 RSC 支持。手动配置 Webpack 支持 RSC 仍处于实验阶段。
深度题:React JSX 经过 Webpack 转换后的结果与 Vue SFC 转换结果极为相似,为何 Vue 不能复用 Babel 而选择开发一个独立的 vue-loader 插件?请从模板语法、作用域、编译优化等角度分析。
实践题:尝试实现一个支持 Fast Refresh 的完整 Webpack 配置,包括:
架构题:设计一个支持微前端的 Webpack 配置方案,使用 Module Federation 实现多个独立 React 应用之间的共享和通信。
对比题:对比分析 Vite 和 Webpack 在以下场景下的表现:
文档版本:v2.0 | 最后更新:2025-01-22 | 适用范围:Webpack 5.107 + React 18/19 + Node.js 18+
作者声明:本文档基于官方文档和社区最佳实践编写,如有疏漏欢迎指正。生产环境使用前请务必进行充分测试。